"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const prettier_1 = __importDefault(require("prettier"));
const json_merge_patch_1 = __importDefault(require("json-merge-patch"));
const properties_reader_1 = __importDefault(require("properties-reader"));
const basePathGenerated = path_1.default.join(process.cwd(), "./@types", "sfcc");
const sfccApi = json_merge_patch_1.default.apply(JSON.parse(fs_1.default.readFileSync("./api/sfcc-api.json", "utf8")), JSON.parse(fs_1.default.readFileSync("./api/patches.json", "utf8")));
var genericsremap = properties_reader_1.default('./api/generics.properties');
const config = {
    typesMapping: {
        Number: "number",
        String: "string",
        Boolean: "boolean",
        Object: "any",
        arguments: "IArguments",
        Array: "Array",
        Module: "NodeModule"
    },
    generics: [
        "dw.util.Collection",
        "dw.util.List",
        "dw.util.ArrayList",
        "dw.util.FilteringCollection",
        "dw.util.SeekableIterator",
        "dw.web.LoopIterator",
        "dw.util.Iterator",
        "dw.util.LinkedHashSet",
        "dw.util.Set",
        "dw.util.SortedSet",
        "dw.web.PagingModel",
        "Array",
        "TopLevel.Array",
        "dw.object.Extensible",
        "dw.object.ExtensibleObject",
        "dw.object.SimpleExtensible",
        "dw.svc.Result"
    ],
    maps: [
        "dw.util.Map",
        "dw.util.HashMap",
        "dw.util.LinkedHashMap",
        "dw.util.MapEntry",
        "dw.util.SortedMap"
    ],
    extensible: [
        "dw.object.Extensible",
        "dw.object.ExtensibleObject",
        "dw.object.SimpleExtensible",
        "dw.object.CustomObject"
    ],
    argsMapping: {
        function: "fn",
    },
    exclusions: {
        classes: ["Generator"],
        global: {
            constants: ["undefined"],
            properties: [],
            methods: [],
        },
    },
};
const checkIsCollection = (fullclassname) => {
    return config.generics.includes(fullclassname);
};
const checkIsMap = (fullclassname) => {
    return config.maps.includes(fullclassname);
};
const sanitizeType = (type, generics, isGeneric) => {
    var sanitizedType = (sfccApi.mapping[type] || type).replace("TopLevel.", "");
    let mapped = config.typesMapping[sanitizedType] || sanitizedType;
    if (generics) {
        return `${mapped}<${generics}>`;
    }
    let result = isGeneric && mapped === 'any' ? 'T' : mapped;
    if (isGeneric && checkIsCollection(result)) {
        return `${result}<T>`;
    }
    if (checkIsCollection(result)) {
        return `${result}<any>`;
    }
    if (checkIsMap(result)) {
        return `${result}<any, any>`;
    }
    return result;
};
const sanitizeArg = (arg) => config.argsMapping[arg] || arg;
const sanitizeValue = (obj) => {
    switch (obj.class.name) {
        case "number":
        case "Number":
            try {
                return obj.value.match(/[0-9.]+/)[0];
            }
            catch (e) {
                console.log(obj.name);
            }
            break;
        default:
            return obj.value;
    }
};
const standardDefinition = (element) => {
    // if (element === 'undefined') {
    //   return true;
    // }
    // return false;
    if (element === 'Iterator' || element === 'Iterable') {
        return true;
    }
    // if (element === 'module') {
    //   return false; // we need this anyway
    // }
    try {
        eval(element);
        return true;
    }
    catch (e) {
        return false;
    }
};
const doc = (obj) => {
    if (!obj.description) {
        return '';
    }
    let description = obj.description;
    if (obj.args) {
        description += '\n' + obj.args.map((param) => `@param ${param.name} ${param.description || ''}`).join("\n");
    }
    if (obj.class && obj.class.description) {
        description += `\n@return ${obj.class.description}`;
    }
    return `/**\n${description
        .split("\n")
        .map((line) => ` * ${line.replace('*/', '*\\/')}`)
        .join("\n")}\n*/\n`;
};
const formatArgument = (arg, isGeneric) => `${arg.multiple ? "..." : ""}${sanitizeArg(arg.name)}: ${sanitizeType(arg.class.name, arg.class.generics, isGeneric)}${arg.multiple ? "[]" : ""}`;
function filterComponent(key, prop) {
    return (element) => (key === "global" && !standardDefinition(element.name)) ||
        (key !== "global" &&
            !(config.exclusions[key] &&
                config.exclusions[key][prop] &&
                config.exclusions[key][prop].includes(element.name)));
}
;
const filterConstants = (key) => filterComponent(key, "constants");
const filterProperties = (key) => filterComponent(key, "properties");
const filterMethods = (key) => filterComponent(key, "methods");
const generateExportFileForClass = (theClass) => {
    var packageTokens = theClass.fullClassName.split(".");
    if (packageTokens.length && packageTokens[0] === "TopLevel") {
        packageTokens.shift();
    }
    var className = packageTokens.pop();
    if (theClass.fullClassName === 'TopLevel.Module') {
        theClass.fullClassName = 'TopLevel.NodeModule'; // hardcoded remapping to extend existing standard declaration
    }
    var foldersPath = path_1.default.join.apply(null, [].concat(basePathGenerated, packageTokens));
    fs_1.default.mkdirSync(foldersPath, {
        recursive: true,
    });
    var sourcePath = path_1.default.join(foldersPath, className + ".d.ts");
    fs_1.default.writeFileSync(sourcePath, `/// <reference path="${packageTokens.length == 0 ? "./" : packageTokens.map(() => "../").join("")}index.d.ts" />\nexport = ${theClass.fullClassName.replace("TopLevel.", "")};`);
};
const generateCodeForClass = (theClass, customAttrTypes) => {
    var source = "";
    var packageTokens = theClass.fullClassName.split(".");
    var isTopLevel = false;
    if (packageTokens.length && packageTokens[0] === "TopLevel") {
        isTopLevel = true;
        packageTokens.shift();
    }
    var className = packageTokens.pop();
    var isInterface = sfccApi.interfaces.includes(theClass.fullClassName);
    var isGlobal = className === "global";
    var isStatic = "static ";
    var readonly = "readonly ";
    if (isGlobal) {
        isStatic = "const ";
        readonly = "";
    }
    let isGeneric = checkIsCollection(theClass.fullClassName);
    let isMap = checkIsMap(theClass.fullClassName);
    if (!isGlobal) {
        if (theClass.description) {
            source += doc(theClass);
        }
        // hardcoded remapping to extend existing standard declaration
        source += `${isTopLevel ? 'declare ' : ''}class ${className === 'Module' ? 'NodeModule' : className}${isGeneric ? '<T>' : isMap ? '<K, V>' : ''} `;
        if (theClass.hierarchy.length > 1) {
            let hierarchyClass = theClass.hierarchy.pop().name;
            let generics = null;
            // if (theClass.hierarchy.find((h: any) => h.name === 'dw.object.ExtensibleObject')) {
            if (hierarchyClass === 'dw.object.ExtensibleObject' || hierarchyClass === 'dw.object.Extensible') {
                generics = className + 'CustomAttributes';
                customAttrTypes.add({ name: className });
            }
            else if (theClass.hierarchy.find((h) => h.name === 'dw.object.ExtensibleObject' || h.name === 'dw.object.Extensible')) {
                // extends an extensible class, eg. ProductLineItem -> LineItem -> ExtensibleObject
                customAttrTypes.add({ name: className, extends: hierarchyClass });
                if (!theClass.properties.custom) {
                    theClass.properties.custom = {
                        "name": "custom",
                        "class": {
                            "name": className + 'CustomAttributes'
                        },
                        "static": false,
                        "readonly": true,
                        "description": "The custom attributes for this object. The returned object is\n used for retrieving and storing attribute values. See\n CustomAttributes for a detailed example of the syntax for\n working with custom attributes.",
                        "deprecated": false
                    };
                    theClass.methods.getCustom = {
                        "name": "getCustom",
                        "args": [],
                        "class": {
                            "name": className + 'CustomAttributes'
                        },
                        "description": "Returns the custom attributes for this extensible object."
                    };
                }
            }
            source += `extends ${sanitizeType(hierarchyClass, generics, isGeneric)} `;
        }
        source += "{\n";
    }
    let constants = Object.values(theClass.constants);
    source += constants
        .filter(filterConstants(className))
        .sort((a, b) => a.name.localeCompare(b.name))
        .reduce((constantSource, constant) => `${constantSource}${doc(constant)}${isGlobal ? 'declare ' : ''}${isStatic}${readonly}${constant.name}${!constant.value ? ": " + sanitizeType(constant.class.name, constant.class.generics, isGeneric && !isStatic) : ""}${constant.value ? " = " + sanitizeValue(constant) : ""};\n`, "");
    source += "\n";
    let properties = Object.values(theClass.properties);
    properties = properties.filter(prop => !constants.find(co => co.name == prop.name)); // properties my duplicate constants
    source += properties
        .filter(filterProperties(className))
        .sort((a, b) => a.name.localeCompare(b.name))
        .reduce((propSource, property) => {
        let returnType = sanitizeType(property.class.name, property.class.generics, isGeneric && !property.static);
        if (!isGeneric) {
            returnType = checkGenerics(returnType, theClass, property);
        }
        if (!config.extensible.includes(theClass.fullClassName) && property.name === 'custom' && returnType === 'dw.object.CustomAttributes') {
            returnType = className + 'CustomAttributes';
            if (!Array.from(customAttrTypes).find(a => a.name === className)) {
                customAttrTypes.add({ name: className });
            }
        }
        return `${propSource}${doc(property)}${isGlobal ? 'declare ' : ''}${property.static ? isStatic : ""}${property.readonly ? readonly : ""}${property.name}: ${returnType};\n`;
    }, "");
    source += "\n";
    if (!isInterface) {
        let constructors = Object.values(theClass.constructors);
        source += constructors
            .reduce((constructorSource, constructor) => `${constructorSource}${doc(constructor)}constructor(${constructor.args.map((m) => formatArgument(m, isGeneric)).join(", ")});\n`, "");
        if (Object.keys(theClass.constructors).length === 0 && !isGlobal) {
            source += "private constructor();\n";
        }
        source += "\n";
    }
    let methods = Object.values(theClass.methods);
    source += methods
        .filter(filterMethods(className))
        .sort((a, b) => a.name.localeCompare(b.name))
        .reduce((methodSource, method) => {
        let returnType = sanitizeType(method.class.name, method.class.generics, isGeneric);
        if (!isGeneric) {
            returnType = checkGenerics(returnType, theClass, method);
        }
        if (!config.extensible.includes(theClass.fullClassName) && method.name === 'getCustom' && returnType === 'dw.object.CustomAttributes') {
            returnType = className + 'CustomAttributes';
            if (!Array.from(customAttrTypes).find(a => a.name === className)) {
                customAttrTypes.add({ name: className });
            }
        }
        return `${methodSource}${doc(method)}${isGlobal ? "declare function " : ""}${method.static && !isGlobal ? isStatic : ""}${method.name.replace('@@iterator', '[Symbol.iterator]')}(${method.args.map((m) => formatArgument(m, isGeneric)).join(", ")}): ${returnType};\n`;
    }, "");
    source += "\n";
    if (!isGlobal) {
        source += "}\n";
    }
    generateExportFileForClass(theClass);
    return source + "\n";
};
const generateCode = (pkg, customAttrTypes) => Object.keys(pkg).reduce((source, key) => {
    if (!config.exclusions.classes[key] && !(pkg[key].package === 'TopLevel' && standardDefinition(key))) {
        if (pkg[key].fullClassName && !config.exclusions.classes[key]) {
            source += generateCodeForClass(pkg[key], customAttrTypes) + "\n";
        }
        else {
            source += `
        namespace ${key} {\n`;
            source += generateCode(pkg[key], customAttrTypes);
            source += `}\n`;
        }
    }
    return source;
}, "");
let customAttrTypes = new Set();
var source = "";
//source += "declare global {\n";
source += generateCodeForClass(sfccApi.api.TopLevel.global, customAttrTypes);
delete sfccApi.api.TopLevel.global;
source += generateCode(sfccApi.api.TopLevel, customAttrTypes);
source += "declare namespace dw {\n";
source += generateCode(sfccApi.api.dw, customAttrTypes);
source += "}\n";
//source += "}\n";
let formatted = source;
try {
    formatted = prettier_1.default.format(source, { parser: "typescript" });
}
catch (e) {
    console.error(e);
}
fs_1.default.writeFileSync(path_1.default.join(basePathGenerated, "index.d.ts"), formatted);
let customattrsrc = Array.from(customAttrTypes).map(i => `
/**
 * Custom attributes for ${i.name} object.
 */
declare class ${i.name}CustomAttributes ${i.extends ? 'extends ' + i.extends.substring(i.extends.lastIndexOf('.') + 1, i.extends.length) + 'CustomAttributes' : ''}{
  /**
   * Returns the custom attribute with this name. Throws an exception if attribute is not defined
   */
  [name: string]: any;
}`).join('\n');
fs_1.default.writeFileSync(path_1.default.join(basePathGenerated, "attrs.d.ts"), prettier_1.default.format(customattrsrc, { parser: "typescript" }));
fs_1.default.writeFileSync(path_1.default.join(basePathGenerated, "attrs.txt"), Array.from(new Set(Array.from(customAttrTypes).map(cu => cu.name))).sort().join('\n'));
function checkGenerics(returnType, theClass, member) {
    if (returnType.indexOf('<any') > -1) {
        let propkey = member.type === 'property' ? `${theClass.fullClassName}.get${member.name.charAt(0).toUpperCase()}${member.name.substring(1)}` : `${theClass.fullClassName}.${member.name}`;
        // @ts-ignore
        returnType = genericsremap.get(propkey) || returnType;
        if (returnType.indexOf('<any') > -1) {
            console.log(`Unmapped generics: ${propkey}=${returnType}`);
        }
    }
    return returnType;
}
