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
 * @property {Function} onSearch Function to call when search is submited
 * @property {CrudModel} model The model used for searching
 * @property {Function} exprBuilder If defined, is used to build each non-null param expresion
 * @property {String} baseState The base state for searchs
 */
export default class Searchbar extends Component {
    constructor($element, $) {
        super($element, $);
        this.searchState = '.';
        this.placeholder = 'Search';
        this.autoState = true;
        this.separateIndex = true;
        this.entityState = 'card.summary';
        this.isIndex = false;

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

    $postLink() {
        if (this.autoState) {
            if (!this.baseState) {
                let stateParts = this.$state.current.name.split('.');
                this.baseState = stateParts[0];
                this.searchState = `${this.baseState}.index`;
            } else
                this.searchState = this.baseState;

            let description = this.$state.get(this.baseState).description;
            this.placeholder = this.$t('Search for', {
                module: this.$t(description).toLowerCase()
            });
        }

        this.fetchStateFilter(this.autoLoad);
    }

    $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(transition) {
        let ignoreHandler =
            !this.element.parentNode
            || transition == this.transition;

        if (ignoreHandler) return;
        this.fetchStateFilter();
    }

    fetchStateFilter(autoLoad) {
        let filter = this.filter ? this.filter : null;

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

            if (!filter && autoLoad)
                filter = {};
        }

        let stateParts = this.$state.current.name.split('.');
        this.isIndex = stateParts[1] == 'index';

        this.doSearch(filter, 'state');
    }

    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, 'panel');
    }

    onSubmit() {
        this.doSearch(this.fromBar(), 'bar');
    }

    removeParam(index) {
        const field = this.params[index].key;
        this.filterSanitizer(field);

        this.params.splice(index, 1);
        this.toRemove = field;
        this.doSearch(this.fromBar(), 'removeBar');
    }

    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' || key == 'tableQ' || key == 'tableOrder') 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});
        });
    }

    doSearch(filter, source) {
        if (filter === this.filter && !this.isIndex) return;
        let promise = this.onSearch({$params: filter}, source);
        promise = promise || this.$q.resolve();
        promise.then(data => this.onFilter(filter, source, data));
        this.toBar(filter);
    }

    onFilter(filter, source, data) {
        let state;
        let params;
        let opts;

        if (filter) {
            let isOneResult = this.autoState
                && source != 'state'
                && !angular.equals(filter, {})
                && data
                && data.length == 1;

            if (isOneResult) {
                let baseDepth = this.baseState.split('.').length;
                let stateParts = this.$state.current.name
                    .split('.')
                    .slice(baseDepth);

                let subState = stateParts[0];

                switch (subState) {
                case 'card':
                    subState += `.${stateParts[1]}`;
                    if (stateParts.length >= 3)
                        subState += '.index';
                    break;
                default:
                    subState = this.entityState;
                }

                if (this.stateParams)
                    params = this.stateParams({$row: data[0]});

                state = `${this.baseState}.${subState}`;
                filter = null;
            } else {
                state = this.searchState;

                if (filter) {
                    if (this.tableQ)
                        filter.tableQ = this.tableQ;
                    params = {q: JSON.stringify(filter)};
                }
                if (this.$state.is(state))
                    opts = {location: 'replace'};
            }
        }

        this.filter = filter;

        if (!filter && this.model)
            this.model.clear();
        if (source != 'state')
            this.transition = this.$state.go(state, params, opts).transition;
        if (source != 'bar' && (source != 'state' || this.$state.is(this.baseState)))
            focus(this.element.querySelector('vn-textfield input'));
    }

    // Default search handlers

    stateParams(params) {
        return {id: params.$row.id};
    }

    onSearch(args, source) {
        if (!this.model) return;
        let filter = args.$params;

        if (filter === null) {
            this.model.clear();
            return;
        }
        if (Object.keys(filter).length === 0) {
            this.filterSanitizer('search');
            if (this.model.userParams)
                delete this.model.userParams['search'];
        }

        let where = null;
        let params = {};

        if (this.exprBuilder) {
            where = buildFilter(filter,
                (param, value) => this.exprBuilder({param, value}));
        } else {
            params = Object.assign({}, filter);

            if (this.fetchParams)
                params = this.fetchParams({$params: params});
        }

        this.tableQ = null;

        const hasParams = this.$params.q && JSON.parse(this.$params.q).tableQ;
        if (hasParams) {
            const stateFilter = JSON.parse(this.$params.q);
            for (let param in stateFilter) {
                if (param != 'tableQ' && param != 'orderQ')
                    this.filterSanitizer(param);
            }

            for (let param in this.suggestedFilter) {
                this.filterSanitizer(param);
                delete stateFilter[param];
            }

            this.tableQ = stateFilter.tableQ;
            for (let param in stateFilter.tableQ)
                params[param] = stateFilter.tableQ[param];

            if (source == 'removeBar') {
                delete params[this.toRemove];
                delete this.model.userParams[this.toRemove];
                delete stateFilter[this.toRemove];
            }

            const newParams = Object.assign(stateFilter, params);
            return this.model.applyParams(newParams)
                .then(() => this.model.data);
        }

        return this.model.applyFilter(where ? {where} : null, params)
            .then(() => this.model.data);
    }

    filterSanitizer(field) {
        if (!field) return;
        const userFilter = this.model.userFilter;
        const userParams = this.model.userParams;
        const where = userFilter && userFilter.where;

        if (this.model.userParams)
            delete this.model.userParams[field];

        if (this.exprBuilder) {
            const param = this.exprBuilder({
                param: field,
                value: null
            });
            if (param) [field] = Object.keys(param);
        }

        if (!where) return;

        const whereKeys = Object.keys(where);
        for (let key of whereKeys) {
            removeProp(where, field, key);

            if (Object.keys(where).length == 0)
                delete userFilter.where;
        }

        function removeProp(obj, targetProp, prop) {
            if (prop == targetProp)
                delete obj[prop];

            if (prop === 'and' || prop === 'or' && obj[prop]) {
                const arrayCopy = obj[prop].slice();
                for (let param of arrayCopy) {
                    const [key] = Object.keys(param);
                    const index = obj[prop].findIndex(param => {
                        return Object.keys(param)[0] == key;
                    });
                    if (key == targetProp)
                        obj[prop].splice(index, 1);

                    if (param[key] instanceof Array)
                        removeProp(param, field, key);

                    if (Object.keys(param).length == 0)
                        obj[prop].splice(index, 1);
                }

                if (obj[prop].length == 0)
                    delete obj[prop];
            }
        }

        return {userFilter, userParams};
    }
}

ngModule.vnComponent('vnSearchbar', {
    controller: Searchbar,
    template: require('./searchbar.html'),
    bindings: {
        filter: '<?',
        suggestedFilter: '<?',
        panel: '@',
        info: '@?',
        onSearch: '&?',
        autoState: '<?',
        baseState: '@?',
        entityState: '@?',
        separateIndex: '<?',
        stateParams: '&?',
        model: '<?',
        exprBuilder: '&?',
        fetchParams: '&?',
        placeholder: '@?'
    }
});

class AutoSearch {
    constructor(vnSlotService) {
        this.vnSlotService = vnSlotService;
    }

    $postLink() {
        let searchbar = this.vnSlotService.getContent('topbar');
        if (searchbar && searchbar.$ctrl instanceof Searchbar)
            this.model = searchbar.$ctrl.model;
    }
}
AutoSearch.$inject = ['vnSlotService'];

ngModule.vnComponent('vnAutoSearch', {
    controller: AutoSearch,
    bindings: {
        model: '=?'
    }
});

/**
 * 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;
}