import { mutate } from 'array-move';

/* @ngInject */
export function StGridFactory(stGrids, StGridDefaults, $timeout, uiGridValidateService, toastr, $translate, StDialogs, $http, $rootScope, $uibModalStack){
    let _getStringDate = () => {
        let date = new Date();
        return `_${date.getDate()}_${date.getMonth()+1}_${date.getFullYear()}_${date.getHours()}_${date.getMinutes()}_${date.getSeconds()}`;
    }
    /**
     * @ngdoc class
     * @name StGrid
     * @memberOf ui.grid.st
     * @description
     * This module is a helper for ui.grid, with this you will have easy access to the main
     * features, and some sugar as well, like methods, some built-in things and defaults to all grids
     * @param { Object } scope Scope of the controller
     * If using the Controller as Syntax, the Scope must be "this" and the grid.appScope will be your "this"
     * @param { string } gridName? Name of the grid
     * If null | undefined | '' it will get the grid id
     * @param { Array } columns? Columns of the grid - Is placed between First and Last Columns defined in the stGridProvider
     * @param { Object } options? Options of the grid - Is merged with the Options provided in the stGridProvider
     * @param { Object } firstLastCols? Name of the columns that goes First and Last
     * @argument { string | Array } firstColumns - Name of the columns that goes First
     * @argument { string | Array } lastColumns - Name of the columns that goes Last
     * @param { Array | string } parentGrids? Name of others grids in the same page
     * It will cause to other grids being disabled when one is loading
     * @example
     * <pre>
     *  myApp.controller('Ctrl', function($scope, StGrid){
     *      let myGrid = new StGrid($scope | this, 
     *                              'gridName',
     *                              Array = [{name: 'field1', display: 'Field 1'}, {name: 'field2', display: 'Field 2'}],
     *                              Object = {rowHeight: 24, enableRowSelection: true},
     *                              Object = {firstColumns: Array | string = 'myFirstColumns', lastColumns: Array | string = ['myLastColumns1', 'myLastColumns2']},
     *                              string | Array = ['myOtherGrid', 'myOtherGrid2']);
     *  })
     * </pre>
     */
    class StGrid {
        constructor(scope, gridName, columns = [], options = {}, firstLastCols = {firstColumns: 'updated'}, parentGrids = []){
            this._scope = scope;
            this._rownum = 0;
            this._setParentGrids(parentGrids);
            this._setCols(columns, firstLastCols);
            this._setOptions(options);
            this._options.onRegisterApi = gridApi => this._gridApi = gridApi;
            this._setOnRegisterApi();
            this._setCustomScroll();
            this._setValidateService();
            this._exclusiveValidators = [];
            this._setDefaultValidators();
            this._setValidatorsCols();
            this._setGridName(gridName);
            $timeout(() => {
                if (!this._gridApi.grid.st) {
                    this._gridApi.grid.st = this;
                }
            });
            this._watchRownum();
        }

        //Primitive methods

        /**
          * @ngdoc method
          * @name toString
          *
          * @methodOf
          * StGrid
          * 
          * @return { string } Returns a string with the grid name and id
          * @example
          * myGrid.toString()
          * * Will return "UI-Grid ST - 'gridName' - ID 'gridId'"
        */
        toString(){
            return `UI-Grid ST - ${this._gridName} - ID ${this._gridApi.grid.id}`;
        }

        /**
          * @ngdoc method
          * @name length
          *
          * @methodOf
          * StGrid
          *
          * @return { number } Returns the length of the data in the grid
        */
        get length(){
            return this._options.data.length;
        }

        //Private Methods
        _watchRownum(){
            $rootScope.$watch(() => this._rownum, (newValue, oldValue) => {
                if (newValue < oldValue) {
                    if (this._options.data.length) {
                        this._options.data.map((o, i) => {
                            o.rownum = i+1;
                            return o;
                        });
                    }
                }
            });
        }
        _setValidateService(){
            this._validateService = {
                validateAll: () => uiGridValidateService.validateAll(this),
                validateUpdated: validators => uiGridValidateService.validateUpdated(this, validators),
                validateColDef: (colDef, validators) => uiGridValidateService.validateColDef(this, colDef, validators),
                setValidator: (name, validatorFactory, messageFunction) => uiGridValidateService.setValidator(name, validatorFactory, messageFunction),
                isInvalid: (rowEntity, colDef) => uiGridValidateService.isInvalid(rowEntity, colDef),
                runValidators: (rowEntity, colDef) => {
                    let newValue = getSafe(rowEntity, colDef.name), 
                    oldValue = `${getSafe(rowEntity, colDef.name)}1`;
                    return uiGridValidateService.runValidators(rowEntity, colDef, newValue, oldValue, this);
                }
            }
        }

        _setGridName(gridName){
            $timeout(() => {
                if (gridName) this._gridName = gridName;
                else this._gridName = this._gridApi.grid.id;
                stGrids[this._gridName] = this;
                let fileName = this._gridName + _getStringDate();
                this._options.exporterCsvFilename = `${fileName}.csv`;
                this._options.exporterExcelFilename = `${fileName}.xlsx`;
            })
        }

        _setDefaultValidators(){
            $timeout(() => {
                if (isNU0(StGridDefaults.validators)) return;
                angular.forEach(StGridDefaults.validators, (value, key) => {
                    if (value.perGrid && this._columns.some(colDef => colDef.validators && colDef.validators[key])){
                        let validatorName = key + this._gridApi.grid.id;
                        this._exclusiveValidators.push({key, validatorName});
                        this._validateService.setValidator(validatorName, argument => {
                            return (oldValue, newValue, rowEntity, colDef) => {
                                return value.func(this.rowData, argument, oldValue, newValue, rowEntity, colDef);
                            }
                        }, args => value.msg(args));
                    }
                })
            })
        }

        _getScope(){
            return isScope(this._scope) ? this._scope : null;
        }

        _setParentGrids(parents){
            $timeout(() => {
                this._parentGrids = [];
                if (isNU0(parents)) return;
                if (angular.isString(parents)){
                    if (stGrids[parents]) this._parentGrids.push(parents);
                    else console.warn(`StGrid - The parent grid "${parents}" was not found`);
                } else if (isArray(parents)){
                    parents.forEach(parent => {
                        if (stGrids[parent] && !this._parentGrids.some(o => o === parent)) this._parentGrids.push(parent);
                        else console.warn(`StGrid - The parent grid "${parent}" was not found`);
                    });
                }
            })

        }

        _setCols(columns, firstLastCols){
            let cols = [],
                lastCols = [],
                firstCols = [];
            if (firstLastCols){
                if (!isNU0(firstLastCols.firstColumns)){
                    if (isArray(firstLastCols.firstColumns)){
                        firstLastCols.firstColumns.forEach(e => {
                            let df = getSafe(StGridDefaults.firstColumns, e);
                            if (angular.isDefined(df)) firstCols = firstCols.concat(df);
                        });
                    } else {
                        let df = getSafe(StGridDefaults.firstColumns, firstLastCols.firstColumns);
                        if (angular.isDefined(df)) firstCols = firstCols.concat(df);
                    }
                }
                if (!isNU0(firstLastCols.lastColumns)){
                    if (isArray(firstLastCols.lastColumns)){
                        firstLastCols.lastColumns.forEach(e => {
                            let df = getSafe(StGridDefaults.lastColumns, e);
                            if (angular.isDefined(df)) lastCols = lastCols.concat(df);
                        });
                    } else {
                        let df = getSafe(StGridDefaults.lastColumns, firstLastCols.lastColumns);
                        if (angular.isDefined(df)) lastCols = lastCols.concat(df);
                    }
                }
            }
            if (!isNU0(firstCols)) cols = cols.concat(firstCols);
            cols = cols.concat(columns);
            if (!isNU0(lastCols)) cols = cols.concat(lastCols);
            let i = cols.length;
            while (i--){
                if (!isNU0(StGridDefaults.columnModels) && cols[i].colModel) cols[i] = Object.assign({}, StGridDefaults.columnModels[cols[i].colModel], cols[i]);
                if (!cols[i].suppressDefaults) cols[i] = Object.assign({}, StGridDefaults.columnDefaults, cols[i]);
            }
            this._columns = angular.copy(cols);
        }

        _colDefValidatorsHasAnyExclusive(colDef){
            if (isNU0(colDef.validators) || isNU0(this._exclusiveValidators)) return false;
            return Object.keys(colDef.validators).some(key => this._exclusiveValidators.some(o => o.validatorName === key));
        }

        _hasGridModule(mod){
            return !!this._gridApi[mod];
        }

        _setValidatorsCols(){
            $timeout(() => {
                let i = this._columns.length;
                while(i--){
                    if (this._columns[i].validators){
                        let { validators } = this._columns[i];
                        angular.forEach(validators, (value, key) => {
                            this._exclusiveValidators.filter(o => o.key === key).forEach(({validatorName}) => {
                                this._columns[i].validators[validatorName] = value;
                                delete this._columns[i].validators[key];
                            })
                        });
                    }
                }
            })
        }

        _translateCol(col){
            return $translate.instant(col);
        }

        _setOptions(options){
            let optionsR = angular.copy(StGridDefaults.options);
            optionsR.columnDefs = this._columns;
            optionsR.gridMenuTitleFilter = this._translateCol;
            optionsR.exporterHeaderFilter = this._translateCol;
            optionsR.appScopeProvider = this._scope;
            this._options = Object.assign({}, optionsR, options);
        }

        _setOnRegisterApi(){
            $timeout(() => {
                if (this._hasGridModule('edit')) {
                    this._gridApi.edit.on.afterCellEdit(this._getScope(), (rowEntity, colDef, newValue, oldValue) => {
                        newValue !== oldValue ? rowEntity.updated = true : '';
                        if (this._colDefValidatorsHasAnyExclusive(colDef)) {
                            let validators = this._exclusiveValidators.map(o => o.validatorName);
                            this._validateService.validateColDef(colDef, validators).catch(() => null);
                        }
                    })
                }
                this._gridApi.grid.registerRowBuilder(row => {
                    row.entity.rownum = ++this._rownum;
                });
            });
        }

        _setLoading(o = true){
            this._options.isLoading = o;
            if (!isNU0(this._parentGrids)){
                let { length } = this._parentGrids;
                while(length--){
                    stGrids[this._parentGrids[length]]._options.parentLoading = o;
                }
            }
        }

        _setCustomScroll(){
            this._options.customScroller = (uiGridViewport, scrollHandler) => {
                uiGridViewport.on('scroll', event => {
                    $rootScope.$broadcast('closeUiSelect');
                    scrollHandler(event);
                });
            }
        }

        _internalError(error){
            let details = '',
                isInternalError = error.hasOwnProperty('status');
            if (!isInternalError){
                let errorShow = '<ul class="list-unstyled">',
                    errorSplit = error.stack.split(window.location.origin).join('').split(' at ');
                errorSplit.forEach((e, i) => {
                    if (i === 0) errorShow += e;
                    else errorShow += `<li style="margin-left:15px">at ${e}`;
                });
                errorShow += '</ul>';
                details = `${$translate.instant('ui-grid-st-internal-error-occurred')} <hr><h5> ${$translate.instant('ui-grid-st-internal-error-info')}:</h5><hr> ${errorShow}`;
            } else if (error.status === -1){
                if (error.xhrStatus === 'abort') return;
                else details = '<p>' + $translate.instant('ui-grid-st-internal-error-no-internet') + '</p>' + $translate.instant('ui-grid-st-internal-error-occurred');
            } else {
                details = $translate.instant('ui-grid-st-internal-error-occurred') + '<hr><h5>' + $translate.instant('ui-grid-st-internal-error-info') + ':</h5><hr>' + error.data;
            }
            toastr.error($translate.instant('ui-grid-st-internal-error-click-details'), $translate.instant('ui-grid-st-internal-error-toastr'), {
                onTap: function(){
                    StDialogs.error((isInternalError ? {
                        translationId: 'ui-grid-st-internal-error-dialogs-http', 
                        values: {
                            code: error.status
                        }
                    } : 'ui-grid-st-internal-error-js'), details, {
                        size: 'lg',
                        rendered: {
                            selector: '.modal-body',
                            apply: 'css',
                            values: ['overflow', 'auto']
                        }
                    });
                }
            });
        }

        _setConfig(config){
            return Object.assign({}, StGridDefaults.config, config);
        }

        _getIndex(row){
            return this._gridApi.grid.renderContainers.body.visibleRowCache.indexOf(row);
        }

        //Public Methods

        /**
         * @ngdoc method
         * @name executeFunction
         * @methodOf StGrid
         * @param { Function } fn
         * @argument { Array } rowData
         * @argument { Object } gridOptions
         * @description Runs a function with rowData and gridOptions
         * @example
         * <pre>
         *      myGrid.executeFunction(function(rowData, gridOptions){
         *          //Do something here
         *      })
         * </pre>
         */
        executeFunction(fn){
            fn.call(this._scope, this._options.data, this._options);
        }

        /**
         * @ngdoc method
         * @name setData
         * @methodOf StGrid
         * @param { Array | Object } data Array/Object to set the data of the Grid
         * @param { Object } config? Object with the options bellow
         * @argument { boolean } noClear? if true it won't clear the current data - Default false
         * @argument { string } orderBy? - Set the data of the grid with order - Default undefined
         * @argument { string | Function } child? - If the data is a object, you can specify the Object/Array that is a child, 
         *   and this will be set instead of the main data - Default undefined
         *   @argument { Array | Object } data Data from grid/Api
         * @example
         * <pre>
         *      myGrid.setData(Array | Object, {
         *          noClear: boolean = true/false, 
         *          orderBy: string = 'field1',
         *          child: string = 'field4'
         *      })
         * </pre>
         */
        setData(data, config = {}){
            if (isNU0(data)){
                this._options.data = [];
                this._rownum = 0;
                return;
            }
            if (config.child) {
                if (angular.isFunction(config.child)) {
                    let child = config.child.call(this._scope, data);
                    data = child ? data[child] : data;
                } else if (angular.isString(config.child)) {
                    data = data[config.child];
                }
            }
            if (!isPlainObject(config)) config = {};
            if (!config.noClear) this.clearData();
            let dateFields = this._columns.filter(colDef => colDef.type === 'date').map(colDef => colDef.name);
            if (isArray(data)){
                for (let i = 0, len = data.length; i < len; i++){
                    let { length } = dateFields;
                    while(length--){
                        data[i][dateFields[length]] = moment(data[i][dateFields[length]]).toDate();
                    }
                    this._options.data.push(data[i]);
                }
            } else if (isPlainObject(data)){
                let { length } = dateFields;
                while(length--){
                    data[dateFields[length]] = moment(data[dateFields[length]]).toDate();
                }
                this._options.data.push(data);
            }
            orderBy(this._options.data, config.orderBy);
            $timeout(() => {
                if (config.doAfter && config.doAfter.args && config.doAfter.fn && angular.isFunction(config.doAfter.fn)) {
                    config.doAfter.args.unshift(this._options.data);
                    config.doAfter.fn.apply(this._scope, config.doAfter.args);
                }
            })
        }
        /**
         * @ngdoc method
         * @name setDataBack
         * @methodOf StGrid
         * @param { Array | Object } data Array/Object to set the data of the Grid
         * @param { Object } config? Object with the options bellow
         * @argument { boolean } noClear? if true it won't clear the current data - Default false
         * @argument { string } orderBy? - Set the data of the grid with order - Default undefined
         * @argument { string } child? - If the data is a object, you can specify the Object/Array that is a child, 
         *   and this will be set instead of the main data - Default undefined
         * @example
         * <pre>
         *      myGrid.setData(Array | Object, {
         *          noClear: boolean = true/false, 
         *          orderBy: string = 'field1',
         *          child: string = 'field4'
         *      })
         * </pre>
         */
        setDataBack(data, config = {}){
            if (!isPlainObject(config)) config = {};
            config.noClear = true;
            this.removeRowsArray('$$hashKey', this.getUpdated);
            this.setData(data, config);
        }
        /**
         * @ngdoc method
         * @name setDataApi
         * @methodOf StGrid
         * @param { Promise } api Rest API
         * @param { Object } config? Object with the options bellow
         * @argument { boolean } noClear? if true it won't clear the current data - Default false
         * @argument { string } orderBy? - Set the data of the grid with order - Default undefined
         * @argument { string } child? - If the data is a object, you can specify the Object/Array that is a child, 
         *   and this will be set instead of the main data - Default undefined
         * @argument { Function } doAfter - Execute a function after the proccess of setting the data is finished
         *   if api is set. Params: gridData and reponseData (from api)
         * @example
         * <pre>
         *      myGrid.setDataApi($http.get('myapi'), {
         *          setData: boolean = true/false
         *          noClear: boolean = true/false, 
         *          orderBy: string = 'field1',
         *          child: string = 'field4',
         *          doAfter: function(gridData, respData){
         *              //do something here
         *          }
         *      })
         * </pre>
         */
        setDataApi(api, config = {}){
            this._setLoading(true);
            config = this._setConfig(config);
            if (!angular.isFunction(api.then)) throw new Error('API must be a promise');
            if (config.setData){
                api.then(resp => {
                    let respData = angular.copy(resp.data);
                    if (config.doAfter) {
                        config.doAfter = {
                            fn: config.doAfter,
                            args: [respData]
                        }
                    }
                    this.setData(resp.data, config);
                });
            }
            api.finally(() => {
                this._setLoading(false);
            }).catch(error => this._internalError(error));
        }

        /**
         * @ngdoc method
         * @name setDataBackApi
         * @methodOf StGrid
         * @param { Object } api Config object for the $http get
         * @param { Object } config? Object with the options bellow
         * @argument { boolean } noClear? if true it won't clear the current data - Default false
         * @argument { string } orderBy? - Set the data of the grid with order - Default undefined
         * @argument { string } child? - If the data is a object, you can specify the Object/Array that is a child, 
         *   and this will be set instead of the main data - Default undefined
         * @argument { Function } doAfter - Execute a function after the proccess of setting the data is finished
         *   if api is set. Params: gridData and reponseData (from api)
         * @argument { boolean } noMsg - Will not show the message at the end of the operation (toast) - default false
         * @example
         * <pre>
         *      myGrid.setDataBackApi($http.get('myapi'), {
         *          setData: boolean = true/false
         *          noClear: boolean = true/false, 
         *          orderBy: string = 'field1',
         *          child: string = 'field4',
         *          doAfter: function(gridData, respData){
         *              //do something here
         *          }
         *      })
         * </pre>
         */
        setDataBackApi(api, config = {}){
            this._setLoading(true);
            config = this._setConfig(config);
            this._validateService.validateUpdated().then(hasErrors => {
                if (hasErrors) {
                    this._setLoading(false);
                    toastr.error($translate.instant('ui-grid-st-save-validation-error'));
                    return;
                }
                let http = $http(api);
                if (config.setData){
                    http.then(resp => {
                        let respData = angular.copy(resp.data);
                        if (config.doAfter) {
                            config.doAfter = {
                                fn: config.doAfter,
                                args: [respData]
                            }
                        }
                        this.setDataBack(resp.data, config);
                        if (!config.noMsg) toastr.success($translate.instant('ui-grid-st-data-saved'));
                    });
                }
                http.finally(() => {
                    this._setLoading(false);
                })
                .catch(error => this._internalError(error))
            }).catch(() => {
                this._setLoading(false);
                toastr.error($translate.instant('ui-grid-st-save-validation-error'));
            })
        }
        /** 
         * @ngdoc method
         * @name removeRows
         * @methodOf StGrid
         * @description Method used to remove rows from the grid
         * @param { string | Function } fn 
         * ID of the row - E.g. "id" or "$$hashKey" or
         * A Callback function with rowEntity, index, rowData as params
         * @param { string | number } value Value to search in the column ID and remove
         * @param { boolean } updateRownum Update rownum?
         * @example
         * <pre>
         *      myGrid.removeRows('myIdField', 1);
         *      or
         *      myGrid.removeRows(function(rowEntity, index, rowData){
         *          return rowEntity.myIdField === 1;
         *      })
         * </pre>
         */
        removeRows(fn, value, updateRownum){
            let isFn = angular.isFunction(fn);
            if (isFn){
                this._options.data = this._options.data.filter((rowEntity, index, rowData) => {
                    return !fn(rowEntity, index, rowData);
                });
            } else if (angular.isString(fn)){
                if (!isArray(value)) value = [value];
                this._options.data = this._options.data.filter(o => {
                    return !value.some(v => v === o[fn]);
                });
            }
            if ((updateRownum) || isFn && value === true) this._rownum = this.length;
        }

        /**
         * @ngdoc method
         * @name removeRowsArray
         * @methodOf StGrid
         * @param { string } key ID of the row - E.g. "id" or "$$hashKey"
         * @param { Array | Object } newData Array/Object with the new Data for remove
         * @description It will compare the value of the column of the grid and the array/object, and remove if equal
         * @example
         * <pre>
         *      myGrid.removeRowsArray('myIdField', Array | Object);
         * </pre>
         */
        removeRowsArray(key, newData){
            if (isPlainObject(newData)){
                this.removeRows(key, newData[key], false);
            } else if (isArray(newData)){
                newData.forEach(rowEntity => this.removeRows(key, rowEntity[key], false));
            } else {
                throw new Error('The newData must be an Array or Object');
            }
            $timeout(() => {
                this._rownum = this.length;
            });
        }

        /**
         * @ngdoc method
         * @name set loading
         * @methodOf StGrid
         * @param { boolean } o : boolean = true to set loading on and false to set off
         * @description Set the current grid and all of its parents to a loading state
         * @example
         * <pre>
         *      myGrid.loading = !myGrid.isLoading;
         * </pre>
         */
        set loading(o){
            this._setLoading(o);
        }

        /**
         * @ngdoc method
         * @name addRows
         * @methodOf StGrid
         * @param { Object | Array } data? Object/Array to add on the Grid
         * @example
         * <pre>
         *      myGrid.addRows(Array | Object)
         * </pre>
         */
        addRows(data = {}){
            this._options.data = this._options.data.concat(data);
        }

        /**
          * @ngdoc method
          * @name removeSelected
          *
          * @methodOf
          * StGrid
          *
          * @description
          * Used to remove the rows from the grid with validations and loading state
          *
          * @param { string } id Name of the colDef containing the id of your rows
          * @param { Object } config A object with some configs:
          * @argument { string } templateUrl A custom template to show the rows that are going to be deleted
          *  in a modal
          * @argument { Object | string } searchBy 
          * Can be defined as a Object and needs to have "field" and "value" as attributes
          * The field is going to be the colDef name that is going to be searched
          * The value is going to be the value to be compared
          * it will compare all rows with have this value in this column and return it
          * If defined as a string it will just compare all rows with the filed to be true
          * If not defined it will get the selectedRows
          * @argument { Object } verify - It's used to verify if the rows can't be deleted
          * Params:
          *   @argument { string } url Url of the rest service
          *   @argument { Object } params? Params of the rest
          *   @argument { string } msg? A message to display if the data can't be deleted
          *   @argument { boolean } noLabel? The list in the modal for deletion won't have a label?
          *   @argument { string } label? What will be the label? If undefined or null it will use
          *   the confirmDeletion param
          *   @argument { string } confirmDeletion? colDef name to show in the list
          *   @argument { boolean } deleteFromList? if true it will enable a icon to the user to remove
          *   the itens from the deletion list
          * It will send a HTTP GET to this URL with the params you sended + an array of ids that will
          * going to be deleted and compare the results with the data to avoid the deletions of the 
          * data returned
          * @argument { Function } doAfter? A function that is executed after the proccess of deletion
          * is done
          * params: 
          *   @argument { Array } rowData The rowData of the grid
          *   @argument { Array } resp The rows deleted
        */
        removeSelected(id, config = {}){
            this._setLoading();
            config.loading = true;
            if (!isPlainObject(config)) config = {};
            if (!config.templateUrl) config.templateUrl = '/stDialogs/confirmDeletion.html';
            let selectedRows = angular.copy(this.getSelectedRows);
            if (config.searchBy){
                let field, value;
                if (isPlainObject(config.searchBy)){
                    field = config.searchBy.field;
                    value = config.searchBy.value;
                } else if (angular.isString(config.searchBy)){
                    field = config.searchBy;
                } else {
                    throw new Error(`UI-Grid ST - removeSelected - config.searchBy must be an Object or string`);
                }
                selectedRows = angular.copy(this._options.data.filter(o => (value ? o[field] === value : o[field])));
            }
            let ids = selectedRows.filter(o => o[id]),
                noIds = selectedRows.filter(o => !o[id]),
                httpGet = fakePromise(true);
            if (isNU0(ids) && !isNU0(noIds)){
                this.removeRowsArray('rownum', noIds);
                toastr.success($translate.instant('ui-grid-st-delete-data'));
                this._setLoading(false);
                return;
            }
            if (config.verify){
                let v = config.verify,
                    params = v.params || {};
                params[v.idParam] = ids.map(o => o[id]);
                httpGet = $http.get(v.url, {params});
            }
            httpGet.then(resp => {
                ids.forEach((e, i) => {
                    if (resp.data.some(o => o[id] === e[id])){
                        ids[i].notRemovable = true;
                    }
                });
                config.loading = false;
            });
            StDialogs.confirmDeletion(ids, config).result.then(resp => {
                this.removeRowsArray(id, resp);
                this.removeRowsArray(id, noIds);
                $timeout(() => {
                    toastr.success($translate.instant('ui-grid-st-delete-data'));
                    if (config.doAfter && angular.isFunction(config.doAfter)){
                        config.doAfter.call(this._scope, this._options.data, resp);
                    }
                    if (this.hasSelectedRows) this._gridApi.selection.clearSelectedRows();
                    this._setLoading(false);
                }, 200);

            }, () => {
                this._setLoading(false);
            });
        }

        /**
          * @ngdoc method
          * @name removeSelectedApi
          *
          * @methodOf
          * StGrid
          *
          * @description
          * Used to remove the rows from the grid with validations and loading state
          *
          * @param { string } id Name of the colDef containing the id of your rows
          * @param { Object } api An object with the configs to do the call for api to delete the rows
          * Needs to have url, method. Optional: params, etc
          * The data sent will be the rows selected at the end of the modal (excluding the ones validated to not be deleted)
          * @param { Object } config A object with some configs:
          * @argument { string } templateUrl A custom template to show the rows that are going to be deleted
          *  in a modal
          * @argument { Object | string } searchBy 
          * Can be defined as a Object and needs to have "field" and "value" as attributes
          * The field is going to be the colDef name that is going to be searched
          * The value is going to be the value to be compared
          * it will compare all rows with have this value in this column and return it
          * If defined as a string it will just compare all rows with the filed to be true
          * If not defined it will get the selectedRows
          * @argument { Object } verify - It's used to verify if the rows can't be deleted
          * Params:
          *   @argument { string } url Url of the rest service
          *   @argument { Object } params? Params of the rest
          *   @argument { string } msg? A message to display if the data can't be deleted
          *   @argument { boolean } noLabel? The list in the modal for deletion won't have a label?
          *   @argument { string } label? What will be the label? If undefined or null it will use
          *   the confirmDeletion param
          *   @argument { string } confirmDeletion? colDef name to show in the list
          *   @argument { boolean } deleteFromList? if true it will enable a icon to the user to remove
          *   the itens from the deletion list
          * It will send a HTTP GET to this URL with the params you sended + an array of ids that will
          * going to be deleted and compare the results with the data to avoid the deletions of the 
          * data returned
          * @argument { Function } doAfter? A function that is executed after the proccess of deletion
          * is done
          * params: 
          *   @argument { Array } rowData The rowData of the grid
          *   @argument { Array } resp The rows deleted
        */
        removeSelectedApi(api, id, config = {}){
            this._setLoading();
            config.loading = true;
            if (!isPlainObject(config)) config = {};
            if (!config.templateUrl) config.templateUrl = '/stDialogs/confirmDeletion.html';
            let selectedRows = angular.copy(this.getSelectedRows);
            if (config.searchBy){
                let field, value;
                if (isPlainObject(config.searchBy)){
                    field = config.searchBy.field;
                    value = config.searchBy.value;
                } else if (angular.isString(config.searchBy)){
                    field = config.searchBy;
                } else {
                    throw new Error(`UI-Grid ST - removeSelected - config.searchBy must be an Object or string`);
                }
                selectedRows = angular.copy(this._options.data.filter(o => (value ? o[field] === value : o[field])));
            }
            let ids = selectedRows.filter(o => o[id]),
                noIds = selectedRows.filter(o => !o[id]),
                httpGet = fakePromise(true);
            if (isNU0(ids) && !isNU0(noIds)){
                this.removeRowsArray(id, noIds);
                toastr.success($translate.instant('ui-grid-st-delete-data'));
                this._setLoading(false);
                return;
            }
            if (config.verify){
                let v = config.verify,
                params = v.params || {};
                params[v.idParam] = ids.map(o => o[id]);
                httpGet = $http.get(v.url, {params});
            }
            httpGet.then(resp => {
                ids.forEach((e, i) => {
                    if (resp.data.some(o => o[id] === e[id])){
                        ids[i].notRemovable = true;
                    }
                });
            }).finally(() => config.loading = false);
            StDialogs.confirmDeletion(ids, config).result.then(resp => {
                api.data = resp.map(o => o[id]);
                return $http(api).then(() => {
                    this.removeRowsArray(id, resp);
                    this.removeRowsArray(id, noIds);
                    if (config.doAfter && angular.isFunction(config.doAfter)){
                        config.doAfter.call(this._scope, this._options.data, resp);
                    }
                    if (this.hasSelectedRows) this._gridApi.selection.clearSelectedRows();
                    toastr.success($translate.instant('ui-grid-st-delete-data'));
                }).finally(() => {
                    this._setLoading(false);
                }).catch(error => this._internalError(error));
            }, () => {
                this._setLoading(false);
            });
        }
        /**
         * @ngdoc method
         * @name get isLoading
         * @methodOf StGrid
         * @returns { boolean }
         * @description Returns true or false depending on the grid state (and its parents)
         */

        get isLoading(){
            if (!isNU0(this._parentGrids)){
                return this._options.isLoading || this._parentGrids.some(grid => stGrids[grid]._options.isLoading);
            }
            return this._options.isLoading;
        }
        /**
         * @ngdoc method
         * @name get getUpdated
         * @methodOf StGrid
         * @returns { Array } The rows that have been updated
         * @example
         * <pre>
         *      let rowsUpdated = myGrid.getUpdated
         * </pre>
         */
        get getUpdated(){
            return this._options.data.filter(o => o.updated);
        }
        /**
         * @ngdoc method
         * @name set parentGrids
         * @methodOf StGrid
         * @param { Array | string } parents Must be and Array of strings or a string
         * @description Used to set the parent grid after the declaration of the grid
         */
        set parentGrids(parents){
            this._setParentGrids(parents)
        }

        /**
         * @ngdoc method
         * @name getErrors
         * @methodOf StGrid
         * @returns { Array } Array with the validation errors of the Grid
         * Array of Objects like this: [{rowEntity: Object, colDef: Object, errors: Array}]
         * @example
         * <pre>
         *      let errors = myGrid.getErrors
         * </pre>
         */
        get getErrors(){
            let errors = [];
            this._columns.forEach(colDef => {
                this._options.data.forEach(rowEntity => {
                    if (!isNU0(this._gridApi.validate.getErrorMessages(rowEntity, colDef))){
                        errors.push({rowEntity, colDef, errors: this._gridApi.validate.getErrorMessages(rowEntity, colDef)});
                    };
                })
            })
            return isNU0(errors) ? null : errors;
        }
        /**
         * @ngdoc method
         * @name hasErrors
         * @methodOf StGrid
         * @returns { boolean } True or false based on the validators
         */
        get hasErrors(){
            return this._columns.some(colDef => {
                return this._options.data.some(rowEntity => {
                    return this._validateService.isInvalid(rowEntity, colDef);
                });
            });
        }
        /**
         * @ngdoc method
         * @name clearData
         * @methodOf StGrid
         * @description Clear the rowData of the grid
         */
        clearData(){
            this._setLoading(false);
            this.setData();
            this._rownum = 0;
        }

        /**
         * @ngdoc method
         * @name get hasSelectedRows
         * @methodOf StGrid
         * @returns { Boolean }
         * @description Returns if the grid has any selected row
         */
        get hasSelectedRows(){
            return !!this._gridApi.selection.getSelectedRows().length;
        }

        /**
         * @ngdoc method
         * @name get getSelectedRows
         * @methodOf StGrid
         * @returns { Array }
         * @description Returns an array of the rowData that is selected
         */
        get getSelectedRows(){
            return this._gridApi.selection.getSelectedRows();
        }

        /**
         * @ngdoc method
         * @name get api
         * @methodOf StGrid
         * @returns { Object } GridApi
         * @description Returns the GridApi of the ui-grid
         * Be aware that you will need to add a $timeout to use it
         * @example
         * <pre>
         *      let myGrid = new StGrid($scope, 'myGrid');
         *      $timeout(() => {
         *          myGrid.api.onSomething();
         *      })
         * </pre>
         */
        get api(){
            return this._gridApi;
        }

        /**
         * @ngdoc method
         * @name get options
         * @methodOf StGrid
         * @returns { Object } GridOptions
         * @description Returns the gridOptions
         */
        get options(){
            return this._options;
        }

        /**
         * @ngdoc method
         * @name get columns
         * @methodOf StGrid
         * @returns { Array }
         * @description Returns an Array with the columns of the grid
         */
        get columns(){
            return this._columns;
        }

        /**
         * @ngdoc method
         * @name get hasUnsavedData
         * @methodOf StGrid
         * @returns { boolean }
         * @description Get if the grid has any unsaved data (if you're using the updated model)
         */
        get hasUnsavedData(){
            return !!this._options.data.some(o => o.updated) && !this.isLoading;
        }

        /**
         * @ngdoc method
         * @name get name
         * @methodOf StGrid
         * @returns { string }
         * @description Returns the name of the grid
         */
        get name(){
            return this._gridName;
        }

        /**
         * @ngdoc method
         * @name get rowData
         * @methodOf StGrid
         * @returns { Array }
         * @description Returns the current data of the grid (rowData)
         */
        get rowData(){
            return this._options.data;
        }

        /**
         * @ngdoc method
         * @name get validateService
         * @methodOf StGrid
         * @returns { Object }
         * @description Returns the uiValidateService from the ui-grid
         */
        get validateService(){
            return this._validateService;
        }

        /**
          * @ngdoc method
          * @name move
          * @methodOf StGrid
          * @description Used to move the elements of the rowData
          * @param { string } dir Direction (up or down)
          * @param { GridRow } row Row of the grid
          * @param { number } count? How many rows to move (default 1)
        */
        canMove(dir, row, count){
            let index = this._getIndex(row);
            if (dir === 'down') {
                return !(index+1 === this.length || index+1 + count > this.length)
            } else if (dir === 'up') {
                return !(index === 0 || index+1 - count < 0);
            }
        }
        move(dir, row){
            if (!this.length) return;
            let index = this._getIndex(row);
            let count = 1;
            if (dir === 'down') {
                if (!this.canMove(dir, row, count)) return;
                this._options.data[index+1].rownum--;
                this._options.data[index+1].updated = true;
                row.entity.rownum++;
                row.entity.updated = true;
                mutate(this._options.data, index, index+count);
            } else if (dir === 'up') {
                if (!this.canMove(dir, row, count)) return;
                this._options.data[index-1].rownum++;
                this._options.data[index-1].updated = true;
                row.entity.rownum--;
                row.entity.updated = true;
                mutate(this._options.data, index, index-count);
            }
        }

    }
    return StGrid;
}