import ngModule from '../../module';
import Component from '../../lib/component';
import {buildFilter} from 'vn-loopback/util/filter';
import focus from '../../lib/focus';
import './style.scss';

/**
 * An input specialized to perform searches, it allows to use a panel
 * for advanced searches when the panel property is defined.
 * When model and exprBuilder properties are used, the model is updated
 * automatically with an and-filter exprexion in which each operand is built
 * by calling the exprBuilder function for each non-null parameter.
 *
 * @property {Object} filter  A key-value object with filter parameters
 * @property {SearchPanel} panel The panel used for advanced searches
 */
export default class Controller extends Component {
    constructor($element, $) {
        super($element, $);
        this.searchState = '.';

        let criteria = {};
        this.deregisterCallback = this.$transitions.onSuccess(
            criteria, () => this.onStateChange());
    }

    $postLink() {
        this.onStateChange();
    }

    $onDestroy() {
        this.deregisterCallback();
    }

    get filter() {
        return this._filter;
    }

    set filter(value) {
        this._filter = value;
        this.toBar(value);
    }

    get shownFilter() {
        return this.filter != null
            ? this.filter
            : this.suggestedFilter;
    }

    get searchString() {
        return this._searchString;
    }

    set searchString(value) {
        this._searchString = value;
        if (value == null) this.params = [];
    }

    onStateChange() {
        let filter = null;

        if (this.$state.is(this.searchState)) {
            if (this.$params.q) {
                try {
                    filter = JSON.parse(this.$params.q);
                } catch (e) {
                    console.error(e);
                }
            }

            focus(this.element.querySelector('vn-textfield input'));
        }

        this.filter = filter;
    }

    openPanel(event) {
        if (event.defaultPrevented) return;
        this.$.popover.show(this.element);

        this.$panelScope = this.$.$new();
        this.panelEl = this.$compile(`<${this.panel}/>`)(this.$panelScope)[0];
        let panel = this.panelEl.$ctrl;
        if (this.shownFilter)
            panel.filter = JSON.parse(JSON.stringify(this.shownFilter));
        panel.onSubmit = filter => this.onPanelSubmit(filter.$filter);

        this.$.popover.content.appendChild(this.panelEl);
    }

    onPopoverClose() {
        this.$panelScope.$destroy();
        this.$panelScope = null;

        this.panelEl.remove();
        this.panelEl = null;
    }

    onPanelSubmit(filter) {
        this.$.popover.hide();
        filter = compact(filter);
        filter = filter != null ? filter : {};
        this.doSearch(filter);
    }

    onSubmit() {
        this.doSearch(this.fromBar());
    }

    removeParam(index) {
        this.params.splice(index, 1);
        this.doSearch(this.fromBar());
    }

    doSearch(filter) {
        this.filter = filter;

        let opts = this.$state.is(this.searchState)
            ? {location: 'replace'} : null;
        this.$state.go(this.searchState,
            {q: JSON.stringify(filter)}, opts);
    }

    fromBar() {
        let filter = {};

        if (this.searchString)
            filter.search = this.searchString;

        if (this.params) {
            for (let param of this.params)
                filter[param.key] = param.value;
        }

        return filter;
    }

    toBar(filter) {
        this.params = [];
        this.searchString = filter && filter.search;
        if (!filter) return;

        let keys = Object.keys(filter);
        keys.forEach(key => {
            if (key == 'search') return;
            let value = filter[key];
            let chip;

            if (typeof value == 'string'
            && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value))
                value = new Date(value);

            switch (typeof value) {
            case 'boolean':
                chip = `${value ? '' : 'not '}${key}`;
                break;
            case 'number':
            case 'string':
                chip = `${key}: ${value}`;
                break;
            default:
                if (value instanceof Date) {
                    let format = 'yyyy-MM-dd';
                    if (value.getHours() || value.getMinutes())
                        format += ' HH:mm';
                    chip = `${key}: ${this.$filter('date')(value, format)}`;
                } else
                    chip = key;
            }

            this.params.push({chip, key, value});
        });
    }
}

ngModule.vnComponent('vnSearchbar', {
    controller: Controller,
    template: require('./searchbar.html'),
    bindings: {
        searchState: '@?',
        filter: '<?',
        suggestedFilter: '<?',
        panel: '@',
        info: '@?'
    }
});

/**
 * @property {CrudModel} model The model used for searching
 * @property {Function} exprBuilder If defined, is used to build each non-null param expresion
 * @property {Function} onSearch Function to call when search is submited
 */
class AutoSearch {
    constructor($state, $transitions) {
        this.$state = $state;
        this.$transitions = $transitions;

        let criteria = {to: this.$state.current.name};
        this.deregisterCallback = this.$transitions.onSuccess(criteria,
            () => this.onStateChange());

        this.fetchFilter();
    }

    $postLink() {
        if (this.filter !== null)
            this.doSearch();
    }

    $onDestroy() {
        this.deregisterCallback();
    }

    fetchFilter() {
        if (this.$state.params.q) {
            try {
                this.filter = JSON.parse(this.$state.params.q);
            } catch (e) {
                console.error(e);
            }
        } else
            this.filter = null;
    }

    onStateChange() {
        this.fetchFilter();
        this.doSearch();
    }

    doSearch() {
        let filter = this.filter;
        if (filter == null && this.autoload)
            filter = {};

        if (this.onSearch)
            this.onSearch({$params: filter});

        if (this.model) {
            if (filter !== null) {
                let where = buildFilter(filter,
                    (param, value) => this.exprBuilder({param, value}));

                let userParams = {};
                let hasParams = false;

                if (this.paramBuilder) {
                    for (let param in filter) {
                        let value = filter[param];
                        if (value == null) continue;
                        let expr = this.paramBuilder({param, value});
                        if (expr) {
                            Object.assign(userParams, expr);
                            hasParams = true;
                        }
                    }
                }
                this.model.applyFilter(
                    where ? {where} : null,
                    hasParams ? userParams : null
                );
            } else
                this.model.clear();
        }
    }

    exprBuilder(param, value) {
        return {[param]: value};
    }
}
AutoSearch.$inject = ['$state', '$transitions'];

ngModule.vnComponent('vnAutoSearch', {
    controller: AutoSearch,
    bindings: {
        model: '<?',
        onSearch: '&?',
        exprBuilder: '&?',
        paramBuilder: '&?'
    }
});

/**
 * Removes null, undefined, empty objects and empty arrays from an object.
 * It also applies to nested objects/arrays.
 *
 * @param {*} obj The value to format
 * @return {*} The formatted value
 */
function compact(obj) {
    if (obj == null)
        return undefined;
    else if (Array.isArray(obj)) {
        for (let i = obj.length - 1; i >= 0; i--) {
            if (compact(obj[i]) === undefined)
                obj.splice(i, 1);
        }
        if (obj.length == 0)
            return undefined;
    } else if (typeof obj == 'object' && obj.constructor == Object) {
        let keys = Object.keys(obj);
        for (let key of keys) {
            if (key.charAt(0) == '$' || compact(obj[key]) === undefined)
                delete obj[key];
        }
        if (Object.keys(obj).length == 0)
            return undefined;
    }

    return obj;
}