// this is stored off so we not need to check each time
import React from "react";
import { Guid } from "guid-typescript";
import $RefParser from "@apidevtools/json-schema-ref-parser";
import { FileData } from "./FileData";
import { ArrayAddDataMemberAccessor, ArrayAddFirstDataMemberAccessor, ArrayFirstDataMemberAccessor, ArrayItemDataMemberAccessor, ArrayLastDataMemberAccessor, PropertyDataMemberAccessor, } from "./DataMemberAccessor";
import { XDataType } from "./Data/XDataType";
import { UndefinedDataDefinition } from "./Data/Definitions/UndefinedDataDefinition";
import { FileDataDefinition } from "./Data/Definitions/FileDataDefinition";
import { ObjectDataDefinition } from "./Data/Definitions/ObjectDataDefinition";
import { DateDataDefinition } from "./Data/Definitions/DateDataDefinition";
import { NumberDataDefinition } from "./Data/Definitions/NumberDataDefinition";
import { StringDataDefinition } from "./Data/Definitions/StringDataDefinition";
import { BooleanDataDefinition } from "./Data/Definitions/BooleanDataDefinition";
import { ValidationIssue } from "./IDesignValidationProvider";
export class DataUtilities {
    static compare(v1, v2) {
        // we are cloning to get rid of stuff we do not want to compare
        // we can not use this function as it's for node (backend).
        // return util.isDeepStrictEqual(this.clone(v1), this.clone(v2));
        return Object.is(this.clone(v1), this.clone(v2));
    }
    static clone(value) {
        let output = {};
        for (var i in value) {
            if (this.reactProperties.indexOf(i) !== -1)
                continue;
            if (i === "isReactComponent")
                continue;
            // these are special properties we do not want in output
            if (!i.startsWith("_____"))
                Reflect.set(output, i, Reflect.get(value, i));
        }
        return output;
    }
    static cloneWithoutReact(value, alsoExclude = []) {
        let output = {};
        for (var i in value) {
            if (this.reactProperties.indexOf(i) !== -1)
                continue;
            if (i === "isReactComponent")
                continue;
            if (alsoExclude.indexOf(i) !== -1) {
                // excluding
            }
            else {
                // these are special properties we do not want in output
                if (!i.startsWith("_____"))
                    Reflect.set(output, i, Reflect.get(value, i));
            }
        }
        return output;
    }
    static spreadData(value, dataToSpread, exclude) {
        for (var i in dataToSpread) {
            if (exclude && exclude.length !== 0) {
                if (exclude.indexOf(i) !== -1)
                    continue;
            }
            Reflect.set(value, i, Reflect.get(dataToSpread, i));
        }
    }
    static spreadDeepData(value, dataToSpread, exclude) {
        for (var i in dataToSpread) {
            if (exclude && exclude.length !== 0) {
                if (exclude.indexOf(i) !== -1)
                    continue;
            }
            let newValue = Reflect.get(dataToSpread, i);
            if (typeof newValue === "object") {
                let copiedValue = Array.isArray(newValue) ? [] : {};
                this.spreadDeepData(copiedValue, newValue);
                Reflect.set(value, i, copiedValue);
            }
            else {
                Reflect.set(value, i, newValue);
            }
        }
    }
    static spreadDataWithoutFunction(value, dataToSpread, exclude) {
        for (var i in dataToSpread) {
            if (exclude && exclude.length !== 0) {
                if (exclude.indexOf(i) !== -1)
                    continue;
            }
            let val = Reflect.get(dataToSpread, i);
            if (val instanceof Function)
                continue;
            Reflect.set(value, i, val);
        }
    }
    static generateUniqueId() {
        return Guid.create().toString();
    }
    static isValidDate(possibleDate) {
        // todo: might need to do more here
        return possibleDate instanceof Date;
    }
    static isValidFile(possibleFile) {
        return this.compareObjectStructure(possibleFile, new FileData("https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png"));
    }
    static getElapsedTime(startTime, endTime) {
        let diff = endTime.getTime() - startTime.getTime();
        let msc = diff % 1000;
        let ss = Math.floor(diff / 1000);
        if (ss === 0) {
            return `${msc} ms`;
        }
        let mm = Math.floor(ss / 60);
        if (mm === 0) {
            return `${ss} second${ss > 1 ? "s" : ""}, ${msc} ms`;
        }
        ss = ss % 60;
        let hh = Math.floor(mm / 60);
        if (hh === 0) {
            return `${mm} minute${mm > 1 ? "s" : ""}, ${ss} second${ss > 1 ? "s" : ""}`;
        }
        mm = mm % 60;
        return `${`${hh}`.padStart(2, "0")}:${`${mm}`.padStart(2, "0")}:${`${ss}`.padStart(2, "0")}`;
    }
    // ensure object has all the values and functions that matchesObject has
    static compareObjectStructure(object, matchesObject) {
        const keys1 = Object.keys(object);
        const keys2 = Object.keys(matchesObject);
        for (let key of keys2) {
            if (keys1.indexOf(key) === -1) {
                return false;
            }
        }
        return true;
    }
    static isAssignable(val, toVal) {
        // we cannot make judgement
        if (val === null || val === undefined || toVal === null || toVal === undefined)
            return true;
        // array types do not match
        if (Array.isArray(val) !== Array.isArray(toVal))
            return false;
        // types do not match
        if (typeof val != typeof toVal)
            return false;
        // if object, do structure compare
        if (typeof val === "object")
            return true;
        return true;
    }
    static isDateString(checkVal) {
        const dateFormat = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*))(?:Z|(\+|-)([\d|:]*))?$/;
        return dateFormat.test(checkVal);
    }
    static getDateFromString(s) {
        return new Date(Date.parse(s));
    }
    // todo: add cache?
    static splitPath(path) {
        let dataMembers = [];
        let pathPieces = path.split(".");
        for (let p of pathPieces) {
            if (p.indexOf("[") !== -1) {
                // remove ]
                p = p.replace("]", "");
                // split again
                let subPieces = p.split("[");
                if (subPieces[subPieces.length - 1] === "")
                    subPieces.splice(subPieces.length - 1, 1);
                if (subPieces.length === 1) {
                    // this is case where [] means we are goign to return all the array pieces
                    dataMembers.push(new PropertyDataMemberAccessor(subPieces[0]));
                }
                else {
                    // first piece = property
                    dataMembers.push(new PropertyDataMemberAccessor(subPieces[0]));
                    // second piece = index or first or last
                    if (subPieces[1] === "add") {
                        dataMembers.push(new ArrayAddDataMemberAccessor());
                    }
                    else if (subPieces[1] === "addfirst") {
                        dataMembers.push(new ArrayAddFirstDataMemberAccessor());
                    }
                    else if (subPieces[1] === "first") {
                        dataMembers.push(new ArrayFirstDataMemberAccessor());
                    }
                    else if (subPieces[1] === "last") {
                        dataMembers.push(new ArrayLastDataMemberAccessor());
                    }
                    else if (subPieces[1] === "all") {
                        // do nothing
                    }
                    else {
                        dataMembers.push(new ArrayItemDataMemberAccessor(+subPieces[1]));
                    }
                }
            }
            else {
                dataMembers.push(new PropertyDataMemberAccessor(p));
            }
        }
        return dataMembers;
    }
    // rules
    // - path is separated by .
    // - [] returns all items of array
    // - . on array without specifying that we want all items '[]' returns the value of the property
    // - . after [] returns new array with the items of that particular property
    // - [first] and [last] return the first and last item
    static get(from, path, defaultvalue) {
        let pieces = this.splitPath(path);
        let v = from;
        for (let p of pieces) {
            v = p.get(v);
            if (v === undefined || v === null)
                return undefined;
        }
        return v;
    }
    static set(from, path, value, throwExceptionIfTargetIsNull = true, notCheckType = false) {
        let pieces = this.splitPath(path);
        let v = from;
        let lastPiece = pieces[pieces.length - 1];
        // removes last item from array
        pieces = pieces.slice(0, -1);
        for (let p of pieces) {
            v = p.get(v);
            if (v === undefined || v === null) {
                if (throwExceptionIfTargetIsNull)
                    throw new Error(`an parent item to item being set is null or undefined ${path}`);
                return;
            }
        }
        let currentVal = lastPiece.get(v);
        if (!notCheckType) {
            if (currentVal && typeof currentVal === "string" && typeof value !== "string" && value)
                value = value + "";
            if (!this.isAssignable(value, currentVal))
                throw new Error(`unassignable: cannot replace ${typeof currentVal} ${currentVal} with ${typeof value} ${value}`);
        }
        lastPiece.set(v, value);
    }
    static getFieldFromDataDefinitionByProperty(fields, path, propertyName) {
        let fls = fields;
        if (fls.length === 0)
            return null;
        let field = fls === null || fls === void 0 ? void 0 : fls.find((f) => Reflect.get(f, propertyName) === path);
        if (field)
            return field;
        if (path.includes(".")) {
            let pieces = path.split(".");
            let [parent] = pieces.splice(0, 1);
            let parentField = fls.find((f) => Reflect.get(f, propertyName) === parent);
            if (parentField) {
                if (pieces.length === 0) {
                    return parentField;
                }
                const subFields = Reflect.get(parentField, "fields");
                if (subFields && subFields.length) {
                    return this.getFieldFromDataDefinitionByProperty(subFields, pieces.join("."), propertyName);
                }
            }
        }
        throw new Error(`Can not find the field by path: ${path}`);
    }
    static filterFieldsByProperty(fields, propertyName, searchStr) {
        let result = [];
        fields.forEach((field) => {
            const subfields = Reflect.get(field, "fields");
            let isAdded = false;
            if (subfields && subfields.length > 0) {
                const searchedSubFields = this.filterFieldsByProperty(subfields, propertyName, searchStr);
                if (searchedSubFields.length > 0) {
                    isAdded = true;
                    result.push(Object.assign(Object.assign({}, field), { fields: searchedSubFields }));
                }
            }
            if (!isAdded && (Reflect.get(field, propertyName) || "").includes(searchStr)) {
                result.push(field);
            }
        });
        return result;
    }
    static createDataByType(type, name) {
        let def = null;
        switch (type) {
            case XDataType.BOOLEAN:
                def = new BooleanDataDefinition(name);
                break;
            case XDataType.STRING:
                def = new StringDataDefinition(name);
                break;
            case XDataType.NUMBER:
                def = new NumberDataDefinition(name);
                break;
            case XDataType.DATE:
                def = new DateDataDefinition(name);
                break;
            case XDataType.OBJECT:
                def = new ObjectDataDefinition(name);
                break;
            case XDataType.FILE:
                def = new FileDataDefinition(name);
                break;
        }
        if (!def) {
            def = new UndefinedDataDefinition(name);
        }
        return def;
    }
    static getAllSubFieldsByType(data, type) {
        const fields = data.fields.filter((f) => f.type === type);
        return fields;
    }
    static getAllDeepSubFieldsByType(data, type, basepath) {
        let result = [];
        let name = Reflect.get(data, "name");
        let currentpath;
        if (name && basepath) {
            currentpath = `${basepath}.${name}`;
        }
        else if (name) {
            currentpath = name;
        }
        let subfields = this.getAllSubFieldsByType(data, type);
        subfields.forEach((s) => {
            result.push({
                path: currentpath,
                def: s,
            });
        });
        let childrenObjFs = this.getAllSubFieldsByType(data, XDataType.OBJECT);
        childrenObjFs.forEach((c) => {
            result = [...result, ...this.getAllDeepSubFieldsByType(c, type, currentpath)];
        });
        return result;
    }
    static isIDataDefinition(def) {
        return Object.keys(def).indexOf("type") !== -1;
    }
    // specific property values should be unique. If other case, it's overwrited by later elem.
    static convertArrayObjectToObjectByProperty(arr, property) {
        let result = arr.reduce((r, cur) => {
            r[Reflect.get(cur, property)] = cur;
            return r;
        }, {});
        return result;
    }
    static compareStructureToStructure(required, dataToCompare) {
        if ((required.list || dataToCompare.list) && required.list !== dataToCompare.list) {
            return false;
        }
        let dataToCompareFieldsObj = this.convertArrayObjectToObjectByProperty(dataToCompare.fields, "name");
        return !required.fields.find((f1) => {
            if (!f1.name)
                return false;
            const f2 = Reflect.get(dataToCompareFieldsObj, f1.name);
            if (!f2)
                return true;
            if (f1.type !== f2.type)
                return true;
            if (f1.type === XDataType.OBJECT) {
                return !this.compareStructureToStructure(f1, f2);
            }
            return false;
        });
    }
    static compareStructureToPath(required, dataToCompare, path) {
        if (path === "")
            return false;
        let field = this.getFieldFromDataDefinitionByProperty(dataToCompare.fields, path, "name");
        let subfields = (Reflect.get(field, "fields") || []);
        if (subfields.length < required.fields.length)
            return false;
        return this.compareStructureToStructure(required, field);
    }
    static compareValueToDataDefinition(value, dataDefinition) {
        if (value === undefined)
            return new ValidationIssue("Undefined Value");
        return dataDefinition.verifyValueType(value);
    }
    // todo
    // 1. might need the confirmation flag for data. ie: reset, remain.
    // 2. reset for list field
    static changeDataDefinitionTypeByPath(data, totype, path) {
        let names = path === "" ? [] : path.split(".");
        let olddef = this.getFieldFromDataDefinitionByProperty(data.fields, path, "name");
        if (olddef.type === totype)
            return;
        let newdef = this.createDataByType(totype, olddef.name);
        if (names.length > 1) {
            names = names.slice(0, -1);
            let parentdef = this.getFieldFromDataDefinitionByProperty(data.fields, names.join("."), "name");
            if (parentdef) {
                parentdef.fields = parentdef.fields.map((f) => {
                    if (f.name === olddef.name)
                        return newdef;
                    return f;
                });
            }
        }
        else if (names.length > 0) {
            data.fields = data.fields.map((f) => {
                if (f.name === olddef.name)
                    return newdef;
                return f;
            });
        }
        // reset the original value
        try {
            this.set(data.getValueObject(), path, undefined);
        }
        catch (_a) { }
    }
    static addDataDefinitionByPath(data, totype, path, name) {
        if (name && this.validateDataDefinitionNameByPath(data, `${path === "" ? "" : `${path}.`}${name}`, name)) {
            throw new Error(`Can not create a field with name: ${name}`);
        }
        let newdef = this.createDataByType(totype, name);
        let names = path === "" ? [] : path.split(".");
        let parentdef = null;
        if (names.length > 0) {
            parentdef = this.getFieldFromDataDefinitionByProperty(data.fields, path, "name");
        }
        else {
            parentdef = data;
        }
        parentdef.fields.push(newdef);
    }
    static validateDataDefinitionNameByPath(data, path, name, availableCount = 0) {
        if (name === "") {
            return "field name can not be empty.";
        }
        if (!/^[a-zA-Z][a-zA-Z0-9_]*$/.test(name)) {
            return "field name is not valid.";
        }
        let parentdef = null;
        if (path.includes(".")) {
            let names = path.split(".");
            names.splice(-1, 1);
            parentdef = this.getFieldFromDataDefinitionByProperty(data.fields, names.join("."), "name");
        }
        else {
            parentdef = data;
        }
        if ((parentdef.fields || []).filter((f) => f.name === name).length > availableCount) {
            return "field name should be unique at same level.";
        }
        return null;
    }
    static changeDataDefinitionNameByPath(data, newname, path) {
        if (this.validateDataDefinitionNameByPath(data, path, newname)) {
            throw new Error(`Can not change the field with name: ${newname}`);
        }
        let field = this.getFieldFromDataDefinitionByProperty(data.fields, path, "name");
        if (!field)
            throw new Error(`Can not find the field with path: ${path}`);
        if (field.name === newname) {
            return;
        }
        let oldname = field.name;
        field.name = newname;
        let val = this.get(data.getValueObject(), path);
        let idx = path.lastIndexOf(oldname);
        let newpath = path.slice(0, idx) + path.slice(idx).replace(oldname, newname);
        this.set(data.getValueObject(), newpath, val, false);
        this.set(data.getValueObject(), path, undefined, false);
    }
    static changeFieldPositionByPath(data, newindex, path) {
        let names = path === "" ? [] : path.split(".");
        let def = this.getFieldFromDataDefinitionByProperty(data.fields, path, "name");
        let parentdef = null;
        if (names.length > 1) {
            names = names.slice(0, -1);
            parentdef = this.getFieldFromDataDefinitionByProperty(data.fields, names.join("."), "name");
        }
        else {
            parentdef = data;
        }
        if (parentdef) {
            let oldindex = parentdef.fields.map((f) => f.name).indexOf(def.name);
            if (oldindex !== newindex) {
                parentdef.fields.splice(oldindex, 1);
                parentdef.fields.splice(newindex, 0, def);
            }
        }
    }
    static removeFieldByPath(data, path) {
        let names = path === "" ? [] : path.split(".");
        let def = this.getFieldFromDataDefinitionByProperty(data.fields, path, "name");
        let parentdef = null;
        if (names.length > 1) {
            names = names.slice(0, -1);
            parentdef = this.getFieldFromDataDefinitionByProperty(data.fields, names.join("."), "name");
        }
        else {
            parentdef = data;
        }
        if (parentdef) {
            let oldindex = parentdef.fields.map((f) => f.name).indexOf(def.name);
            parentdef.fields.splice(oldindex, 1);
        }
    }
    static getFieldsLevel(data, path, extraField) {
        let def = this.getFieldFromDataDefinitionByProperty(data.fields, path, "name");
        if (def.type !== XDataType.OBJECT) {
            return 1;
        }
        return def.fields.reduce((s, f) => {
            s += this.getFieldsLevel(data, `${path}.${f.name}`, extraField);
            return s;
        }, extraField || 0);
    }
    static checkType(value, dataType) {
        if (dataType.list && !Array.isArray(value)) {
            return false;
        }
        switch (dataType.type) {
            case XDataType.NUMBER:
            case XDataType.STRING:
            case XDataType.OBJECT:
            case XDataType.BOOLEAN:
                if (dataType.list && Array.isArray(value)) {
                    return !value.find((v) => typeof v !== dataType.type);
                }
                return typeof value === dataType.type;
            case XDataType.DATE:
                const isDate = (v) => {
                    if (typeof v === "string") {
                        let d = new Date(v);
                        return !isNaN(d.getTime());
                    }
                    else if (v instanceof Date) {
                        return !isNaN(v.getTime());
                    }
                    return false;
                };
                if (dataType.list && Array.isArray(value)) {
                    return !value.find((v) => !isDate(v));
                }
                return isDate(value);
            case XDataType.FILE:
                const isFile = (v) => {
                    if (typeof v === "object") {
                        return (v.file &&
                            v.file instanceof File &&
                            (typeof v.data === "string" || v.data instanceof ArrayBuffer || v.data === null));
                    }
                    return false;
                };
                if (dataType.list && Array.isArray(value)) {
                    if (dataType.list && Array.isArray(value)) {
                        return !value.find((v) => !isFile(v));
                    }
                }
                return isFile(value);
        }
        return true;
    }
    static defRefSchema(schema) {
        return new Promise((resolve, reject) => {
            $RefParser.dereference(schema, (err, schemaRef) => {
                if (err) {
                    throw new Error(`Unable to De-Reference schema!, ${err}`);
                }
                else {
                    resolve(JSON.stringify(schemaRef));
                }
            });
        });
    }
    static getObjectDisplayText(o) {
        return JSON.stringify(o);
    }
    static summerizeText(text, maxLen) {
        if (text.length < maxLen)
            return text;
        else {
            return `${text.slice(0, 6)}...${text.slice(-3)}`;
        }
    }
}
DataUtilities.reactProperties = Object.getOwnPropertyNames(new React.Component({}));
//# sourceMappingURL=DataUtilities.js.map