import { BaseView } from "./base-view";
import { PouchService } from "../services/pouch-service";
import { Observable } from "rxjs/Observable";
import { Observer } from "rxjs/Observer";

import { EventsManager } from "../base/events/event-manager";

/**
 *
 * ██████╗  █████╗ ███████╗███████╗██╗     ██╗███████╗████████╗██╗   ██╗██╗███████╗██╗    ██╗
 * ██╔══██╗██╔══██╗██╔════╝██╔════╝██║     ██║██╔════╝╚══██╔══╝██║   ██║██║██╔════╝██║    ██║
 * ██████╔╝███████║███████╗█████╗  ██║     ██║███████╗   ██║   ██║   ██║██║█████╗  ██║ █╗ ██║
 * ██╔══██╗██╔══██║╚════██║██╔══╝  ██║     ██║╚════██║   ██║   ╚██╗ ██╔╝██║██╔══╝  ██║███╗██║
 * ██████╔╝██║  ██║███████║███████╗███████╗██║███████║   ██║    ╚████╔╝ ██║███████╗╚███╔███╔╝
 * ╚═════╝ ╚═╝  ╚═╝╚══════╝╚══════╝╚══════╝╚═╝╚══════╝   ╚═╝     ╚═══╝  ╚═╝╚══════╝ ╚══╝╚══╝
 *
 *  Clase que reúne todas la capacidades para mostrar e interactuar con una vista de lista.
 *
 */
export abstract class BaseListView<T> extends BaseView<T>{

    items: T[];
    itemsShown: T[];
    itemsMatched: T[];
    selectedItem: T;
    filters: Array<[string, string, any]>;
    search: boolean;
    range: { start: number, end: number };

    constructor(c: { new (): T; }, ...filters: Array<[string, string, any]>) {
        super(c, PouchService);

        this.items = [];
        this.itemsShown = [];
        this.itemsMatched = [];
        this.selectedItem = null;
        this.filters = filters;
        this.search = false;
        this.range = { start: 0, end: 10 }
        
        this.initialize();
    }

    // ------------------------------------------------------------
    //   ██████╗ ███████╗████████╗████████╗███████╗██████╗ ███████╗
    //  ██╔════╝ ██╔════╝╚══██╔══╝╚══██╔══╝██╔════╝██╔══██╗██╔════╝
    //  ██║  ███╗█████╗     ██║      ██║   █████╗  ██████╔╝███████╗
    //  ██║   ██║██╔══╝     ██║      ██║   ██╔══╝  ██╔══██╗╚════██║
    //  ╚██████╔╝███████╗   ██║      ██║   ███████╗██║  ██║███████║
    //   ╚═════╝ ╚══════╝   ╚═╝      ╚═╝   ╚══════╝╚═╝  ╚═╝╚══════╝
    // ------------------------------------------------------------

    /**
     * 
     */
    getItems(): T[] {
        return this.items;
    }

    /**
     * 
     */
    getItemsShown(): T[] {
        return this.itemsShown;
    }

    /**
     * 
     */
    getItemsMatched(): T[] {
        return this.itemsMatched;
    }

    /**
     * 
     */
    getSelectedItem(): T {
        return this.selectedItem;
    }

    /**
     * 
     */
    getFilters(): Array<[string, string, any]> {
        return this.filters;
    }

    /**
     * 
     */
    getRange(): { start: number, end: number } {
        return this.range;
    }
    
    // -----------------------------------------------------------
    //  ███████╗███████╗████████╗████████╗███████╗██████╗ ███████╗
    //  ██╔════╝██╔════╝╚══██╔══╝╚══██╔══╝██╔════╝██╔══██╗██╔════╝
    //  ███████╗█████╗     ██║      ██║   █████╗  ██████╔╝███████╗
    //  ╚════██║██╔══╝     ██║      ██║   ██╔══╝  ██╔══██╗╚════██║
    //  ███████║███████╗   ██║      ██║   ███████╗██║  ██║███████║
    //  ╚══════╝╚══════╝   ╚═╝      ╚═╝   ╚══════╝╚═╝  ╚═╝╚══════╝
    // -----------------------------------------------------------
                                                          
    /**
     * 
     * @param items 
     */
    setItems(items: T[]): void {
        this.items = items;
        this.setItemsShown(this.getItems().slice(this.getRange().start, this.getRange().end));
    }

   
    /**
     * 
     * @param itemsShown 
     */
    setItemsShown(itemsShown: T[]): void {
        this.itemsShown = itemsShown;
    } 

    

    /**
     * 
     * @param itemsMatched 
     */
    setItemsMatched(itemsMatched: T[]): void {
        this.itemsMatched = itemsMatched;
    }

    /**
     * 
     * @param item 
     */
    setSelectedItem(item: T): void {
        this.selectedItem = item;
    }

    /**
     * 
     * @param filters 
     */
    setFilters(filters: Array<[string, string, any]>): void {
        this.filters = filters;

        this.applyFilter(this.getItems());
    }

    /**
     * 
     */
    isSearch(): boolean {
        return this.search;
    }
    
    /**
     * 
     * @param search 
     */
    setSearch(search: boolean): void {
        this.search = search;
    }

    /**
     * 
     * @param range 
     */
    setRange(range: { start: number, end: number }): void {
        this.range = range;
    }

    // -------------------------------------------------------------------------------------------------------------------
    //  ███████╗██████╗ ███████╗ ██████╗██╗ █████╗ ██╗          ██████╗ ███████╗████████╗████████╗███████╗██████╗ ███████╗
    //  ██╔════╝██╔══██╗██╔════╝██╔════╝██║██╔══██╗██║         ██╔════╝ ██╔════╝╚══██╔══╝╚══██╔══╝██╔════╝██╔══██╗██╔════╝
    //  ███████╗██████╔╝█████╗  ██║     ██║███████║██║         ██║  ███╗█████╗     ██║      ██║   █████╗  ██████╔╝███████╗
    //  ╚════██║██╔═══╝ ██╔══╝  ██║     ██║██╔══██║██║         ██║   ██║██╔══╝     ██║      ██║   ██╔══╝  ██╔══██╗╚════██║
    //  ███████║██║     ███████╗╚██████╗██║██║  ██║███████╗    ╚██████╔╝███████╗   ██║      ██║   ███████╗██║  ██║███████║
    //  ╚══════╝╚═╝     ╚══════╝ ╚═════╝╚═╝╚═╝  ╚═╝╚══════╝     ╚═════╝ ╚══════╝   ╚═╝      ╚═╝   ╚══════╝╚═╝  ╚═╝╚══════╝
    // -------------------------------------------------------------------------------------------------------------------
                                                                                                                  
    /**
     * 
     */
    getStartRange(): number {
        return this.getRange().start;
    }

    /**
     * 
     */
    getEndRange(): number {
        return this.getRange().end;
    }

    /**
     * 
     */
    hasItems(): boolean {
        return this.getItems().length > 0;
    }

    /**
     * 
     */
    hasItemsShown(): boolean {
        return this.getItemsShown().length > 0;
    }

    /**
     * 
     */
    isExpandableRange(): boolean {
        return this.getRange().end < this.getItemsShown().length;
    }

    /**
     * 
     */
    getSelectedIndexInAll(): number {
        if (!this.getSelectedItem()) {
            return -1;
        }

        return this.getItems().indexOf(this.getSelectedItem());
    }

    /**
     * 
     */
    getSelectedIndexInShown(): number {
        if (!this.getSelectedItem()) {
            return -1;
        }

        return this.getItemsShown().indexOf(this.getSelectedItem());
    }

    /**
     * 
     */
    getSelectedIndexInMatched(): number {
        if (!this.getSelectedItem()) {
            return -1;
        }

        return this.getItemsMatched().indexOf(this.getSelectedItem());
    }

    // ------------------------------------------------------------------------------------------------------------------
    //  ███████╗██████╗ ███████╗ ██████╗██╗ █████╗ ██╗         ███████╗███████╗████████╗████████╗███████╗██████╗ ███████╗
    //  ██╔════╝██╔══██╗██╔════╝██╔════╝██║██╔══██╗██║         ██╔════╝██╔════╝╚══██╔══╝╚══██╔══╝██╔════╝██╔══██╗██╔════╝
    //  ███████╗██████╔╝█████╗  ██║     ██║███████║██║         ███████╗█████╗     ██║      ██║   █████╗  ██████╔╝███████╗
    //  ╚════██║██╔═══╝ ██╔══╝  ██║     ██║██╔══██║██║         ╚════██║██╔══╝     ██║      ██║   ██╔══╝  ██╔══██╗╚════██║
    //  ███████║██║     ███████╗╚██████╗██║██║  ██║███████╗    ███████║███████╗   ██║      ██║   ███████╗██║  ██║███████║
    //  ╚══════╝╚═╝     ╚══════╝ ╚═════╝╚═╝╚═╝  ╚═╝╚══════╝    ╚══════╝╚══════╝   ╚═╝      ╚═╝   ╚══════╝╚═╝  ╚═╝╚══════╝
    // ------------------------------------------------------------------------------------------------------------------
                                                                                                                 
    /**
     * 
     * @param start 
     */
    setStartRange(start: number): void {
        this.setRange({ start: start, end: this.getRange().end });
    }

    /**
     * 
     * @param end 
     */
    setEndRange(end: number): void {
        this.setRange({ start: this.getRange().start, end: end });
    }

    //---------------------------------------------------------------
    //  ███████╗███████╗██████╗ ██╗   ██╗██╗ ██████╗███████╗███████╗
    //  ██╔════╝██╔════╝██╔══██╗██║   ██║██║██╔════╝██╔════╝██╔════╝
    //  ███████╗█████╗  ██████╔╝██║   ██║██║██║     █████╗  ███████╗
    //  ╚════██║██╔══╝  ██╔══██╗╚██╗ ██╔╝██║██║     ██╔══╝  ╚════██║
    //  ███████║███████╗██║  ██║ ╚████╔╝ ██║╚██████╗███████╗███████║
    //  ╚══════╝╚══════╝╚═╝  ╚═╝  ╚═══╝  ╚═╝ ╚═════╝╚══════╝╚══════╝                                                    
    //---------------------------------------------------------------

    /**
     * Inicializa la lista.
     * Emite una evento al empezar y terminar el proceso.
     */
    initialize() {
        EventsManager.publish("app:loading", true);

        super.getInjectable(PouchService).getAll(this.getModelName()).subscribe(result => {
            this.applyFilter(result.docs);            
            this.subcribeToEvents();

            EventsManager.publish("app:loading", false);
        }, error => { 
            console.log(error);

            EventsManager.publish("app:loading", false);
            EventsManager.publish("app:error", error);
        });
    }

    /**
     * Aplica los filtros correspondientes a la lista
     */
    applyFilter(records: T[]): void {
        if (records.length == 0) {
            return;
        }
        
        if (this.getFilters().length == 0) {
            this.setItems(records);

            return;
        }

        this.setItems(records.filter(record => {
            let expressions = this.getFilters().map(filter => {
                let operator = "===";
                let leftOperand = "record." + filter[0];
                let rightOperand = typeof(filter[2]) === "string" ? "'" + filter[2] + "'" : filter[2];

                if (filter[1] === "!=") {
                    operator = "!==";
                }

                if (filter[1] === ">") {
                    operator = ">";
                }

                if (filter[1] === "<") {
                    operator = "<";
                }

                if (filter[1] === ">=") {
                    operator = "<=";
                }

                if (filter[1] === "<=") {
                    operator = "<=";
                }

                return leftOperand + operator + rightOperand;
            });

            return eval(expressions.join("&&"));
        }));
    }

    /**
     * Suscribe a los eventos de la lista
     */
    private subcribeToEvents(): void {
        EventsManager.subscribe("app:changed", data => {
            if (data.action === "add") {
                this.add(data.item);
            }

            if (data.action === "update") {
                this.update(data.item);
            }

            if (data.action === "delete") {
                this.remove();
            }

            this.unselectItem();
        });
    }

    /**
     * Adicioana a la lista un nuevo objeto guardado
     * @param obj 
     */
    add(item: T): void {
        this.items.unshift(item);

        if (this.getItemsShown().length < this.getEndRange()) {
            this.itemsShown.unshift(item);
        }
    }

    /**
     * Actualiza el objeto modificado
     * @param item 
     */
    update(item: T): void {
        this.itemsShown[this.getSelectedIndexInShown()] = item;

        if (this.isSearch()) {
            this.itemsMatched[this.getSelectedIndexInShown()] = item;
        }

        this.items[this.getSelectedIndexInAll()] = item;
    }

    /**
     * Quita el objeto borrado de la lista
     */
    remove(): void {
        this.itemsShown.splice(this.getSelectedIndexInShown(), 1);

        if (this.isSearch()) {
            this.itemsMatched.splice(this.getSelectedIndexInShown(), 1);
        }

        this.items.splice(this.getSelectedIndexInAll(), 1);
    }

    /**
     * Deselecciona el elemento que haya sido previamente seleccionado.
     */
    unselectItem(): void {
        this.setSelectedItem(null);
    }

    /**
     * Limpia las coincidencias de búsqueda.
     */
    clearItemsMatched(): void {
        this.setItemsMatched([]);
    }

    /**
     * Alterna entre el modo de lista y el modo de búsqueda de la lista.
     */
    toggleSearch(): void {
        this.setSearch(!this.isSearch());
        this.setRange({ start: 0, end: 10 });

        if (!this.isSearch()) {
            this.setItemsMatched([]);
            this.setItemsShown(this.getItems().slice(this.getRange().start, this.getRange().end));
        }
    }

    /**
     * Realiza la expansión del rango para la vista.
     */
    expandRange(): void {
        this.setRange({ start: this.getRange().end, end: this.getRange().end + 10 });

        if (this.isSearch()) {
            this.setItemsShown(this.getItemsShown().concat(this.getItemsMatched().slice(this.getRange().start, this.getRange().end)));
        } else {
            this.setItemsShown(this.getItemsShown().concat(this.getItems().slice(this.getRange().start, this.getRange().end)));
        }
    }

 
    /**
     *  Realiza la expasión de la vista de la lista.
     * 
     * @param event 
     */
    expand(event: any): void {
        this.expandRange();

        if (this.isExpandableRange()) {
            event.enable(false);
        }

        event.complete();
    }

    /**
     * Realiza la busqueda cada campo de los objetos en la lista de items mientras este campo sea de tipo string.
     * Emite un evento cuando comienza y termina el proceso.
     * @param text 
     */
    performSearch(event: any): void {
        let value: string = event.target.value;

        if (!value || value.length < 3) {
            return;
        }
        
        EventsManager.publish("app:loading", true);

        this.clearItemsMatched();
        
        let regExp = new RegExp(value, "g");
        let item: T = null;
        let keys: Array<any> = null;

        for (let i = 0; i < this.getItems().length; i++) {
            item = this.getItems()[i];
            keys = Object.keys(item);

            for (let j = 0; j < keys.length; j++) {
                if (typeof item[keys[j]] !== "string") {
                    continue;
                }

                if (!!item[keys[j]].toLowerCase().match(regExp)) {
                    this.getItemsMatched().push(item);
                    break;
                }
            }
        }

        this.setItemsShown(this.getItemsMatched().slice(this.getRange().start, this.getRange().end));
        
        EventsManager.publish("app:loading", false);
    }

    /**
     * Realiza el borrado del item seleccionado.
     * Emite un evento al terminar el proceso.
     */
    performDelete(): void {
        super.getInjectable(PouchService).remove(this.getSelectedItem()).subscribe(result => { 
            EventsManager.publish("app:changed", {
                action: "delete",
                details: result
            });
        }, error => { 
            EventsManager.publish("app:error", error);
        });
    }
} 