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
 */
export default class Searchbar extends Component {
    constructor($element, $) {
        super($element, $);
        this.searchState = '.';
        this.autoState = true;

        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`;
        }

        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 = 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 = {};
        }

        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) {
        this.params.splice(index, 1);
        this.doSearch(this.fromBar(), 'bar');
    }

    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});
        });
    }

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

    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 = 'card.summary';
                }

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

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

                if (filter)
                    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')
            focus(this.element.querySelector('vn-textfield input'));
    }

    // Default search handlers

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

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

        if (filter === null) {
            this.model.clear();
            return;
        }

        let where = null;
        let params = null;

        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});
        }

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

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

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