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

/**
 * 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 {Function} onSearch Function to call when search is submited
 * @property {SearchPanel} panel The panel used for advanced searches
 * @property {CrudModel} model The model used for searching
 * @property {Function} exprBuilder If defined, is used to build each non-null param expresion
 */
export default class Controller extends Component {
    constructor($element, $scope, $compile, $state, $transitions) {
        super($element, $scope);
        this.$compile = $compile;
        this.$state = $state;

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

        this._filter = null;
        this.searchString = '';
        this.autoLoad = false;
    }

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

    set filter(value) {
        this._filter = value;
        this.$state.go('.', {q: JSON.stringify(value)});
    }

    get filter() {
        return this._filter;
    }

    onStateChange() {
        this._filter = null;

        if (this.$state.params.q) {
            try {
                this._filter = JSON.parse(this.$state.params.q);
            } catch (e) {}
        }

        this.doSearch();
    }

    openPanel(event) {
        if (event.defaultPrevented) return;
        event.preventDefault();

        this.$panel = this.$compile(`<${this.panel}/>`)(this.$.$new());
        let panel = this.$panel.isolateScope().$ctrl;
        panel.filter = this._filter;
        panel.onSubmit = filter => this.onPanelSubmit(filter);

        this.$.popover.parent = this.element;
        this.$.popover.child = this.$panel[0];
        this.$.popover.show();
    }

    onPopoverClose() {
        this.$panel.scope().$destroy();
        this.$panel.remove();
        this.$panel = null;
    }

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

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

    onSubmit() {
        this.filter = this.getObjectFromString(this.searchString);
    }

    doSearch() {
        let filter = this._filter;

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

        this.searchString = this.getStringFromObject(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};
    }

    /**
     * Finds pattern key:value or key:(extra value) and passes it to object.
     *
     * @param {String} searchString The search string
     * @return {Object} The parsed object
     */
    getObjectFromString(searchString) {
        let result = {};
        if (searchString) {
            let regex = /((([\w_]+):([\w_]+))|([\w_]+):\(([\w_ ]+)\))/gi;
            let findPattern = searchString.match(regex);
            let remnantString = searchString.replace(regex, '').trim();
            if (findPattern) {
                for (let i = 0; i < findPattern.length; i++) {
                    let aux = findPattern[i].split(':');
                    let property = aux[0];
                    let value = aux[1].replace(/\(|\)/g, '');
                    result[property] = value.trim();
                }
            }
            if (remnantString)
                result.search = remnantString;
        }
        return result;
    }

    /**
     * Passes an object to pattern key:value or key:(extra value).
     *
     * @param {Object} searchObject The search object
     * @return {String} The passed string
     */
    getStringFromObject(searchObject) {
        let search = [];

        if (searchObject) {
            let keys = Object.keys(searchObject);
            keys.forEach(key => {
                if (key == 'search') return;

                let value = searchObject[key];
                let valueString;

                if (typeof value === 'string' && value.indexOf(' ') !== -1)
                    valueString = `(${value})`;
                else if (value instanceof Date)
                    valueString = value.toJSON();
                else {
                    switch (typeof value) {
                    case 'number':
                    case 'string':
                    case 'boolean':
                        valueString = `${value}`;
                    }
                }

                if (valueString)
                    search.push(`${key}:${valueString}`);
            });

            if (searchObject.search)
                search.unshift(searchObject.search);
        }

        return search.length ? search.join(' ') : '';
    }

    $onDestroy() {
        this.deregisterCallback();
    }
}
Controller.$inject = ['$element', '$scope', '$compile', '$state', '$transitions'];

ngModule.component('vnSearchbar', {
    template: require('./searchbar.html'),
    bindings: {
        filter: '<?',
        onSearch: '&?',
        panel: '@',
        model: '<?',
        exprBuilder: '&?',
        paramBuilder: '&?',
        autoLoad: '<?'
    },
    controller: Controller
});

/**
 * 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.substr(0, 2) == '$$' || compact(obj[key]) === undefined)
                delete obj[key];
        }
        if (Object.keys(obj).length == 0)
            return undefined;
    }

    return obj;
}