import { compareTwoStrings } from 'string-similarity';
import { Util } from '../../util/Util';
import orderBy from 'lodash/orderBy';
import maxBy from 'lodash/maxBy';

let inject = ['StTypeaheadDefaults', '$http', '$scope', '$timeout', '$element', '$attrs']
export class StTypeaheadMultipleController {
    static get $inject(){
        return inject;
    }
    constructor(...injectables){
        Util.injectNg(injectables, inject, this);
    }
    bestMatch = {};
    lastSelected = [];
    allOptionsDisabled = false;
    optionDisabled($item){

        if (this.allOptionsDisabled) return true;
        else if (!!this.isOptionDisabled && this.options.length && this.isOptionDisabled({$item})) return true;
        else return false;
    }
    selectedDisabled($item){
        return !!this.isSelectedDisabled && this.modelValue.length ? this.isSelectedDisabled({$item}) : false;
    }
    get modelValue(){
        return this.ngModel.$viewValue;
    }
    get hasAnySelectable(){
        return !!this.rest || !!this.options.length;
    }
    get showNoDataFound(){
        return this.isOpen && this.translations.noDataFound && !this.options.length;
    }
    get showLoading(){
        return this.isOpen && this.loading && !this.options.length;
    }
    get isChoicesVisible(){
        return this.isOpen && this.hasAnySelectable || this.showLoading || this.showNoDataFound;
    }
    get visibleOptions(){
        return {
            height: `${(this.options.length ? Math.min(this.config.visibleOptions, this.options.length) * 28 + 8 : 28) }px`
        };
    }
    isMatch(item){
        return item && !Util.isObjectEmpty(this.matchSelected) && this.config.comparator(item, this.matchSelected);
    }
    isOptionSelected(item){
        return item && !Util.isObjectEmpty(this.optionSelected) && this.config.comparator(item, this.optionSelected);
    }
    open(){
        if (!this.isOpen) this.isOpen = true;
    }
    close(){
        if (this.isOpen) this.isOpen = false;
    }
    _setModel(type){
        type = `$set${type}`;
        this.ngModel[type]();
    }
    _setValidity(type, valid){
        switch (type) {
            case 'required': {
                if (angular.isDefined(this.ngRequired) || angular.isDefined(this.$attrs.required)){
                    valid = !!this.modelValue.length
                    this.ngModel.$setValidity(type, valid);
                }
                break;
            }
            case 'limit': this.ngModel.$setValidity(type, valid); break;
        }
    }
    checkForLimit(){
        if (this.limit) {
            let hasPassed = this.modelValue.length > this.limit.limit,
            isEqual = this.modelValue.length === this.limit.limit;
            if (this.limit.type === 'validator'){
                this._setValidity('limit', !hasPassed);
            } else {
                this.allOptionsDisabled = isEqual;
            }
        }
    }
    select($event, $item, $index){
        if ($event) $event.stopPropagation();
        if (this.ngDisabled || this.optionDisabled($item)) return;
        let callbackItem = Util.deleteIfExists($item, 'similarity');
        this.ngModel.$viewValue.push(callbackItem);
        this.lastSelected.push($item);
        this.options.splice($index, 1);
        if (this.config.closeOnSelect) this.close();
        this._setModel('Dirty');
        this._setValidity('required');
        this.$searchElement[0].focus();
        this.checkForLimit();
        if (this.config.clearSearch) this.search = '';
        if (this.onSelect) this.onSelect({$item: callbackItem});
    }
    remove($event, $item, $index){
        $event.stopPropagation();
        if (this.ngDisabled || this.selectedDisabled($item)) return;
        let callbackItem = Util.deleteIfExists($item, 'similarity');
        if (angular.isUndefined($index)) $index = this.modelValue.findIndex(o => this.config.comparator(o, $item));
        this.ngModel.$viewValue.splice($index, 1);
        this.options.unshift($item);
        this._setModel('Dirty');
        this._setValidity('required');
        this.checkForLimit();
        if (this.onRemove) this.onRemove({$item: callbackItem});
    }
    getter(item){
        if (this.config.label) return item[this.config.label];
        return item;
    }
    getOptionsApi(){
        this.loading = true;
        let restInfo = {
            url: this.rest,
            method: 'GET'
        }
        if (angular.isObject(this.rest)) restInfo = this.rest;
        if (this.restParams) restInfo.params = this.restParams;
        this.$http(restInfo).then(resp => {
            let data = resp.data ? resp.data : resp;
            if (Util.exists(this.api, 'rest.result', angular.isFunction)){
                this.api.rest.result({response: angular.copy(resp)});
            }
            data = data.filter(o => !this.modelValue.some(m => this.config.comparator(o, m)));
            if (!data.length) this.restBlocked = this.config.keepRefreshing;
            this.hadError = false;
            if (this.config.checkForSimilarity){
                let search = this.search.toLowerCase();
                this.options = data.filter((o, i) => {
                    let similarity = compareTwoStrings(o[this.config.label].toLowerCase(), search);
                    data[i].similarity = similarity;
                    return similarity > this.config.similarityPerc / 100;
                });
                this.options = orderBy(this.options, 'similarity', 'desc').slice(0, this.config.visibleOptions);
                this.findBestMatch();
            } else {
                this.options = data.slice(0, this.config.visibleOptions);
                if (this.config.similarity) this.findBestMatch();
            }
            
        }).finally(() => {
            this.loading = false;
        }).catch(error => {
            if (Util.exists(this.api, 'rest.error', angular.isFunction)){
                this.api.rest.error({response: angular.copy(error)});
            }
        });
    }
    getOptionsLocal(){
        if (angular.isArray(this.data)){
            if (this.config.checkForSimilarity){
                let search = this.search.toLowerCase();
                this.options = orderBy(angular.copy(this.data).map(o => {
                    o.similarity = compareTwoStrings(o[this.config.label].toLowerCase(), search);
                    return o;
                }).filter(o => o.similarity > this.config.similarityPerc / 100), 'similarity', 'desc').slice(0, this.config.visibleOptions);
                this.findBestMatch();
            } else {
                this.options = angular.copy(this.data).slice(0, this.config.visibleOptions);
                if (this.config.similarity) this.findBestMatch();
            }
        }
    }
    findBestMatch(){
        this.$timeout(() => {
            if (this.options.length === 1 && this.options[0][this.config.similarity] > this.config.autoSelectSimilarity / 100){
                this.select(null, this.options[0], 0);
            } else if (this.options.length !== 0) {
                this.optionSelected = maxBy(this.options, this.config.similarity);
            }
        }, this.config.delayAutoSelect)
    }
    selectMatch($event, $item, $index){
        $event.stopPropagation();
        this._setModel('Touched');
        if (!Util.throttle('stTypeaheadSelectMatch')) return;
        if (this.ngDisabled || this.selectedDisabled($item)) return;
        if (!Util.isObjectEmpty(this.matchSelected) && this.config.comparator(this.matchSelected, $item)) this.matchSelected = {};
        else this.matchSelected = $item;
    }
    keyup($event){
        if (!Util.throttle('stTypeaheadKeyup')) return;
        switch ($event.which){
            case 8: this.handleBackspace($event);
        }
    }
    handleBackspace($event){
        let modelLen = this.modelValue.length;
        if (!this.search && modelLen) {
            if (!Util.isObjectEmpty(this.matchSelected) && this.config.comparator(this.matchSelected, this.modelValue[modelLen-1])) {
                this.remove($event, this.matchSelected, modelLen-1);
            } else {
                this.matchSelected = this.modelValue[modelLen-1];
            }
        }
    }
    handleCtrlZ($event){
        if (!$event.ctrlKey && Util.isEmpty(this.lastSelected) || !this.modelValue.length) return;
        $event.preventDefault();
        let i = this.lastSelected.length-1;
        this.remove($event, this.lastSelected[i]);
        this.lastSelected.pop();
    }
    handleDown($event){
        if (Util.isObjectEmpty(this.optionSelected)) this.optionSelected = this.options[0];
        else {
            let index = this.options.findIndex(o => this.config.comparator(o, this.optionSelected));
            if (angular.isDefined(index) && index < this.options.length-1){
                this.optionSelected = this.options[index+1];
            } else {
                this.optionSelected = this.options[0];
            }
            
        }
    }
    handleUp($event){
        if (Util.isObjectEmpty(this.optionSelected)) return;
        let index = this.options.findIndex(o => this.config.comparator(o, this.optionSelected));
        if (angular.isDefined(index) && index > 0){
            this.optionSelected = this.options[index-1];
        } else if (index === 0) {
            this.optionSelected = this.options[this.options.length-1];
        }
    }
    handleUpDown($event){
        $event.preventDefault();
        if (!this.isOpen || !this.options.length || !Util.throttle('stTypeaheadHandleUpDown', 100)) return;
        switch ($event.which) {
            case 38: this.handleUp($event); break;
            case 40: this.handleDown($event); break;
        }
    }
    handleEnterTab($event){
        $event.preventDefault();
        if (!this.isOpen && this.search) {
            this.open();
            return;
        } else if (!Util.isObjectEmpty(this.optionSelected)){
            let index = this.options.findIndex(o => this.config.comparator(o, this.optionSelected));
            if (angular.isDefined(index)){
                this.select($event, this.optionSelected, index);
                this.optionSelected = this.options[index] ? this.options[index] : this.options[index-1];
            }
            
        }
         
    }
    keydown($event){
        switch ($event.which){
            case 90: this.handleCtrlZ($event); break;
            case 38: this.handleUpDown($event); break;
            case 40: this.handleUpDown($event); break;
            case 13: this.handleEnterTab($event); break;
            case 9: this.handleEnterTab($event); break;
        }
    }
    _setApi(){
        if (this.api){
            if (this.data) {
                this.api.refreshLocal = this.getOptionsLocal;
            }
        }
    }
    $postLink(){
        this.$searchElement = this.$element.find('input');
    }
    $onInit(){
        this.containerId = Util.generateId();
        if (!this.data && !this.rest) throw new Error('Need REST or ARRAY');
        if (this.data && this.rest) throw new Error(`Cant't have both REST and OPTIONS`);
        this.options = [];
        if (!this.config) this.config = this.StTypeaheadDefaults;
        else this.config = angular.merge({}, this.StTypeaheadDefaults, this.config);
        if (angular.isString(this.config.id)) this.config.comparator = (a, b) => a[this.config.id] === b[this.config.id];
        if (!this.modelValue) this.ngModel.$setViewValue([]);
        this._setModel('Pristine');
        if (!this.modelValue.length) this._setValidity('required', false);
        this.trs = Util.setTranslations(this.translations);
        if (angular.isNumber(this.limit)) this.limit = {limit: this.limit, type: 'block'};
        this.$scope.$watch(() => this.search, (oldValue, newValue) => {
            if (this.ngDisabled) return;
            if (oldValue !== newValue) {
                this.open();
                this.optionSelected = {};
                if (this.rest) {
                    if (this.onRefresh) this.onRefresh({$search: this.search});
                    this.$timeout(() => {
                        this.getOptionsApi();
                    });
                } else {
                    this.$timeout(() => {
                        this.getOptionsLocal();
                    });
                }
            }
        });
    }
}