import HTMLElement from '../html-element/HTMLElement.js';
import * as PropertySymbol from '../../PropertySymbol.js';
import ValidityState from '../../validity-state/ValidityState.js';
import HTMLOptionElement from '../html-option-element/HTMLOptionElement.js';
import HTMLOptionsCollection from './HTMLOptionsCollection.js';
import Event from '../../event/Event.js';
import HTMLLabelElementUtility from '../html-label-element/HTMLLabelElementUtility.js';
import HTMLCollection from '../element/HTMLCollection.js';
import NodeTypeEnum from '../node/NodeTypeEnum.js';
import QuerySelector from '../../query-selector/QuerySelector.js';
import ClassMethodBinder from '../../utilities/ClassMethodBinder.js';
import Node from '../node/Node.js';
import Element from '../element/Element.js';
import EventTarget from '../../event/EventTarget.js';
import ElementEventAttributeUtility from '../element/ElementEventAttributeUtility.js';
/**
 * HTML Select Element.
 *
 * Reference:
 * https://developer.mozilla.org/en-US/docs/Web/API/HTMLSelectElement.
 */
export default class HTMLSelectElement extends HTMLElement {
    // Internal properties.
    [PropertySymbol.validationMessage] = '';
    [PropertySymbol.validity] = new ValidityState(this);
    [PropertySymbol.options] = null;
    [PropertySymbol.selectedOptions] = null;
    [PropertySymbol.selectedIndex] = -1;
    [PropertySymbol.proxy];
    /**
     * Constructor.
     */
    constructor() {
        super();
        const methodBinder = new ClassMethodBinder(this, [
            HTMLSelectElement,
            HTMLElement,
            Element,
            Node,
            EventTarget
        ]);
        const proxy = new Proxy(this, {
            get: (target, property) => {
                if (property in target || typeof property === 'symbol') {
                    methodBinder.bind(property);
                    return target[property];
                }
                const index = Number(property);
                if (!isNaN(index)) {
                    return QuerySelector.querySelectorAll(target, 'option')[PropertySymbol.items][index];
                }
            },
            set(target, property, newValue) {
                methodBinder.bind(property);
                if (typeof property === 'symbol') {
                    target[property] = newValue;
                    return true;
                }
                const index = Number(property);
                if (isNaN(index)) {
                    target[property] = newValue;
                    return true;
                }
                if (!newValue || !(newValue instanceof HTMLOptionElement)) {
                    throw new target[PropertySymbol.window].Error(`TypeError: Failed to set an indexed property [${index}] on 'HTMLSelectElement': parameter 2 is not of type 'HTMLOptionElement'.`);
                }
                const options = QuerySelector.querySelectorAll(target, 'option')[PropertySymbol.items];
                const childNodes = target[PropertySymbol.nodeArray];
                while (childNodes.length) {
                    target[PropertySymbol.removeChild](childNodes[0]);
                }
                for (let i = 0; i <= index; i++) {
                    if (i === index) {
                        target[PropertySymbol.appendChild](newValue);
                    }
                    else if (options[i]) {
                        target[PropertySymbol.appendChild](options[i]);
                    }
                    else {
                        target[PropertySymbol.appendChild](target[PropertySymbol.ownerDocument].createElement('option'));
                    }
                }
                return true;
            },
            deleteProperty(target, property) {
                if (typeof property === 'symbol') {
                    delete target[property];
                    return true;
                }
                const index = Number(property);
                if (isNaN(index)) {
                    delete target[property];
                }
                return true;
            },
            ownKeys(target) {
                return Object.keys(QuerySelector.querySelectorAll(target, 'option')[PropertySymbol.items]);
            },
            has(target, property) {
                if (property in target) {
                    return true;
                }
                if (typeof property === 'symbol') {
                    return false;
                }
                const index = Number(property);
                if (!isNaN(index)) {
                    return !!QuerySelector.querySelectorAll(target, 'option')[PropertySymbol.items][index];
                }
                return false;
            },
            defineProperty(target, property, descriptor) {
                methodBinder.preventBinding(property);
                const index = Number(property);
                if (isNaN(index)) {
                    Object.defineProperty(target, property, descriptor);
                    return true;
                }
                if (!descriptor.value || !(descriptor.value instanceof HTMLOptionElement)) {
                    throw new target[PropertySymbol.window].Error(`TypeError: Failed to set an indexed property [${index}] on 'HTMLSelectElement': parameter 2 is not of type 'HTMLOptionElement'.`);
                }
                const options = QuerySelector.querySelectorAll(target, 'option')[PropertySymbol.items];
                const childNodes = target[PropertySymbol.nodeArray];
                while (childNodes.length) {
                    target[PropertySymbol.removeChild](childNodes[0]);
                }
                for (let i = 0; i <= index; i++) {
                    if (i === index) {
                        target[PropertySymbol.appendChild](descriptor.value);
                    }
                    else if (options[i]) {
                        target[PropertySymbol.appendChild](options[i]);
                    }
                    else {
                        target[PropertySymbol.appendChild](target[PropertySymbol.ownerDocument].createElement('option'));
                    }
                }
                return true;
            },
            getOwnPropertyDescriptor(target, property) {
                if (property in target) {
                    return Object.getOwnPropertyDescriptor(target, property);
                }
                const index = Number(property);
                if (isNaN(index)) {
                    return;
                }
                const options = QuerySelector.querySelectorAll(target, 'option')[PropertySymbol.items];
                if (!options[index]) {
                    return;
                }
                return {
                    value: options[index],
                    writable: true,
                    enumerable: true,
                    configurable: true
                };
            }
        });
        this[PropertySymbol.proxy] = proxy;
        this[PropertySymbol.selectNode] = proxy;
        return proxy;
    }
    // Events
    /* eslint-disable jsdoc/require-jsdoc */
    get onchange() {
        return ElementEventAttributeUtility.getEventListener(this, 'onchange');
    }
    set onchange(value) {
        this[PropertySymbol.propertyEventListeners].set('onchange', value);
    }
    get oninput() {
        return ElementEventAttributeUtility.getEventListener(this, 'oninput');
    }
    set oninput(value) {
        this[PropertySymbol.propertyEventListeners].set('oninput', value);
    }
    /* eslint-enable jsdoc/require-jsdoc */
    /**
     * Returns length.
     *
     * @returns Length.
     */
    get length() {
        return QuerySelector.querySelectorAll(this, 'option')[PropertySymbol.items].length;
    }
    /**
     * Returns options.
     *
     * @returns Options.
     */
    get options() {
        if (!this[PropertySymbol.options]) {
            this[PropertySymbol.options] = new HTMLOptionsCollection(PropertySymbol.illegalConstructor, this);
        }
        return this[PropertySymbol.options];
    }
    /**
     * Returns validation message.
     *
     * @returns Validation message.
     */
    get validationMessage() {
        return this[PropertySymbol.validationMessage];
    }
    /**
     * Returns validity.
     *
     * @returns Validity.
     */
    get validity() {
        return this[PropertySymbol.validity];
    }
    /**
     * Returns name.
     *
     * @returns Name.
     */
    get name() {
        return this.getAttribute('name') || '';
    }
    /**
     * Sets name.
     *
     * @param name Name.
     */
    set name(name) {
        this.setAttribute('name', name);
    }
    /**
     * Returns disabled.
     *
     * @returns Disabled.
     */
    get disabled() {
        return this.getAttribute('disabled') !== null;
    }
    /**
     * Sets disabled.
     *
     * @param disabled Disabled.
     */
    set disabled(disabled) {
        if (!disabled) {
            this.removeAttribute('disabled');
        }
        else {
            this.setAttribute('disabled', '');
        }
    }
    /**
     * Returns multiple.
     *
     * @returns Multiple.
     */
    get multiple() {
        return this.getAttribute('multiple') !== null;
    }
    /**
     * Sets multiple.
     *
     * @param multiple Multiple.
     */
    set multiple(multiple) {
        if (!multiple) {
            this.removeAttribute('multiple');
        }
        else {
            this.setAttribute('multiple', '');
        }
    }
    /**
     * Returns autofocus.
     *
     * @returns Autofocus.
     */
    get autofocus() {
        return this.getAttribute('autofocus') !== null;
    }
    /**
     * Sets autofocus.
     *
     * @param autofocus Autofocus.
     */
    set autofocus(autofocus) {
        if (!autofocus) {
            this.removeAttribute('autofocus');
        }
        else {
            this.setAttribute('autofocus', '');
        }
    }
    /**
     * Returns required.
     *
     * @returns Required.
     */
    get required() {
        return this.getAttribute('required') !== null;
    }
    /**
     * Sets required.
     *
     * @param required Required.
     */
    set required(required) {
        if (!required) {
            this.removeAttribute('required');
        }
        else {
            this.setAttribute('required', '');
        }
    }
    /**
     * Returns type.
     *
     * @returns type.
     */
    get type() {
        return this.hasAttributeNS(null, 'multiple') ? 'select-multiple' : 'select-one';
    }
    /**
     * Returns value.
     *
     * @returns Value.
     */
    get value() {
        const options = QuerySelector.querySelectorAll(this, 'option')[PropertySymbol.items];
        for (let i = 0, max = options.length; i < max; i++) {
            const option = options[i];
            if (option[PropertySymbol.selectedness]) {
                return option.value;
            }
        }
        return '';
    }
    /**
     * Sets value.
     *
     * @param value Value.
     */
    set value(value) {
        const options = QuerySelector.querySelectorAll(this, 'option')[PropertySymbol.items];
        this[PropertySymbol.selectedIndex] = -1;
        for (let i = 0, max = options.length; i < max; i++) {
            const option = options[i];
            if (option.value === value) {
                option[PropertySymbol.selectedness] = true;
                option[PropertySymbol.dirtyness] = true;
                this[PropertySymbol.selectedIndex] = i;
            }
            else {
                option[PropertySymbol.selectedness] = false;
            }
        }
    }
    /**
     * Returns value.
     *
     * @returns Value.
     */
    get selectedIndex() {
        return this[PropertySymbol.selectedIndex];
    }
    /**
     * Sets value.
     *
     * @param selectedIndex Selected index.
     */
    set selectedIndex(selectedIndex) {
        selectedIndex = Number(selectedIndex);
        if (isNaN(selectedIndex)) {
            return;
        }
        const options = QuerySelector.querySelectorAll(this, 'option')[PropertySymbol.items];
        this[PropertySymbol.selectedIndex] = -1;
        if (typeof selectedIndex === 'number' && !isNaN(selectedIndex)) {
            for (let i = 0, max = options.length; i < max; i++) {
                options[i][PropertySymbol.selectedness] = false;
            }
            const selectedOption = options[selectedIndex];
            if (selectedOption) {
                selectedOption[PropertySymbol.selectedness] = true;
                selectedOption[PropertySymbol.dirtyness] = true;
                this[PropertySymbol.selectedIndex] = selectedIndex;
            }
        }
    }
    /**
     * Returns selected options.
     *
     * @returns HTMLCollection.
     */
    get selectedOptions() {
        if (!this[PropertySymbol.selectedOptions]) {
            this[PropertySymbol.selectedOptions] = new HTMLCollection(PropertySymbol.illegalConstructor, () => {
                const options = QuerySelector.querySelectorAll(this, 'option')[PropertySymbol.items];
                // If we hit the cache, we should recieve the same Array instance as before, which will then contain the selected options.
                if (options[PropertySymbol.selectedOptions]) {
                    return options[PropertySymbol.selectedOptions];
                }
                const selectedOptions = [];
                for (let i = 0, max = options.length; i < max; i++) {
                    const option = options[i];
                    if (option[PropertySymbol.selectedness]) {
                        selectedOptions.push(option);
                    }
                }
                options[PropertySymbol.selectedOptions] = selectedOptions;
                return selectedOptions;
            });
        }
        return this[PropertySymbol.selectedOptions];
    }
    /**
     * Returns the associated label elements.
     *
     * @returns Label elements.
     */
    get labels() {
        return HTMLLabelElementUtility.getAssociatedLabelElements(this);
    }
    /**
     * Returns the parent form element.
     *
     * @returns Form.
     */
    get form() {
        if (this[PropertySymbol.formNode]) {
            return this[PropertySymbol.formNode];
        }
        const id = this.getAttribute('form');
        if (!id || !this[PropertySymbol.isConnected]) {
            return null;
        }
        return this[PropertySymbol.ownerDocument].getElementById(id);
    }
    /**
     * Returns "true" if it will validate.
     *
     * @returns "true" if it will validate.
     */
    get willValidate() {
        return (this.type !== 'hidden' && this.type !== 'reset' && this.type !== 'button' && !this.disabled);
    }
    /**
     * @override
     */
    get tabIndex() {
        const tabIndex = this.getAttribute('tabindex');
        if (tabIndex !== null) {
            const parsed = Number(tabIndex);
            return isNaN(parsed) ? 0 : parsed;
        }
        return 0;
    }
    /**
     * @override
     */
    set tabIndex(tabIndex) {
        super.tabIndex = tabIndex;
    }
    /**
     * Returns item from options collection by index.
     *
     * @param index Index.
     */
    item(index) {
        return this.options.item(index);
    }
    /**
     * Adds new option to options collection.
     *
     * @param element HTMLOptionElement to add.
     * @param before HTMLOptionElement or index number.
     */
    add(element, before) {
        const options = QuerySelector.querySelectorAll(this, 'option')[PropertySymbol.items];
        if (!before && before !== 0) {
            const childNodes = this[PropertySymbol.nodeArray];
            while (childNodes.length) {
                this[PropertySymbol.removeChild](childNodes[0]);
            }
            for (const option of options) {
                this[PropertySymbol.appendChild](option);
            }
            this[PropertySymbol.appendChild](element);
            return;
        }
        const window = this[PropertySymbol.window];
        if (typeof before !== 'number') {
            if (!(before instanceof HTMLOptionElement)) {
                throw new window.DOMException("Failed to execute 'add' on 'HTMLFormElement': The node before which the new node is to be inserted before is not an 'HTMLOptionElement'.");
            }
            before = options.indexOf(before);
        }
        const optionsElement = options[before];
        if (!optionsElement) {
            throw new window.DOMException("Failed to execute 'add' on 'HTMLFormElement': The node before which the new node is to be inserted before is not a child of this node.");
        }
        const childNodes = this[PropertySymbol.nodeArray];
        while (childNodes.length) {
            this[PropertySymbol.removeChild](childNodes[0]);
        }
        for (let i = 0, max = options.length; i < max; i++) {
            if (i === before) {
                this[PropertySymbol.appendChild](element);
            }
            this[PropertySymbol.appendChild](options[i]);
        }
    }
    /**
     * Removes indexed element from collection or the select element.
     *
     * @param [index] Index.
     */
    remove(index) {
        if (typeof index === 'number') {
            const options = QuerySelector.querySelectorAll(this, 'option')[PropertySymbol.items];
            if (!options[index]) {
                return;
            }
            const childNodes = this[PropertySymbol.nodeArray];
            while (childNodes.length) {
                this[PropertySymbol.removeChild](childNodes[0]);
            }
            for (let i = 0, max = options.length; i < max; i++) {
                if (i !== index) {
                    this[PropertySymbol.appendChild](options[i]);
                }
            }
        }
        else {
            super.remove();
        }
    }
    /**
     * Sets validation message.
     *
     * @param message Message.
     */
    setCustomValidity(message) {
        this[PropertySymbol.validationMessage] = String(message);
    }
    /**
     * Checks validity.
     *
     * @returns "true" if the field is valid.
     */
    checkValidity() {
        const valid = this.disabled || this[PropertySymbol.validity].valid;
        if (!valid) {
            this.dispatchEvent(new Event('invalid', { bubbles: true, cancelable: true }));
        }
        return valid;
    }
    /**
     * Reports validity.
     *
     * @returns "true" if the field is valid.
     */
    reportValidity() {
        return this.checkValidity();
    }
    /**
     * Updates option item.
     *
     * Based on:
     * https://github.com/jsdom/jsdom/blob/master/lib/jsdom/living/nodes/HTMLSelectElement-impl.js
     *
     * @see https://html.spec.whatwg.org/multipage/form-elements.html#selectedness-setting-algorithm
     * @param [selectedOption] Selected option.
     */
    [PropertySymbol.updateSelectedness](selectedOption) {
        const isMultiple = this.hasAttribute('multiple');
        const options = QuerySelector.querySelectorAll(this, 'option')[PropertySymbol.items];
        const selected = [];
        if (selectedOption) {
            this[PropertySymbol.selectedIndex] = -1;
        }
        if (!isMultiple) {
            for (let i = 0, max = options.length; i < max; i++) {
                const option = options[i];
                if (selectedOption) {
                    option[PropertySymbol.selectedness] = option === selectedOption;
                    if (option === selectedOption) {
                        this[PropertySymbol.selectedIndex] = i;
                    }
                }
                if (option[PropertySymbol.selectedness]) {
                    selected.push(option);
                }
            }
        }
        const size = this.#getDisplaySize();
        if (size === 1 && !selected.length) {
            this[PropertySymbol.selectedIndex] = -1;
            for (let i = 0, max = options.length; i < max; i++) {
                const option = options[i];
                const parentNode = option[PropertySymbol.parentNode];
                let disabled = option.hasAttributeNS(null, 'disabled');
                if (parentNode &&
                    parentNode[PropertySymbol.nodeType] === NodeTypeEnum.elementNode &&
                    parentNode[PropertySymbol.tagName] === 'OPTGROUP' &&
                    parentNode.hasAttributeNS(null, 'disabled')) {
                    disabled = true;
                }
                if (!disabled) {
                    option[PropertySymbol.selectedness] = true;
                    this[PropertySymbol.selectedIndex] = i;
                    break;
                }
            }
        }
        else if (selected.length >= 2) {
            this[PropertySymbol.selectedIndex] = -1;
            for (let i = 0, max = options.length; i < max; i++) {
                options[i][PropertySymbol.selectedness] = i === selected.length - 1;
                if (i === selected.length - 1) {
                    this[PropertySymbol.selectedIndex] = i;
                }
            }
        }
    }
    /**
     * Returns display size.
     *
     * @returns Display size.
     */
    #getDisplaySize() {
        if (this.hasAttributeNS(null, 'size')) {
            const size = parseInt(this.getAttribute('size'));
            if (!isNaN(size) && size >= 0) {
                return size;
            }
        }
        return this.hasAttributeNS(null, 'multiple') ? 4 : 1;
    }
}
//# sourceMappingURL=HTMLSelectElement.js.map