import * as CONSTANTS from './constants';
import { api, LightningElement } from 'lwc';
import { normalizeVariant, VARIANT } from 'lightning/inputUtils';
import { classListMutation, normalizeBoolean } from 'lightning/utilsPrivate';
import labelNone from '@salesforce/label/LightningLookup.none';
import formFactor from '@salesforce/client/formFactor';

const i18n = {
    none: labelNone,
};

export default class LightningLookup extends LightningElement {
    // ================================================================================
    // PUBLIC PROPERTIES
    // ================================================================================
    /**
     * Checks the lookup validity, and fires an 'invalid' event if it's in invalid state.
     * @return {Boolean} - The validity status of the lookup.
     */
    @api
    checkValidity() {
        if (this._lookupElement) {
            return this._lookupElement.checkValidity();
        }

        return false;
    }

    /**
     * @return {Boolean} - Indicates whether the field is disabled.
     */
    @api
    get disabled() {
        if (this._lookupElement) {
            this._disabled = this._lookupElement.disabled;
        }

        return this._disabled;
    }

    /**
     * Sets whether the field is disabled.
     * @param {Boolean} value - A flag to mark the field as disabled.
     */
    set disabled(value) {
        this._disabled = normalizeBoolean(value);

        if (this._lookupElement) {
            this._lookupElement.disabled = this._disabled;
        }
    }

    /**
     * @return {String} - The field name.
     */
    @api
    get fieldName() {
        if (this._lookupElement) {
            this._fieldName = this._lookupElement.fieldName;
        }

        return this._fieldName;
    }

    /**
     * Sets the field name.
     * @param {String|FieldId} value - The qualified field name.
     */
    set fieldName(value) {
        this._fieldName = value;

        if (this._lookupElement) {
            this._lookupElement.fieldName = this._fieldName;
        }
    }

    /**
     * Sets focus on the input element.
     */
    @api
    focus() {
        if (!this._connected) {
            return;
        }

        if (this._lookupElement) {
            this._lookupElement.focus();
        }
    }

    /**
     * @return {String} - The text label for the field.
     */
    @api
    get label() {
        if (this._lookupElement) {
            this._label = this._lookupElement.label;
        }

        return this._label;
    }

    /**
     * Sets the field text label.
     * @param {String} value - The field text label.
     */
    set label(value) {
        this._label = value;

        if (this._lookupElement) {
            this._lookupElement.label = this._label;
        }
    }

    /**
     * @return {Number} - The maximum number of values supported.
     */
    @api
    get maxValues() {
        if (this._lookupElement) {
            this._maxValues = this._lookupElement.maxValues;
        }

        return this._maxValues;
    }

    /**
     * Sets the maximum number of values that can be inserted into the lookup.
     * @param {Number} value - The maximum number of values for the lookup.
     */
    set maxValues(value) {
        this._maxValues = value;

        if (this._lookupElement) {
            this._lookupElement.maxValues = this._maxValues;
        }
    }

    /**
     * @return {String} - The error message to be displayed when the user enters the text in
     * the input but does not select a valid option.
     */
    @api
    get messageWhenBadInput() {
        if (this._lookupElement) {
            this._messageWhenBadInput = this._lookupElement.messageWhenBadInput;
        }

        return this._messageWhenBadInput;
    }

    /**
     * Sets the error message to be displayed when the user enters the text in the input
     * but does not select a valid option.
     * @param {String} value - The error message.
     */
    set messageWhenBadInput(value) {
        this._messageWhenBadInput = value;

        if (this._lookupElement) {
            this._lookupElement.messageWhenBadInput = this._messageWhenBadInput;
        }
    }

    /**
     * @return {String} - The error message to be displayed when the lookup value
     * is required but is currently missing.
     */
    @api
    get messageWhenValueMissing() {
        if (this._lookupElement) {
            this._messageWhenValueMissing = this._lookupElement.messageWhenValueMissing;
        }

        return this._messageWhenValueMissing;
    }

    /**
     * The error message to be displayed when the lookup value is required but is currently missing.
     * @param {String} value - The error message.
     */
    set messageWhenValueMissing(value) {
        this._messageWhenValueMissing = value;

        if (this._lookupElement) {
            this._lookupElement.messageWhenValueMissing = this._messageWhenValueMissing;
        }
    }

    /**
     * @return {Object} - The source record's objectInfos.
     */
    @api
    get objectInfos() {
        if (this._lookupElement) {
            this._objectInfos = this._lookupElement.objectInfos;
        }

        return this._objectInfos;
    }

    /**
     * Sets the source record's objectInfos.
     * @param {Object} value - The source record's objectInfos.
     */
    set objectInfos(value) {
        this._objectInfos = value;

        if (this._lookupElement) {
            this._lookupElement.objectInfos = this._objectInfos;
        }
    }

    /**
     * @return {Object} - The source record representation.
     */
    @api
    get record() {
        if (this._lookupElement) {
            this._record = this._lookupElement.record;
        }

        return this._record;
    }

    /**
     * Sets the source record representation.
     * @param {Object} value - The source record.
     */
    set record(value) {
        this._record = value;

        if (this._lookupElement) {
            this._lookupElement.record = this._record;
        }
    }

    /**
     * The field info in the object info is not updated based on the layout metadata.
     * It allows field to be marked as required for the given layout.
     * @return {Boolean} - Indicates whether or not the field is required.
     */
    @api
    get required() {
        if (this._lookupElement) {
            this._required = this._lookupElement.required;
        }

        return this._required;
    }

    /**
     * Sets the flag to mark the field as required.
     * @param {Boolean} value - A flag to mark the field as required.
     */
    set required(value) {
        this._required = normalizeBoolean(value);

        if (this._lookupElement) {
            this._lookupElement.required = this._required;
        }
    }

    /**
     * Shows validation message based on the validity status.
     * @return {Boolean} - The validity status of the lookup.
     */
    @api
    reportValidity() {
        if (this._lookupElement) {
            return this._lookupElement.reportValidity();
        }

        return false;
    }

    /**
     * Sets a custom validity message.
     * @param {String} message - The validation message to be shown in an error state.
     */
    @api
    setCustomValidity(message) {
        if (this._lookupElement) {
            this._lookupElement.setCustomValidity(message);
        }
    }

    /**
     * @return {Boolean} - Indicates whether the advanced search option is enabled.
     */
    @api
    get showAdvancedSearch() {
        if (this._lookupElement) {
            this._showAdvancedSearch = this._lookupElement.showAdvancedSearch;
        }

        return this._showAdvancedSearch;
    }

    /**
     * Sets a flag to display advanced search option.
     * @param {Boolean} value - A flag to show advanced search option.
     */
    set showAdvancedSearch(value) {
        this._showAdvancedSearch = normalizeBoolean(value);

        if (this._lookupElement) {
            this._lookupElement.showAdvancedSearch = this._showAdvancedSearch;
        }
    }

    /**
     * TODO - Remove when @wire(getLookupActions) response is invocable.
     * @return {Boolean} - Indicates whether or not to show the create new option.
     */
    @api
    get showCreateNew() {
        if (this._lookupElement) {
            this._showCreateNew = this._lookupElement.showCreateNew;
        }

        return this._showCreateNew;
    }

    /**
     * TODO - Remove when @wire(getLookupActions) response is invocable.
     * Sets the flag to enable or disable create new option.
     * If set to true, a backend validation is made using @wire(getLookupActions) to check if
     * create action from lookup is supported for the given target api, and option is added accordingly.
     * @param {Boolean} value - A flag to enable or disable create new option.
     */
    set showCreateNew(value) {
        this._showCreateNew = normalizeBoolean(value);

        if (this._lookupElement) {
            this._lookupElement.showCreateNew = this._showCreateNew;
        }
    }

    /**
     * Displays a validation message if the lookup is in invalid state.
     */
    @api
    showHelpMessageIfInvalid() {
        if (this._lookupElement) {
            this._lookupElement.showHelpMessageIfInvalid();
        }
    }

    /**
     * Gets the validity constraint of the lookup.
     * @return {Object} - The current validity constraint.
     */
    @api
    get validity() {
        if (this._lookupElement) {
            return this._lookupElement.validity;
        }

        return null;
    }

    /**
     * @return {Array} - An array of selected lookup values.
     */
    @api
    get value() {
        if (this._lookupElement) {
            this._value = this._lookupElement.value;
        }

        return this._value;
    }

    /**
     * Sets the values for the lookup.
     * @param {Array} value - An array of record ids.
     */
    set value(value) {
        this._value = value;

        if (this._lookupElement) {
            this._lookupElement.value = value;
        }
    }

    /**
     * @return {String} - The value of variant.
     */
    @api
    get variant() {
        if (this._lookupElement) {
            this._variant = this._lookupElement.variant;
        }

        return this._variant;
    }

    /**
     * Sets the variant type for the lookup.
     * @param {String} value - The value of variant.
     */
    set variant(value) {
        this._variant = normalizeVariant(value);

        if (this._lookupElement) {
            this._lookupElement.variant = this._variant;
        }

        this.updateClassList();
    }

    // ================================================================================
    // PRIVATE PROPERTIES
    // ================================================================================
    /**
     * Indicates whether or not the component is connected.
     * @type {Boolean}
     */
    _connected = false;

    /**
     * Indicates whether the field is disabled.
     * @type {Boolean}
     */
    _disabled = false;

    /**
     * The qualified field name.
     * @type {String|FieldId}
     */
    _fieldName;

    /**
     * Indicates whether or not the component is loaded on the desktop form factor.
     * @type {Boolean}
     */
    _isDesktop;

    /**
     * Indicates whether or not the inititial props are set on the child lookup element.
     * @type {Boolean}
     */
    _initProps = false;

    /**
     * The text label for the field.
     * @type {String}
     */
    _label = i18n.none;

    /**
     * The lookup DOM element.
     * @type {Object}
     */
    _lookupElement;

    /**
     * The maximum number of values supported.
     * @type {Number}
     */
    _maxValues = 1;

    /**
     * The error message to be displayed when the user enters the text in the input but does
     * not select a valid option.
     * @type {String}
     */
    _messageWhenBadInput;

    /**
     * The error message to be displayed when the lookup value is required but is currently missing.
     * @type {String}
     */
    _messageWhenValueMissing;

    /**
     * The source record's objectInfos.
     * @type {Object}
     */
    _objectInfos;

    /**
     * The source record.
     * @type {Object}
     */
    _record;

    /**
     * Indicates whether or not the field is required.
     * @type {Boolean}
     */
    _required = false;

    /**
     * Indicates whether or not to show advanced search option.
     * @type {Boolean}
     */
    _showAdvancedSearch = true;

    /**
     * Indicates whether or not to show the create new option.
     * @type {Boolean}
     */
    _showCreateNew = false;

    /**
     * An array of selected values for the lookup.
     * @type {Array}
     */
    _value;

    /**
     * Variant type of the lookup.
     * @type {String}
     */
    _variant = VARIANT.STANDARD;

    // ================================================================================
    // ACCESSOR METHODS
    // ================================================================================
    /**
     * Indicates whether or not the component is loaded on the desktop form factor.
     * @return {Boolean} - See desc.
     */
    get isDesktop() {
        return this._isDesktop;
    }

    /**
     * Returns the lookup DOM element.
     * @returns {Object} - See desc.
     */
    get lookupElement() {
        if (!this._connected) {
            return null;
        }

        if (this._lookupElement) {
            return this._lookupElement;
        }

        return null;
    }

    // ================================================================================
    // LIFECYCLE METHODS
    // ================================================================================
    constructor() {
        super();
        this._isDesktop = formFactor === CONSTANTS.FORM_FACTOR_DESKTOP;
    }

    connectedCallback() {
        this._connected = true;
        this.classList.add('slds-form-element');
        this.updateClassList();
    }

    disconnectedCallback() {
        this._connected = false;
        this._initProps = false;
        this._lookupElement = undefined;
    }

    renderedCallback() {
        if (!this._lookupElement) {
            const lookupSelector = this._isDesktop
                ? CONSTANTS.LIGHTNING_LOOKUP_DESKTOP
                : CONSTANTS.LIGHTNING_LOOKUP_MOBILE;
            this._lookupElement = this.template.querySelector(lookupSelector);
        }

        if (!this._initProps) {
            this.synchronizeProps();
            this._initProps = true;
        }
    }

    // ================================================================================
    // PRIVATE METHODS
    // ================================================================================
    /**
     * Synchronizes properties with child lookup element.
     */
    synchronizeProps() {
        if (this._lookupElement) {
            this._lookupElement.disabled = this._disabled;
            this._lookupElement.fieldName = this._fieldName;
            this._lookupElement.label = this._label;
            this._lookupElement.maxValues = this._maxValues;
            if (this._messageWhenBadInput !== undefined) {
                this._lookupElement.messageWhenBadInput = this._messageWhenBadInput;
            }
            if (this._messageWhenValueMissing !== undefined) {
                this._lookupElement.messageWhenValueMissing = this._messageWhenValueMissing;
            }
            this._lookupElement.objectInfos = this._objectInfos;
            this._lookupElement.record = this._record;
            this._lookupElement.required = this._required;
            this._lookupElement.showAdvancedSearch = this._showAdvancedSearch;
            this._lookupElement.showCreateNew = this._showCreateNew;
            this._lookupElement.value = this._value;
            this._lookupElement.variant = this._variant;
        }
    }

    /**
     * Updates classList based on the variant.
     */
    updateClassList() {
        classListMutation(this.classList, {
            'slds-form-element_stacked':
                this._variant === VARIANT.LABEL_STACKED,
            'slds-form-element_horizontal':
                this._variant === VARIANT.LABEL_INLINE,
        });
    }
}
