/* eslint-disable react/jsx-props-no-spreading */
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import Select from 'react-select';
import AsyncSelect from 'react-select/async';
import CreatableSelect from 'react-select/creatable';
import styled, { css } from 'styled-components';
import { Controller } from 'react-hook-form';
import useTranslation from 'next-translate/useTranslation';
import get from 'lodash.get';
import { InputFieldWrapper, InputLabel, InputError, Row } from './styles';
import { validationErrorMessage } from '../../utils/helper';
import Help from './Help';
import RequiredIndicator from './Required/Indicator';
import { theme } from '../../styles';

const reactSelectStyles = {
	default: {
		container: (base) => ({
			...base,
			width: '100%',
		}),
		control: (base) => ({
			...base,
			minHeight: '4.4rem',
			borderRadius: '0.2rem',
		}),

		singleValue: (base) => ({
			...base,
			color: theme.colors.lightGray,
		}),
	},

	rounded: {
		container: (base) => ({
			...base,
			width: '100%',
		}),
		control: (base) => ({
			...base,
			minHeight: '4.4rem',
			borderRadius: `${theme.metrics.baseRadius}rem`,
		}),
		singleValue: (base) => ({
			...base,
			color: theme.colors.lightGray,
		}),
		indicatorSeparator: () => ({
			display: 'none',
		}),
		dropdownIndicator: (base) => ({
			...base,
			color: theme.colors.secondary,
		}),
	},

	gray: {
		container: (base) => ({
			...base,
			width: '100%',
		}),
		control: (base) => ({
			...base,
			backgroundColor: theme.colors.lightGray4,
			borderColor: theme.colors.lightGray4,
			minHeight: '4.4rem',
			borderRadius: `${theme.metrics.baseRadius}rem`,
		}),
		singleValue: (base) => ({
			...base,
			color: theme.colors.lightGray,
		}),
		indicatorSeparator: () => ({
			display: 'none',
		}),
		dropdownIndicator: (base) => ({
			...base,
			color: theme.colors.secondary,
		}),
	},

	lightRounded: {
		container: (base) => ({
			...base,
			width: '100%',
		}),
		control: (base) => ({
			...base,
			minHeight: '4.4rem',
			borderRadius: `${theme.metrics.baseRadius}rem`,
			borderColor: theme.colors.lightGray4,
		}),
		singleValue: (base) => ({
			...base,
			color: theme.colors.darkGray,
		}),
		indicatorSeparator: () => ({
			display: 'none',
		}),
		placeholder: (base) => ({
			...base,
			color: theme.colors.lightGray3,
		}),
	},
};

const styles = css`
	width: 100%;
	margin: 0.5rem 0;
	font-size: 1.4rem;
`;

const StyledSelect = styled(Select)`
	${styles}
`;
const StyledCreatable = styled(CreatableSelect)`
	${styles}
`;
const StyledAsync = styled(AsyncSelect)`
	${styles}
`;

const Hint = styled.span`
	${({ theme: { colors } }) => css`
		color: ${colors.lightGray2};
		margin-bottom: 1rem;
		display: inline-block;
	`}
`;

const SelectField = ({
	name,
	form,
	label,
	help,
	options,
	defaultOptions,
	validation,
	creatable,
	onCreate,
	isMulti,
	callback,
	wrapperCss,
	variant,
	isHidden,
	isLoading,
	instanceId,
	isAsync,
	onChange,
	...selectProps
}) => {
	const { t } = useTranslation(['error']);
	const [needsUpdate, setNeedsUpdate] = useState(true);
	const [internalIsLoading, setInternalIsLoading] = useState(false);
	const [selectOptions, setSelectOptions] = useState(options);

	const { formState: { errors } = {}, control, watch, setValue, getValues } = form;
	const errorObject = get(errors, name);
	const hasError = typeof errorObject !== 'undefined';

	let selectedValue = watch(name);
	if (selectedValue) {
		selectedValue = Array.isArray(selectedValue)
			? selectedValue.map((value) => `${value}`)
			: selectedValue;
		selectedValue = Array.isArray(selectedValue) && !isMulti ? selectedValue[0] : selectedValue;
		selectedValue =
			!Array.isArray(selectedValue) && typeof selectedValue !== 'object'
				? `${selectedValue}`
				: selectedValue;
	}

	/**
	 * Compares each option's label with each other in order to sort them later.
	 *
	 * @param {object} firstOption The first option
	 * @param {object} secondOption The second option
	 * @returns {number}
	 */
	function compareOptions(firstOption, secondOption) {
		const { label: firstLabel } = firstOption;
		const { label: secondLabel } = secondOption;

		return firstLabel.localeCompare(secondLabel);
	}

	// update the select options whenever options prop changes
	useEffect(() => {
		const useOptionsFrom = isAsync ? defaultOptions : options;
		setSelectOptions(useOptionsFrom.sort(compareOptions));
	}, [options, defaultOptions, isAsync]);

	/**
	 * React-select expects value to be in { value: '', label: '' } shape so we run a useEffect
	 * to ensure it's in the right format. This allows this component to be intialized just with the value.
	 */
	useEffect(() => {
		if (!needsUpdate) {
			return;
		}

		if (
			(!options || options.length === 0) &&
			(!defaultOptions || defaultOptions.length === 0)
		) {
			return;
		}

		const useOptionsFrom = isAsync ? defaultOptions : options;

		if (!selectedValue) {
			setNeedsUpdate(false);
			return;
		}

		if (isMulti) {
			setValue(
				name,
				selectedValue.map((value) =>
					useOptionsFrom.find((option) => `${option.value}` === `${value}`),
				),
				{ shouldDirty: true },
			);
		} else if (typeof selectedValue === 'object') {
			setValue(
				name,
				useOptionsFrom.find((option) => `${option.value}` === `${selectedValue.value}`),
				{ shouldDirty: true },
			);
		} else {
			setValue(
				name,
				useOptionsFrom.find((option) => `${option.value}` === `${selectedValue}`),
				{ shouldDirty: true },
			);
		}

		setNeedsUpdate(false);
	}, [selectedValue, options, defaultOptions, name, setValue, isMulti, isAsync, needsUpdate]);

	/**
	 * Handles creating a new element in the select field.
	 *
	 * Only called if `creatable` is true.
	 *
	 * @param {string} inputValue The inserted input value.
	 *
	 */
	const onCreateOption = async (inputValue) => {
		setInternalIsLoading(true);
		const newOption = await onCreate(inputValue);
		setInternalIsLoading(false);
		setSelectOptions([...options, newOption]);

		const currentValue = getValues(name) || [];

		if (isMulti) {
			setValue(name, [...currentValue, newOption], { shouldDirty: true });
		} else {
			setValue(name, newOption, { shouldDirty: true });
		}

		return newOption;
	};

	// eslint-disable-next-line no-nested-ternary
	const Component = creatable ? StyledCreatable : isAsync ? StyledAsync : StyledSelect;

	return (
		<InputFieldWrapper hasError={hasError} customCss={wrapperCss} isHidden={isHidden}>
			{label && (
				<InputLabel htmlFor={name}>
					{label}
					{validation.required && <RequiredIndicator />}
				</InputLabel>
			)}

			<Row>
				<Controller
					control={control}
					rules={validation}
					name={name}
					render={({ field }) => (
						<Component
							id={name}
							className="react-select-container"
							classNamePrefix="react-select"
							aria-label={label}
							aria-required={validation.required}
							options={selectOptions}
							defaultOptions={selectOptions}
							isMulti={isMulti}
							onCreateOption={creatable ? onCreateOption : null}
							isDisabled={internalIsLoading || isLoading || isHidden}
							isLoading={internalIsLoading || isLoading}
							styles={reactSelectStyles[variant]}
							instanceId={instanceId}
							{...selectProps}
							{...field}
							onChange={(selectedValues) => {
								if (typeof callback === 'function') callback(selectedValues);

								if (typeof onChange === 'function')
									return field.onChange(onChange(selectedValues));

								return field.onChange(selectedValues);
							}}
						/>
					)}
				/>
				{help && <Help id={name} label={label} HelpComponent={help} />}
			</Row>
			{creatable && (
				<Hint>
					É possível adicionar novas opções neste campo. Basta digitar a opção e
					pressionar a tecla Enter.
				</Hint>
			)}
			{hasError && Object.keys(errors).length ? (
				<InputError>{validationErrorMessage(errors, name, t)}</InputError>
			) : null}
		</InputFieldWrapper>
	);
};

SelectField.propTypes = {
	name: PropTypes.string.isRequired,
	label: PropTypes.string,
	creatable: PropTypes.bool,
	onCreate: PropTypes.func,
	isMulti: PropTypes.bool,
	form: PropTypes.shape({
		formState: PropTypes.shape({ errors: PropTypes.shape({}) }),
		control: PropTypes.shape({}),
		watch: PropTypes.func,
		setValue: PropTypes.func,
		getValues: PropTypes.func,
	}),
	help: PropTypes.node,
	/**
	 * @see https://react-hook-form.com/api#register
	 */
	validation: PropTypes.shape({
		required: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
	}),
	options: PropTypes.arrayOf(
		PropTypes.shape({
			label: PropTypes.string,
			value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
		}),
	),
	defaultOptions: PropTypes.arrayOf(
		PropTypes.shape({
			label: PropTypes.string,
			value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
		}),
	),
	callback: PropTypes.func,
	wrapperCss: PropTypes.arrayOf(PropTypes.string),
	variant: PropTypes.oneOf(['default', 'rounded', 'gray', 'lightRounded']),
	isHidden: PropTypes.bool,
	isLoading: PropTypes.bool,
	instanceId: PropTypes.string,
	isAsync: PropTypes.bool,
	onChange: PropTypes.func,
};

SelectField.defaultProps = {
	label: '',
	form: {},
	creatable: false,
	onCreate: () => {},
	isMulti: false,
	validation: {},
	options: [],
	defaultOptions: [],
	help: null,
	callback: null,
	wrapperCss: [],
	variant: 'default',
	isHidden: false,
	isLoading: false,
	instanceId: '',
	isAsync: false,
	onChange: null,
};

export default SelectField;