const { __ } = wp.i18n;
const { Component } = wp.element;
const { Dashicon } = wp.components;
const { apiFetch } = wp;

/**
 * Product data cache.
 * Reduces the number of API calls and makes UI smoother and faster.
 */
const PRODUCT_DATA = {};

/**
 * When the display mode is 'Specific products' search for and add products to the block.
 */
export class ProductsSpecificSelect extends Component {
	/**
	 * Constructor.
	 */
	constructor( props ) {
		super( props );

		this.state = {
			selectedProducts: props.selected_display_setting || [],
		};
	}

	/**
	 * Add a product to the list of selected products.
	 *
	 * @param id int Product ID.
	 */
	addOrRemoveProduct( id ) {
		let selectedProducts = this.state.selectedProducts;

		if ( ! selectedProducts.includes( id ) ) {
			selectedProducts.push( id );
		} else {
			selectedProducts = selectedProducts.filter( ( product ) => product !== id );
		}

		this.setState( {
			selectedProducts: selectedProducts,
		} );

		/**
		 * We need to copy the existing data into a new array.
		 * We can't just push the new product onto the end of the existing array because Gutenberg seems
		 * to do some sort of check by reference to determine whether to *actually* update the attribute
		 * and will not update it if we just pass back the same array with an extra element on the end.
		 */
		this.props.update_display_setting_callback( selectedProducts.slice() );
	}

	/**
	 * Render the product specific select screen.
	 */
	render() {
		return (
			<div className="wc-products-list-card wc-products-list-card--specific">
				<ProductsSpecificSearchField
					addOrRemoveProductCallback={ this.addOrRemoveProduct.bind( this ) }
					selectedProducts={ this.state.selectedProducts }
				/>
				<ProductSpecificSelectedProducts
					columns={ this.props.attributes.columns }
					productIds={ this.state.selectedProducts }
					addOrRemoveProduct={ this.addOrRemoveProduct.bind( this ) }
				/>
			</div>
		);
	}
}

/**
 * Product search area
 */
class ProductsSpecificSearchField extends Component {
	/**
	 * Constructor.
	 */
	constructor( props ) {
		super( props );

		this.state = {
			searchText: '',
			dropdownOpen: false,
		};

		this.updateSearchResults = this.updateSearchResults.bind( this );
		this.setWrapperRef = this.setWrapperRef.bind( this );
		this.handleClickOutside = this.handleClickOutside.bind( this );
		this.isDropdownOpen = this.isDropdownOpen.bind( this );
	}

	/**
	 * Hook in the listener for closing menu when clicked outside.
	 */
	componentDidMount() {
		document.addEventListener( 'mousedown', this.handleClickOutside );
	}

	/**
	 * Remove the listener for closing menu when clicked outside.
	 */
	componentWillUnmount() {
		document.removeEventListener( 'mousedown', this.handleClickOutside );
	}

	/**
	 * Set the wrapper reference.
	 *
	 * @param node DOMNode
	 */
	setWrapperRef( node ) {
		this.wrapperRef = node;
	}

	/**
	 * Close the menu when user clicks outside the search area.
	 */
	handleClickOutside( event ) {
		if ( this.wrapperRef && ! this.wrapperRef.contains( event.target ) ) {
			this.setState( {
				searchText: '',
			} );
		}
	}

	isDropdownOpen( isOpen ) {
		this.setState( {
			dropdownOpen: !! isOpen,
		} );
	}

	/**
	 * Event handler for updating results when text is typed into the input.
	 *
	 * @param evt Event object.
	 */
	updateSearchResults( evt ) {
		this.setState( {
			searchText: evt.target.value,
		} );
	}

	/**
	 * Render the product search UI.
	 */
	render() {
		const divClass = 'wc-products-list-card__search-wrapper';

		return (
			<div className={ divClass + ( this.state.dropdownOpen ? ' ' + divClass + '--with-results' : '' ) } ref={ this.setWrapperRef }>
				<div className="wc-products-list-card__input-wrapper">
					<Dashicon icon="search" />
					<input type="search"
						className="wc-products-list-card__search"
						value={ this.state.searchText }
						placeholder={ __( 'Search for products to display' ) }
						onChange={ this.updateSearchResults }
					/>
				</div>
				<ProductSpecificSearchResults
					searchString={ this.state.searchText }
					addOrRemoveProductCallback={ this.props.addOrRemoveProductCallback }
					selectedProducts={ this.props.selectedProducts }
					isDropdownOpenCallback={ this.isDropdownOpen }
				/>
			</div>
		);
	}
}

/**
 * Render product search results based on the text entered into the textbox.
 */
class ProductSpecificSearchResults extends Component {
	/**
	 * Constructor.
	 */
	constructor( props ) {
		super( props );
		this.state = {
			products: [],
			query: '',
			loaded: false,
		};

		this.updateResults = this.updateResults.bind( this );
		this.getQuery = this.getQuery.bind( this );
	}

	/**
	 * Get the preview when component is first loaded.
	 */
	componentDidMount() {
		this.updateResults();
	}

	/**
	 * Update the preview when component is updated.
	 */
	componentDidUpdate() {
		if ( this.getQuery() !== this.state.query ) {
			this.updateResults();
		}
	}

	/**
	 * Get the endpoint for the current state of the component.
	 *
	 * @return string
	 */
	getQuery() {
		if ( ! this.props.searchString.length ) {
			return '';
		}

		return '/wc-pb/v3/products?per_page=10&status=publish&search=' + this.props.searchString;
	}

	/**
	 * Update the search results.
	 */
	updateResults() {
		const self = this;
		const query = this.getQuery();

		self.setState( {
			query: query,
			loaded: false,
		} );

		if ( query.length ) {
			apiFetch( { path: query } ).then( ( products ) => {
				// Only update the results if they are for the latest query.
				if ( query === self.getQuery() ) {
					self.setState( {
						products: products,
						loaded: true,
					} );
				}
			} );
		} else {
			self.setState( {
				products: [],
				loaded: true,
			} );
		}
	}

	/**
	 * Render.
	 */
	render() {
		if ( ! this.state.loaded || ! this.state.query.length ) {
			return null;
		}

		if ( 0 === this.state.products.length ) {
			return <span className="wc-products-list-card__search-no-results"> { __( 'No products found' ) } </span>;
		}

		// Populate the cache.
		for ( const product of this.state.products ) {
			PRODUCT_DATA[ product.id ] = product;
		}

		return <ProductSpecificSearchResultsDropdown
			products={ this.state.products }
			addOrRemoveProductCallback={ this.props.addOrRemoveProductCallback }
			selectedProducts={ this.props.selectedProducts }
			isDropdownOpenCallback={ this.props.isDropdownOpenCallback }
		/>;
	}
}

/**
 * The dropdown of search results.
 */
class ProductSpecificSearchResultsDropdown extends Component {
	/**
	 * Set the state of the dropdown to open.
	 */
	componentDidMount() {
		this.props.isDropdownOpenCallback( true );
	}

	/**
	 * Set the state of the dropdown to closed.
	 */
	componentWillUnmount() {
		this.props.isDropdownOpenCallback( false );
	}

	/**
	 * Render dropdown.
	 */
	render() {
		const { products, addOrRemoveProductCallback, selectedProducts } = this.props;

		const productElements = [];

		for ( const product of products ) {
			productElements.push(
				<ProductSpecificSearchResultsDropdownElement
					product={ product }
					addOrRemoveProductCallback={ addOrRemoveProductCallback }
					selected={ selectedProducts.includes( product.id ) }
					key={ product.id }
				/>
			);
		}

		return (
			<div role="menu" className="wc-products-list-card__search-results" aria-orientation="vertical" aria-label={ __( 'Products list' ) }>
				<div>
					{ productElements }
				</div>
			</div>
		);
	}
}

/**
 * One search result.
 */
class ProductSpecificSearchResultsDropdownElement extends Component {
	/**
	 * Constructor.
	 */
	constructor( props ) {
		super( props );

		this.handleClick = this.handleClick.bind( this );
	}

	/**
	 * Add product to main list and change UI to show it was added.
	 */
	handleClick() {
		this.props.addOrRemoveProductCallback( this.props.product.id );
	}

	/**
	 * Render one result in the search results.
	 */
	render() {
		const product = this.props.product;
		const icon = this.props.selected ? <Dashicon icon="yes" /> : null;
		const productImage = product.images.length !== 0 ? 
			(<img src={ product.images[ 0 ].src } alt={ product.name } />) : null;

		/* eslint-disable jsx-a11y/click-events-have-key-events */
		/* eslint-disable jsx-a11y/no-static-element-interactions */
		/* reason: This interface will be deprecated, the new component is accessible. */
		return (
			<div className={ 'wc-products-list-card__content' + ( this.props.selected ? ' wc-products-list-card__content--added' : '' ) } onClick={ this.handleClick }>
				{ productImage }
				<span className="wc-products-list-card__content-item-name">{ product.name }</span>
				{ icon }
			</div>
		);
		/* eslint-enable */
	}
}

/**
 * List preview of selected products.
 */
class ProductSpecificSelectedProducts extends Component {
	/**
	 * Constructor
	 */
	constructor( props ) {
		super( props );
		this.state = {
			query: '',
			loaded: false,
		};

		this.updateProductCache = this.updateProductCache.bind( this );
		this.getQuery = this.getQuery.bind( this );
	}

	/**
	 * Get the preview when component is first loaded.
	 */
	componentDidMount() {
		this.updateProductCache();
	}

	/**
	 * Update the preview when component is updated.
	 */
	componentDidUpdate() {
		if ( this.state.loaded && this.getQuery() !== this.state.query ) {
			this.updateProductCache();
		}
	}

	/**
	 * Get the endpoint for the current state of the component.
	 */
	getQuery() {
		if ( ! this.props.productIds.length ) {
			return '';
		}

		// Determine which products are not already in the cache and only fetch uncached products.
		const uncachedProducts = [];
		for ( const productId of this.props.productIds ) {
			if ( ! PRODUCT_DATA.hasOwnProperty( productId ) ) {
				uncachedProducts.push( productId );
			}
		}

		return uncachedProducts.length ? '/wc-pb/v3/products?include=' + uncachedProducts.join( ',' ) : '';
	}

	/**
	 * Add newly fetched products to the cache.
	 */
	updateProductCache() {
		const self = this;
		const query = this.getQuery();

		self.setState( {
			query: query,
			loaded: false,
		} );

		// Add new products to cache.
		if ( query.length ) {
			apiFetch( { path: query } ).then( ( products ) => {
				if ( products.length ) {
					for ( const product of products ) {
						PRODUCT_DATA[ product.id ] = product;
					}
				}

				self.setState( {
					loaded: true,
				} );
			} );
		}
	}

	/**
	 * Render.
	 */
	render() {
		const self = this;
		const productElements = [];

		for ( const productId of this.props.productIds ) {
			// Skip products that aren't in the cache yet or failed to fetch.
			if ( ! PRODUCT_DATA.hasOwnProperty( productId ) ) {
				continue;
			}

			const productData = PRODUCT_DATA[ productId ];
			const productImage = productData.images.length !== 0 ? 
				(<img src={ productData.images[ 0 ].src } alt={ productData.name } />) : null;

			productElements.push(
				<li className="wc-products-list-card__item" key={ productData.id + '-specific-select-edit' } >
					<div className="wc-products-list-card__content">
						{ productImage }
						<span className="wc-products-list-card__content-item-name">{ productData.name }</span>
						<button
							type="button"
							id={ 'product-' + productData.id }
							onClick={ function() {
								self.props.addOrRemoveProduct( productData.id );
							} } >
							<Dashicon icon="no-alt" />
						</button>
					</div>
				</li>
			);
		}

		return (
			<div className={ 'wc-products-list-card__results-wrapper wc-products-list-card__results-wrapper--cols-' + this.props.columns }>
				<div role="menu" className="wc-products-list-card__results" aria-orientation="vertical" aria-label={ __( 'Selected products' ) }>

					{ productElements.length > 0 && <h3>{ __( 'Selected products' ) }</h3> }

					<ul>
						{ productElements }
					</ul>
				</div>
			</div>
		);
	}
}
