import { DecimalPipe } from '@angular/common';
import { HttpErrorResponse } from '@angular/common/http';
import { Injectable } from '@angular/core';

import { parse, stringify } from 'flatted/esm';
import { Observable } from 'rxjs/Observable';
import { Subscription } from 'rxjs/Subscription';

import { environment } from '../../../environments/environment';
import { OneITError } from '../error/oneit.error';
import { DateDiff } from '../utils/date-diff';
import { ApiService } from './api.service';
import { ConfigService } from './config.service';
import { MsgsService } from './msgs.service';


@Injectable()
export class UtilsService {

    allValidationMessages;
    counter: number = 1;

    constructor(
        private configService: ConfigService,
        private apiService: ApiService,
        private msgsService: MsgsService,
        private decimalPipe: DecimalPipe
    ) {

    }

    /**
   * Function get ID from string
   */
    getId(id) {
        return id.split(":")[1];
    }

	/**
   * Function create new string to show in autocomplete field
   */
    joinAttribs(obj: any, separator: string, attribs: string[]) {
        let str: string = "";
        let isFirst: boolean = true;

        if (obj) {
            for (let attrib of attribs) {
                let value   =   this.getActualFieldValue(obj, attrib);
                if (value) {

                    if (!isFirst) {
                        str += separator;
                    }
                    str += value;
                    isFirst = false;
                }
            }
        }
        return str;
    }

    addValidationErrorsWithKey(errorMsgKeys: string[], showErrorImmediately: boolean = false) {
        if(errorMsgKeys) {
            for(let key of errorMsgKeys) {
                this.msgsService.addErrorMessages([this.configService.settings.validation[key]]);
            }

            if(showErrorImmediately) {
                this.showAllErrorMessages();
            }
        }
    }

    handleError(e) {
        let errorMsgs: any[];

        if (e instanceof HttpErrorResponse && e.error) {

            if (e.error instanceof Error) {
                errorMsgs = [e.error];
            }
            else if (e.error.errorDetails) {
                errorMsgs = e.error.errorDetails;
            }
            else if (e.status !== undefined && e.status == 0) {
                errorMsgs = ['Server not reachable'];
            }
        } else if (e.oneITMessages) {   //Can't check instanceof OneITError here, strange!!!
            errorMsgs = e.oneITMessages;
        }

        if (!errorMsgs) {
            if(typeof e == 'string') {
                errorMsgs = [e];
            } else {
                console.error(e);
                errorMsgs = ['Unexpected error occurred'];
            }
        }

        if (!environment.production) {
            console.log(errorMsgs);
        }
        this.msgsService.addErrorMessages(errorMsgs);
    }

    clearErrorMessages() {
        this.msgsService.clearErrorMessages();
    }

    showAllErrorMessages() {
        this.msgsService.showAllErrors.next(true);
    }

    handleSuccess() {
        this.handleSuccessMessage(this.configService.settings.messages.SAVE_SUCCESS);
    }

    handleSuccessMessage(message: string) {
        this.msgsService.addSuccessMessage({ severity: 'success', summary: this.configService.settings.messages.MESSAGE_SUCCESS_SUMMARY, detail: message });
    }

    unsubscribeSubscriptions(subscriptions: Array<Subscription>) {
        subscriptions.forEach((subscription: Subscription) => {
            subscription.unsubscribe();
        });
    }

    /**
     * This function will check if server response is successful, if not it will throw error with error message.
     */
    isSuccessfulResponse(data) {
        if (data.result === this.configService.settings.status.success) {
            //Success
            return true;
        } else if (data.errorDetails) {
            throw new OneITError(data.errorDetails);
        }
        throw new Error('Unknown error');
    }

    /**
     * This function will combine result with references. Update references etc. 
     * So final output will have ALL assoc objects set.
     */
    convertResponseToObjects(data, assocs: string[]): any[] {
        let result: any[] = [];
        let record: any = {};

        if (this.isSuccessfulResponse(data)) {
            for (let i in data.results) {
                record = data.references[data.results[i]];

                if (assocs) {
                    this.updateReferences(record, assocs, data.references, 0);
                }
                result.push(record);
            }
        }
        return result;
    }

    updateReferences(obj: any, origAssocs: string[], references, index: number): void {
        let assocs = this.getAssocsForIndex(origAssocs, index);
        for (let assoc of assocs) {
            if (obj[assoc]) {
                if (typeof obj[assoc] == 'string') {
                    if (references[obj[assoc]]) {
                        this.updateReferences(references[obj[assoc]], origAssocs, references, index + 1);
                        obj[assoc] = references[obj[assoc]];
                    }
                } else if (Array.isArray(obj[assoc])) {
                    let multiAssocs = [];

                    for (let assocObj of obj[assoc]) {
                        if (typeof assocObj == 'string') {
                            if (references[assocObj]) {
                                this.updateReferences(references[assocObj], origAssocs, references, index + 1);
                                multiAssocs.push(references[assocObj]);
                            }
                        } else {
                            multiAssocs.push(assocObj);
                        }
                    }
                    obj[assoc] = multiAssocs;
                }
            }
        }
    }

    /**
     * This function will return valid Assoc names from original assocs array supplied.
     * e.g. input assocs:  ["PostcodeSuburb", "PriceList.BaseCosts", "PriceList.ExtraCosts"]
     *      output: for index 0 ["PostcodeSuburb", "PriceList"]
     *              for index 1 ["BaseCosts", "ExtraCosts"]
     */
    getAssocsForIndex(assocs: string[], index: number): string[] {
        let finalAssocs: Set<string> = new Set();
        for (let assoc of assocs) {

            let splittedAssocs: string[] = assoc.split(".");

            if (splittedAssocs.length > index) {
                finalAssocs.add(splittedAssocs[index]);
            }
        }
        return Array.from(finalAssocs.values());
    }

    /**
     * This function will replace BBCs referenced inside main object with just ObjectID.
     * This is added mainly for autocompletes where it will set whole object as reference.
     */
    replaceObjectsWithIDIfExists(origObj: any): any {
        if (origObj) {
            var updatedObj = this.cloneObject(origObj);
            var keys: string[] = Object.keys(updatedObj);

            keys.forEach(key => {
                if (updatedObj[key] && typeof updatedObj[key] == 'object' && updatedObj[key].hasOwnProperty("ObjectID")) {
                    updatedObj[key] = updatedObj[key]["ObjectID"];
                }
                else if (updatedObj[key] && Array.isArray(updatedObj[key])) {
                    updatedObj[key] = this.replaceObjectsWithIDIfExists(updatedObj[key]);
                }
            })
            return updatedObj;
        }
        return origObj;
    }

    /* Check for object is empty or note */
    isEmpty(obj) {
        for (var key in obj) {
            if (obj.hasOwnProperty(key))
                return false;
        }
        return true;
    }

    getValidationMessage(msgKey: string, label: string, error: any) {

        if (!this.allValidationMessages) {
            this.allValidationMessages = this.configService.settings.validation;    //Its initialsed here because its possible that config.settings not initialised when constructor is called
        }
        let errorMsg = this.allValidationMessages[msgKey];

        if (!errorMsg) {
            errorMsg = this.allValidationMessages["default"];
        }
        let params: Map<string, string> = new Map<string, string>();

        params.set("name", label);

        if (error && Object.keys(error)) {
            Object.keys(error).forEach(key => {
                params.set(key, error[key]);
            });
            if (error.requiredLength || (error.requiredLength == 0)) {
                params.set("minlength", error.requiredLength);
            }
        }
        //can add another params here
        return this.replaceParams(errorMsg, params);
    }

    getToday() {
        var date = new Date();
        date.setHours(0, 0, 0, 0);
        return date;
    }

    processObjects(serviceName: string, formParams): Observable<any> {
        let params: any = {
            environment: environment.envName,    //Fetch this from environment e.g. environment.envName
            formParams: this.replaceObjectsWithIDIfExists(formParams)
        }

        return this.apiService.post("svc/" + serviceName, params)
            .map(
                data => {
                    if (this.isSuccessfulResponse(data)) {
                        return data;
                    }
                }
            );
    }

    isEmail(value) {
        return value && /^[_a-z0-9]+(\.[_a-z0-9]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,4})$/.test(value);
    }

    addNULLOptionForAssocSelect(options: any[], labelField: string) {
        return this.addNULLOptionForAssocSelectWithLabel(options, labelField, 'Please Select');
    }

    addNULLOptionForAssocSelectWithLabel(options: any[], labelField: string, blankLabel: string) {
        return this.addNULLOptionForSelectWithLabel(options, labelField, 'ObjectID', blankLabel);
    }

    addNULLOptionForSelectWithLabel(options: any[], labelField: string, valueField: string, blankLabel: string) {
        let nullOption: any = {};

        nullOption[valueField] = null;
        nullOption[labelField] = blankLabel;

        options.unshift(nullOption);
    }

    getNewObjectID() {
        return "NEW:" + this.decimalPipe.transform(this.counter++, '3.0');
    }

    resetCounter() {
        this.counter = 1;
    }

    createObject(createdObj: any, createdObjs: any) {
        let newID = this.getNewObjectID();
        createdObj.ObjectID = newID;
        createdObj.IsNewObject = true;  // flag to identify newly created object

        createdObjs[newID] = createdObj;
    }

    addMultiRefObject(createdObj: any, parentObj: any, multiRefName: string, createdObjs: any) {
        
        this.createObject(createdObj, createdObjs);

        if (!parentObj[multiRefName]) {
            parentObj[multiRefName] = [];
        }
        parentObj[multiRefName].push(createdObj);
    }

    removeMultiRefObject(deletedObj: any, parentObj: any, multiRefName: string, createdObjs: any, updatedObjs: any, deletedObjs: any) {
        const index = parentObj[multiRefName].indexOf(deletedObj);
        parentObj[multiRefName].splice(index, 1);

        this.deleteObject(deletedObj, createdObjs, updatedObjs, deletedObjs);
    }

    deleteObject(deletedObj: any, createdObjs: any, updatedObjs: any, deletedObjs: any) {
        let deletedID = deletedObj.ObjectID;
        if (updatedObjs[deletedID]) {
            deletedObjs[deletedID] = updatedObjs[deletedID];
            delete updatedObjs[deletedID];
        }

        if (createdObjs[deletedID]) {
            delete createdObjs[deletedID];
        }
    }

    addObjsToJSONByObjectID(json: any, objsToBePushed: any[]) {
        if (objsToBePushed) {
            objsToBePushed.forEach(item => {
                if (item && item.ObjectID) {
                    json[item.ObjectID] = item;
                }
            });
        }
    }

    getRouteLinkForIframePage(url: string): string[] {
        return ['iframe-page', url];
    }

    replaceParams(originalStr: string, tokenReplacements: Map<string, string>): string {
        var resultStr = '';
        var prevIndex = 0;
        var myRegexp = /\${([^}]*)}/g;
        var match;
        var clean = true;

        while (match = myRegexp.exec(originalStr)) {
            clean = false;

            var replacementObj = tokenReplacements && tokenReplacements.has(match[1]) ? tokenReplacements.get(match[1]) : '';

            resultStr += originalStr.substring(prevIndex, match.index);
            resultStr += replacementObj.toString();
            prevIndex = myRegexp.lastIndex;
        }

        if (clean) {
            return originalStr;
        }
        else {
            // Append the last bit
            resultStr += originalStr.substring(prevIndex);

            return resultStr;
        }
    }

    formatArray(array: any[], field: string, prefix: string = "", postfix: string = "", delimiter: string = ", ") {

        let result = "";

        if (array == null) {
            return result;
        }

        result += prefix;

        if (array.length > 0) {
            result += (array[0] && array[0][field] ? array[0][field] : "");
        }

        for (let arrayIndex = 1; arrayIndex < array.length; ++arrayIndex) {
            if(result) {    //Don't append delimeter if nothing appended yet
                result += delimiter;
            }

            result += (array[arrayIndex] && array[arrayIndex][field] ? array[arrayIndex][field] : "");
        }

        result += postfix;

        return result;
    }

    sort(array: any[], fields: any[], orders: number[], nullAtTop?: boolean): any[] {
        if (array == null || fields == null) {
            return array;
        }

        let _this = this;

        array = array.sort(function (o1, o2) {
            let diff = 0;
            fields.forEach(function (key, index) {
                if (diff == 0) {
                    let order   =   (orders && orders.length > index) ? orders[index] : 1;
                    diff        =   _this.compare(o1, o2, key, order, nullAtTop);
                }
            });
            return diff;
        });

        return array;
    }

    public compare(o1, o2, key: string, order : number = 1, nullAtTop : boolean = false) {
        let nullSortValue: number = (nullAtTop ? -1 : 1);
        let a = this.getActualFieldValue(o1, key);
        let b = this.getActualFieldValue(o2, key);
    
        if (a == null) {
            return (b == null ? 0 : nullSortValue);
        } else if (b == null) {
            return -nullSortValue;
        } else {
            if (typeof a === 'string' || a instanceof String) {
                if (a.toLowerCase() < b.toLowerCase()) {
                    return -1 * order;
                }
                if (a.toLowerCase() > b.toLowerCase()) {
                    return 1 * order;
                }
            }
            return ((a < b) ? -order : ((a > b) ? order : 0))
        }
    }

    filter(array: any[], fields: any[], filterText : string, matchFull : boolean = true): any[] {
        if (!array || array.length === 0 || !fields || fields.length == 0 || !filterText || filterText.length == 0) {
            return array;
        }

        return array.filter((item) => {
            let     match = false;
            fields.forEach((field) => {
                if (!match && item[field]) {
                    let value =item[field];
                    if (typeof value === 'string' || value instanceof String ) {
                        match = matchFull ? (value.toLowerCase() == filterText.toLowerCase()) : (value.toLowerCase().indexOf(filterText.toLowerCase()) >= 0);
                    } else if (value instanceof Array) {
                        match = value.indexOf(filterText) >= 0;
                    } else {
                        match =  value == filterText;
                    }
                }
            });
            return match;
        });
    }
    
    //This will work similar to Cougar Pipe 
    //e.g. if you pass Job and want to fetch Customer First Name from the same
    //getActualFieldValue(job, Customer.FirstName) will return it
    getActualFieldValue(obj: any, fieldName: string, arrayPrefix: string = '[', arraySuffix: string = ']', arrayDelim: string = ', ') {

        if (obj == null || fieldName == null) {
            return null;
        } else if (fieldName.indexOf('.') == -1) {
            return obj[fieldName];
        } else {
            let val = obj;

            for (let field of fieldName.split('.')) {
                if (val == null) {
                    return null;
                }

                if(val instanceof Array) {
                    val = this.formatArray(val, field, arrayPrefix, arraySuffix, arrayDelim);
                } else {
                    val = val[field];
                }
            }
            return val;
        }
    }

    cloneObject(originalObj) {
        return parse(stringify(originalObj));//Similar to JSON.parse(JSON.stringify(origObj));
    }

    convertDateAttribute(objs: any[], ...fieldNames: string[]) {
        if(objs) {
            objs.forEach(obj => {
                fieldNames.forEach(fieldName => {
                    if(obj[fieldName]) {
                        obj[fieldName] = new Date(obj[fieldName]);
                    }
                });
            });
        }
    }

    convertBlobToFile(theBlob: Blob, fileName:string): File {
        var b: any = theBlob;

        b.lastModifiedDate = new Date();
        b.name = fileName;
        
        return <File>theBlob;
    }

    convertBase64StrToBlob(data: string, sliceSize: number = 512) { // Data are like : "data:image/png;base64,iVBORw0KGg........"

        let block           =   data.split(";");
        var contentType     =   block[0].split(":")[1];// In this case "image/png"
        var realData        =   block[1].split(",")[1];// In this case "iVBORw0KGg...."
        let byteCharacters  =   atob(realData);
        var byteArrays      =   [];

        for (var offset = 0; offset < byteCharacters.length; offset += sliceSize) {

            var slice       =   byteCharacters.slice(offset, offset + sliceSize);
            var byteNumbers =   new Array(slice.length);

            for (var i = 0; i < slice.length; i++) {
                byteNumbers[i] = slice.charCodeAt(i);
            }
            byteArrays.push(new Uint8Array(byteNumbers));
        }
        return new Blob(byteArrays, {type: contentType});
    }

    joinFields(obj: any, fields: string[], prefix: string = "", postfix: string = "", delimiter: string = ", ", includeFieldName: boolean = false, overrideLabels: string[] = []) : string {
        let result = "";

        if (obj == null || fields == null) {
            return result;
        }

        result += prefix;

        if (fields.length > 0) {
            let value   =  this.getActualFieldValue(obj, fields[0]);

            result += ((value || value === 0) ? (this.getLabelIfApplicable(includeFieldName, overrideLabels, fields[0], 0) + value) : "");
        }

        for (let arrayIndex = 1; arrayIndex < fields.length; ++arrayIndex) {

            let value   =  this.getActualFieldValue(obj, fields[arrayIndex]);

            if(value || value === 0) {
                if(result) {    //Don't append delimeter if nothing appended yet
                    result += delimiter;
                }
                result += (this.getLabelIfApplicable(includeFieldName, overrideLabels, fields[arrayIndex], arrayIndex) + value);
            }
        }
        result += postfix;

        return result;
    }

    private getLabelIfApplicable(includeFieldName: boolean = false, overrideLabels: string[] = [], fieldName: string, index: number) {
        if(includeFieldName) {
            if(overrideLabels && overrideLabels.length > index && overrideLabels[index]) {
                fieldName   =   overrideLabels[index];
            }
            return (fieldName + ": ");
        }
        return "";
    }

    public static convertDateToString(date : Date, format : string = 'DD-MM-YYYY') : string {
        return DateDiff.convertDateToString(date, format);
    }

    public static convertStringToDate(dateString : string, format : string = 'DD-MM-YYYY') : Date {
        return DateDiff.convertStringToDate(dateString, format);
    }
}