import './style.scss';
import ngModule from '../../module';
import Popover from '../popover';
import ArrayModel from '../array-model/array-model';
import CrudModel from '../crud-model/crud-model';
import {mergeWhere} from 'vn-loopback/util/filter';
import focus from '../../lib/focus';

/**
 * @event select Thrown when model item is selected
 * @event change Thrown when model data is ready
 */
export default class DropDown extends Popover {
    constructor($element, $, $transclude) {
        super($element, $, $transclude);
        this.valueField = 'id';
        this.showField = 'name';
        this._search = undefined;
        this._activeOption = -1;
        this.showLoadMore = true;
        this.showFilter = true;
        this.searchDelay = 300;
    }

    get search() {
        return this._search;
    }

    set search(value) {
        function nullify(value) {
            return value == '' || value == undefined ? null : value;
        }

        let oldValue = nullify(this._search);
        this._search = value;

        if (!this.shown) return;

        value = nullify(value);
        if (value === oldValue && this.modelData != null) return;

        if (value != null)
            this._activeOption = 0;

        this.$timeout.cancel(this.searchTimeout);

        if (this.model) {
            this.model.clear();

            if (this.model instanceof CrudModel) {
                this.searchTimeout = this.$timeout(() => {
                    this.refreshModel();
                    this.searchTimeout = null;
                }, value != null ? this.searchDelay : 0);
            } else
                this.refreshModel();
        }

        this.buildList();
    }

    get statusText() {
        let model = this.model;
        let data = this.modelData;

        if (!model)
            return 'No data';
        if (model.isLoading || this.searchTimeout)
            return 'Loading...';
        if (data == null)
            return 'No data';
        if (model.moreRows)
            return 'Load More';
        if (data.length === 0)
            return 'No results found';

        return null;
    }

    get activeOption() {
        return this._activeOption;
    }

    /**
     * Shows the drop-down. If a parent is specified it is shown in a visible
     * relative position to it.
     *
     * @param {HTMLElement} parent The parent element to show drop down relative to
     * @param {String} search The initial search term or %null
     */
    show(parent, search) {
        if (this.shown) return;
        super.show(parent);

        this._activeOption = -1;

        this.list = this.popup.querySelector('.list');
        this.ul = this.popup.querySelector('ul');

        this.docKeyDownHandler = e => this.onDocKeyDown(e);
        this.document.addEventListener('keydown', this.docKeyDownHandler);

        this.listScrollHandler = e => this.onScroll(e);
        this.list.addEventListener('scroll', this.listScrollHandler);
        this.list.scrollTop = 0;

        this.search = search;
        this.buildList();

        focus(this.popup.querySelector('input'));
    }

    hide() {
        if (!this.shown) return;
        super.hide();

        this.document.removeEventListener('keydown', this.docKeyDownHandler);
        this.docKeyDownHandler = null;

        this.list.removeEventListener('scroll', this.listScrollHandler);
        this.listScrollHandler = null;
    }

    onClose() {
        this.destroyList();
        this.list = null;
        this.ul = null;

        super.onClose();
    }

    /**
     * Activates an option and scrolls the drop-down to that option.
     *
     * @param {Number} option The option index
     */
    moveToOption(option) {
        this.activateOption(option);

        let list = this.list;
        let li = this.activeLi;
        if (!li) return;

        let liRect = li.getBoundingClientRect();
        let listRect = list.getBoundingClientRect();

        if (liRect.bottom > listRect.bottom)
            list.scrollTop += liRect.bottom - listRect.bottom;
        else if (liRect.top < listRect.top)
            list.scrollTop -= listRect.top - liRect.top;
    }

    /**
     * Activates an option.
     *
     * @param {Number} option The option index
     */
    activateOption(option) {
        this._activeOption = option;

        if (this.activeLi)
            this.activeLi.className = '';

        let data = this.modelData;

        if (option >= 0 && data && option < data.length) {
            this.activeLi = this.ul.children[option];
            this.activeLi.className = 'active';
        }
    }

    /**
     * Selects an option.
     *
     * @param {Number} option The option index
     */
    selectOption(option) {
        let data = this.modelData;
        let item = option != -1 && data ? data[option] : null;

        if (item) {
            let value = item[this.valueField];

            if (this.multiple) {
                if (!Array.isArray(this.selection)) {
                    this.selection = [];
                    this.field = [];
                }

                this.selection.push(item);
                this.field.push(value);
            } else {
                this.selection = item;
                this.field = value;
            }

            this.emit('select', {item});
        }

        if (!this.multiple)
            this.hide();
    }

    onClearClick() {
        this.search = null;
    }

    onScroll() {
        let list = this.list;
        let shouldLoad =
            list.scrollTop + list.clientHeight >= (list.scrollHeight - 40)
            && !this.model.isLoading;

        if (shouldLoad)
            this.model.loadMore();
    }

    onLoadMoreClick(event) {
        if (event.defaultPrevented) return;
        this.model.loadMore();
    }

    onContainerClick(event) {
        if (event.defaultPrevented) return;
        let index = getPosition(this.ul, event);
        if (index != -1) this.selectOption(index);
    }

    onDocKeyDown(event) {
        if (event.defaultPrevented) return;

        let data = this.modelData;
        let option = this.activeOption;
        let nOpts = data ? data.length - 1 : 0;

        switch (event.key) {
        case 'Tab':
            this.selectOption(option);
            return;
        case 'Enter':
            this.selectOption(option);
            break;
        case 'ArrowUp':
            this.moveToOption(option <= 0 ? nOpts : option - 1);
            break;
        case 'ArrowDown':
            this.moveToOption(option >= nOpts ? 0 : option + 1);
            break;
        case 'End':
            this.moveToOption(nOpts);
            break;
        case 'Home':
            this.moveToOption(0);
            break;
        default:
            return;
        }

        event.preventDefault();
        this.$.$applyAsync();
    }

    buildList() {
        if (!this.shown) return;
        this.destroyList();

        let hasTemplate = this.$transclude && this.$transclude.isSlotFilled('tplItem');
        let fragment = this.document.createDocumentFragment();
        let data = this.modelData;

        if (data) {
            for (let i = 0; i < data.length; i++) {
                let option = data[i];
                option.orgShowField = option[this.showField];

                if (this.translateFields) {
                    option = Object.assign({}, option);
                    for (let field of this.translateFields)
                        option[field] = this.$t(option[field]);
                }

                let li = this.document.createElement('li');

                li.setAttribute('name', option.orgShowField);
                fragment.appendChild(li);

                if (this.multiple) {
                    let check = this.document.createElement('input');
                    check.type = 'checkbox';
                    li.appendChild(check);

                    if (this.field && this.field.indexOf(option[this.valueField]) != -1)
                        check.checked = true;
                }

                if (hasTemplate) {
                    this.$transclude((clone, scope) => {
                        Object.assign(scope, option);
                        li.appendChild(clone[0]);
                        this.scopes[i] = scope;
                    }, null, 'tplItem');
                } else {
                    let text = this.document.createTextNode(option[this.showField]);
                    li.appendChild(text);
                }
            }
        }

        this.ul.appendChild(fragment);
        this.activateOption(this._activeOption);
        this.$.$applyAsync(() => this.relocate());
    }

    destroyList() {
        if (this.ul)
            this.ul.innerHTML = '';

        if (this.scopes) {
            for (let scope of this.scopes)
                scope.$destroy();
        }

        this.scopes = [];
    }

    getFields() {
        let fields = [];
        fields.push(this.valueField);
        fields.push(this.showField);

        if (this.fields) {
            for (let field of this.fields)
                fields.push(field);
        }

        return fields;
    }

    // Model related code

    onDataChange() {
        if (this.model.orgData)
            this.emit('dataReady');
        this.buildList();
    }

    get modelData() {
        return this._model ? this._model.data : null;
    }

    get model() {
        return this._model;
    }

    set model(value) {
        this.linkEvents({_model: value}, {dataChange: this.onDataChange});
        this.onDataChange();
        this.search = this.search;
    }

    get url() {
        return this._url;
    }

    set url(value) {
        this._url = value;
        if (value) {
            let model = new CrudModel(this.$q, this.$http);
            model.url = value;
            this.initModel(model);
        }
    }

    get data() {
        return this._data;
    }

    set data(value) {
        this._data = value;
        if (value) {
            let model = new ArrayModel(this.$q, this.$filter);
            model.orgData = value;
            this.initModel(model);
        }
    }

    initModel(model) {
        model.autoLoad = false;
        model.$onInit();
        this.model = model;
    }

    refreshModel() {
        let model = this.model;

        let order;
        if (this.order)
            order = this.order;
        else if (this.showField)
            order = `${this.showField} ASC`;

        let filter = {
            order,
            limit: this.limit || 30
        };

        if (model instanceof CrudModel) {
            let searchExpr = this._search == null
                ? null
                : this.searchFunction({$search: this._search});

            if (this.whereFunction)
                this.where = this.whereFunction();

            Object.assign(filter, {
                fields: this.getFields(),
                include: this.include,
                where: mergeWhere(this.where, searchExpr)
            });
        } else if (model instanceof ArrayModel) {
            if (this._search != null)
                filter.where = this.searchFunction({$search: this._search});
        }

        return this.model.applyFilter(filter);
    }

    searchFunction(scope) {
        if (this.model instanceof CrudModel)
            return {[this.showField]: {like: `%${scope.$search}%`}};
        if (this.model instanceof ArrayModel)
            return {[this.showField]: scope.$search};
    }
}

/**
 * Gets the position of an event element relative to a parent.
 *
 * @param {HTMLElement} parent The parent element
 * @param {Event} event The event object
 * @return {Number} The element index
 */
function getPosition(parent, event) {
    let target = event.target;
    let children = parent.children;

    if (target === parent)
        return -1;

    while (target.parentNode !== parent)
        target = target.parentNode;

    for (let i = 0; i < children.length; i++) {
        if (children[i] === target)
            return i;
    }

    return -1;
}

ngModule.vnComponent('vnDropDown', {
    slotTemplate: require('./index.html'),
    controller: DropDown,
    transclude: {
        tplItem: '?tplItem'
    },
    bindings: {
        field: '=?',
        selection: '=?',
        search: '<?',
        showFilter: '<?',
        parent: '<?',
        multiple: '<?',
        onSelect: '&?',
        translateFields: '<?',
        data: '<?',
        url: '@?',
        fields: '<?',
        include: '<?',
        where: '<?',
        order: '@?',
        limit: '<?',
        searchFunction: '&?',
        searchDelay: '<?'
    }
});