/*!
 * 
 * 4DView Pro library 0.0.0
 * 
 * Copyright(c) 4D SAS.  All rights reserved.
 * 
 * 4D (the "Software") and the corresponding source code remain
 * the exclusive property of 4D and/or its licensors and are protected by national
 * and/or international legislations.
 * 
 * This file is part of the source code of the Software provided under the relevant
 * 4D License Agreement available on http://www.4D.com/license whose compliance
 * constitutes a prerequisite to any use of this file and more generally of the
 * Software and the corresponding source code.
 * 
 */

/**
 * Globals
 * 
 * /!\ Try not to add new globals unless really necessary
 * If using `Utils` is enough to do what you need to do
 * prefer using it over directly interacting with the global.
 * This leaves room for future evolution.
 * 
 * Examples:
 * 
 * - `Utils.getCommand(commandName)` -> get a command from `Commands`
 * - `Utils.addCommand(commandName, commandHandler)` -> add a new command to `Commands`
 * - `Utils.currentDocument` -> a reference to the current document object
 * - `Utils.spread` -> a reference to the `spread` global object
 * 
 * See `utils.js` for more.
 */

/**
 * `var` used instead of `let` so the 4D code inside of the View Pro component
 * continues to work. When `let` is used the variable `spread` won't be
 * accessible via `window.spread`.
 * Try not to use `var`. 
 */
var spread;
var useRibbon = false;
var useToolbar = false;
var designerReady = false;

let currentDocument = {};
let Commands = {};

function hideSpreadJSElements() {
    try {
        $('li [href="#file"]').hide();
    } catch (e) { }
}

function adaptSpreadUI(options) {
    hideSpreadJSElements();

    /**
     * sync the "withFormulaBar" View Pro option
     * with the Spread options UI
     */
    if (options.withFormulaBar === false) {
        $("#formulaBar")["hide"]();
        $('label[data-name="showFormulaBar"]')[0].children[0].checked = false;
        adjustSpreadSize();
    }
}

/**
 * Do it asap to avoid the user seing anything on the screen
 * but also need to do it later in case something is created asynchronously
 */
hideSpreadJSElements();

// Entry point:
document.addEventListener('DOMContentLoaded', function () {
    //Create a spreadsheet
    hideSpreadJSElements();
    $4d._vp_init(function (ret, err) {
        let obj = JSON.parse(ret);
        let options = obj.options || {};
        GC.Spread.Sheets.LicenseKey = obj.licenseKey;
        options.sheetName = 'Sheet1';
        if (options.userInterface === 'none') {
            obj.options.tabStripVisible = false;
            createZone(options);
        } else if (options.userInterface === 'toolbar') {
            (function checkSpread() {

                if (!spread) {
                    setTimeout(checkSpread, 0);
                } else {
                    
                    useToolbar = true;

                    adaptSpreadUI(options);
                    obj.options.tabStripVisible = true;
                    initSpread({ obj, setOptions: true });
                }
            })();
        } else if (options.userInterface === 'ribbon') {
            (function checkDesigner() {

                if (!designerReady) {
                    setTimeout(checkDesigner, 0);
                } else {

                    useRibbon = true;

                    options.withFormulaBar = true;
                    adaptSpreadUI(options);
                    obj.options.tabStripVisible = true;
                    initSpread({ obj, setOptions: true });
                }
            })();
        }

        function createZone(options) {
            if (options.withFormulaBar) {
                let formulaParent = document.getElementById("formulaBar");
                formulaParent.classList.remove('hidden');
                if (options.userInterface === 'none') {
                    document.getElementById("ss").classList.add("withFormulaBar");
                }
            }
            else {
                if (options.userInterface === 'none') {
                    document.getElementById("ss").classList.add("withoutFormulaBar");
                }
            }

            spread = new GC.Spread.Sheets.Workbook(document.getElementById("ss"), { sheetCount: 1 });

            if (options.withFormulaBar) {
                let formulaElement = document.getElementById("formulaTextBox");
                let fbx = new GC.Spread.Sheets.FormulaTextBox.FormulaTextBox(formulaElement);
                fbx.workbook(spread);
            }
            initSpread({ obj });
        }
    });

    function initSpread({ obj, setOptions = true }) {
        /*********
         * Culture
         *********/
        // in japanese, use builtin japanese culture
        if (vp_localizedFolder === 'ja.lproj') {
            GC.Spread.Common.CultureManager.culture("ja-jp");
        } else {
            // get a new culture based on 4D intl manager
            let culture = getCulture(obj);
            //add one culture
            GC.Spread.Common.CultureManager.addCultureInfo("4d-4D", culture);
            //switch to "4d-4D" culture
            GC.Spread.Common.CultureManager.culture("4d-4D");
        }
        /*********
         * Options
         *********/
        // store regular menuhandler
        Utils.contextMenu = Utils.spread.contextMenu;

        //Freeze the redraw
        Utils.spread.suspendPaint();

        if (setOptions === true) {
            setWorkbookOptions(obj.options);
        }

        //Draw now
        Utils.spread.resumePaint();

        init4DCommands();
        initInternalComponentCommands();
        init4DMethods(obj.methods);
        initVirtualStructure(obj.virtualStructure);

        Utils.send4DEvent("onLoad");
    }

    // --- utility function to format result given by 4D expression
    function outputResult(context, ret) {
        if (ret.type == "string") {
            context.setAsyncResult(ret.string);
        } else if (ret.type == "number") {
            context.setAsyncResult(ret.number);
        } else if (ret.type == "boolean") {
            context.setAsyncResult(ret.boolean);
        } else if (ret.type == "date") {
            let date = new Date(ret.date.year, ret.date.month - 1, ret.date.day);
            context.setAsyncResult(date);
        } else if (ret.type == "time") {
            // 30 dec 1899 is day 0 for SpreadJS
            let date = new Date(1899, 11, 30, ret.time.hours, ret.time.minutes, ret.time.seconds);
            context.setAsyncResult(date);
        } else if (ret.type == "picture") {
            Utils.currentSheet.setCellType(context.row, context.col, new D4Picture(ret.picture));
            context.setAsyncResult("_pict_");
        } else if (ret.type == "none") {
            context.setAsyncResult("");
        } else if (ret.type == "error") {
            context.setAsyncResult(Utils.errors.wrongTypeOfArgument);
        } else if (ret.type == "unsupported") {
            context.setAsyncResult(Utils.errors.wrongTypeOfArgument);
        }
    }

    function adjustParameters(parameters) {
        let ret = [];

        parameters.forEach(function (parameter) {
            if (parameter.constructor === Date) {

                let day = parameter.getDate();
                let month = parameter.getMonth();
                let year = parameter.getFullYear();

                let hours = parameter.getHours();
                let minutes = parameter.getMinutes();
                let seconds = parameter.getSeconds();

                let val = {
                    "value": { "day": day, "month": month + 1, "year": year },
                    "time": (hours * 3600) + (minutes * 60) + seconds
                };

                ret.push(val);
            }
            else
                ret.push(parameter);
        })
        return ret;
    }

    // --- add some custom commands to emulate 4D specific commands

    function init4DCommands() {
        let arr = [
            { 'name': 'RUNTIME_STRING', 'number': 10 },
            { 'name': 'RUNTIME_DATE', 'number': 102 },
            { 'name': 'RUNTIME_TIME', 'number': 179 },
            { 'name': 'RUNTIME_CURRENT_TIME', 'number': 178 }];

        arr.forEach(function (command) {

            let myFunc = function () {
                this.name = command.name;
            }
            myFunc.prototype = new CustomAsyncFunction();
            myFunc.prototype.evaluateAsync = function (...args) {

                vp_startOptimizer();

                let context = args[0];
                args[0] = command.number;
                args = adjustParameters(args);
                try {
                    $4d._vp_call4dCommand(...args, function (ret, err) {
                        try {
                            if (err === null)
                                outputResult(context, ret);
                            else
                                context.setAsyncResult(Utils.errors.wrongTypeOfArgument);
                        } finally {                        }
                    });
                } catch (e) {
                    Utils.logEvent({ type: 'error-catched', data: e });
                }
            };
            let instance = new myFunc();
            Utils.defineGlobalCustomFunction(command.name, instance, '4D-commands');
        });
    }

    // --- 4D project methods ------------------------------------------------------------
    function initInternalComponentCommands() {
        let arr = [
            { 'name': 'RUNTIME_VIEW_STRING', 'method': 'vp_4DViewString' }
        ];

        arr.forEach(function (method) {

            let myFunc = function () {
                this.name = method.name;
            }
            myFunc.prototype = new CustomAsyncFunction();
            myFunc.prototype.evaluateAsync = function (...args) {
 
                vp_startOptimizer();

                let context = args[0];
 
                args[0] = method.method;
                args = adjustParameters(args);
                try {
                    $4d._vp_callComponentMethod(...args, function (ret, err) {
                        try {
                            if (err === null)
                                outputResult(context, ret);
                            else
                                context.setAsyncResult(Utils.errors.wrongTypeOfArgument);
                        } finally {}
                    });
                } catch (e) {
                    Utils.logEvent({ type: 'error-catched', data: e });
                }
            };
            let instance = new myFunc();
            Utils.defineGlobalCustomFunction(method.name, instance, '4D-method');
        });
    }

    // --- 4D project methods ------------------------------------------------------------
    function init4DMethods(arr) {
        arr.forEach(function (method) {
            let myFunc = function () {
                this.name = method.spreadJSMethod;
                this.descr = method.summary;

                if ('minParams' in method) {
                    this.minArgs = method.minParams;
                }
    
                if ('maxParams' in method) {
                    this.maxArgs = method.maxParams;
                }
    
                if('parameters' in method){
                    this.parameters = method.parameters;
                }
            }

            myFunc.prototype = new CustomAsyncFunction();
            myFunc.prototype.evaluateAsync = function (...args) {

                vp_startOptimizer();

                let context = args[0];

                // check if parameters are respecting users type rules

                let i;

                for (i = 1; i < args.length; i++) {

                    let arg = args[i];
                    let ok = false;

                    let wantedType = -1;

                    if ('parametersType' in method) {
                        if ((i - 1) < method.parametersType.length) {
                            wantedType = method.parametersType[i - 1];
                        }
                    }

                    switch (wantedType) {
                        case 1: // real
                        case 8: // integer
                        case 9: // longint
                            ok = typeof (arg) === 'number';
                            break;

                        case 2: // text
                            ok = typeof (arg) === 'string';
                            break;

                        case 6: // boolean
                            ok = typeof (arg) === 'boolean';
                            break;

                        case 4: // date
                            ok = arg.constructor === Date;
                            if (ok) {
                                args[i] = { "value": { "day": arg.getDate(), "month": arg.getMonth() + 1, "year": arg.getFullYear() } };
                            }
                            break;

                        case 11: //time
                            ok = arg.constructor === Date;
                            if (ok) {
                                args[i] = { "time": (arg.getHours() * 3600) + (arg.getMinutes() * 60) + arg.getSeconds() }
                            }
                            break;

                        case 38: // object
                            if (arg.constructor === Date) {
                                args[i] = {
                                    "value": { "day": arg.getDate(), "month": arg.getMonth() + 1, "year": arg.getFullYear() },
                                    "time": (arg.getHours() * 3600) + (arg.getMinutes() * 60) + arg.getSeconds()
                                };
                            } else {
                                args[i] = { "value": arg };
                            }
                            ok = true;
                            break;

                        case -1: // standard mode, convert only dates
                            if (arg.constructor === Date) {
                                args[i] = {
                                    "value": { "day": arg.getDate(), "month": arg.getMonth() + 1, "year": arg.getFullYear() },
                                    "time": (arg.getHours() * 3600) + (arg.getMinutes() * 60) + arg.getSeconds()
                                };
                            }
                            ok=true;
                            break;
                    }
                    if (!ok) {
                        context.setAsyncResult(Utils.errors.wrongTypeOfArgument);
                        return;
                    }
                }

                args[0] = method.method;

                try {
                    $4d._vp_call4dMethod(...args, function (ret, err) {
                        try {
                            if (err === null)
                                outputResult(context, ret);
                            else
                                context.setAsyncResult(Utils.errors.wrongTypeOfArgument);
                        } finally {}
                    });
                } catch (e) {
                    Utils.logEvent({ type: 'error-catched', data: e });
                }
            };
            let instance = new myFunc();
            Utils.defineGlobalCustomFunction(method.spreadJSMethod, instance, '4D-method');
        });
    }

    // --- virtual structure ------------------------------------------------------------
    function initVirtualStructure(arr) {
        arr.forEach(function (obj) {
            let myFunc = function () {
                this.name = obj.name;
                this.minArgs = 0;
                this.maxArgs = 0;
            }
            myFunc.prototype = new CustomAsyncFunction();
            myFunc.prototype.evaluateAsync = function (...args) {

                vp_startOptimizer();

                let context = args[0];
                try {
                    $4d._vp_getTableField(obj.ref, function (ret, err) {
                        try {
                            outputResult(context, ret);
                        } finally {}
                    });
                } catch (e) {
                    Utils.logEvent({ type: 'error-catched', data: e });
                }
            };
            let instance = new myFunc();
            Utils.defineGlobalCustomFunction(obj.name, instance, '4D-virtual-structure');
        });
    }

    function getCulture(obj) {
        let culture = new GC.Spread.Common.CultureInfo();

        culture.NumberFormat.currencySymbol = obj.intl.currency;
        culture.NumberFormat.currencyDecimalSeparator = obj.intl.decimalSeparator;
        culture.NumberFormat.currencyGroupSeparator = obj.intl.thousandSeparator;
        culture.NumberFormat.numberDecimalSeparator = obj.intl.decimalSeparator;
        culture.NumberFormat.numberGroupSeparator = obj.intl.thousandSeparator;

        culture.NumberFormat.arrayListSeparator = "\\";

        // not this information in 4D intl manager, 
        // so a little hack to be able to not collide decimal separator
        if (obj.intl.decimalSeparator == ",") {
            culture.NumberFormat.arrayGroupSeparator = ";";
            culture.NumberFormat.listSeparator = ";";
        } else {
            culture.NumberFormat.arrayGroupSeparator = ",";
            culture.NumberFormat.listSeparator = ",";
        }

        if (obj.intl.amString !== '') {
            culture.DateTimeFormat.amDesignator = obj.intl.amString;
            culture.DateTimeFormat.pmDesignator = obj.intl.pmString;
        }

        culture.DateTimeFormat.abbreviatedMonthNames = obj.intl.abbreviatedMonthNames;
        culture.DateTimeFormat.abbreviatedDayNames = obj.intl.abbreviatedDayNames;

        culture.DateTimeFormat.dayNames = obj.intl.dayNames;
        culture.DateTimeFormat.monthNames = obj.intl.monthNames;

        // no "genitive" names in 4D intl manager, so use names
        culture.DateTimeFormat.abbreviatedMonthGenitiveNames = obj.intl.abbreviatedMonthNames;
        culture.DateTimeFormat.monthGenitiveNames = obj.intl.monthNames;
        culture.DateTimeFormat.fullDateTimePattern = obj.intl.longDatePattern + ' ' + obj.intl.longTimePattern; //"dddd, d. MMMM yyyy HH:mm:ss";
        culture.DateTimeFormat.longDatePattern = obj.intl.longDatePattern; // "dddd, d. MMMM yyyy";
        culture.DateTimeFormat.longTimePattern = obj.intl.longTimePattern; // "HH:mm:ss";

        culture.DateTimeFormat.monthDayPattern = obj.intl.monthDayPattern; // dd MMMM or MMMM dd

        culture.DateTimeFormat.shortDatePattern = obj.intl.shortDatePattern; // "dd.MM.yyyy";
        culture.DateTimeFormat.shortTimePattern = obj.intl.shortTimePattern; // "HH:mm";
        culture.DateTimeFormat.yearMonthPattern = obj.intl.yearMonthPattern; // MMMM yyyy
        return culture;
    }

    function setWorkbookOptions(options) {
        //Only one sheet
        // Utils.spread.setSheetCount(1);
        //Hide the tabs strip
        Utils.spread.options.tabStripVisible = options.tabStripVisible;
        //Get the active sheet
        let sheet = Utils.currentSheet;
        //Set the number of rows and columns like Excel default values.
        sheet.setRowCount(options.numberOfRows, GC.Spread.Sheets.SheetArea.viewport);
        sheet.setColumnCount(options.numberOfColumns, GC.Spread.Sheets.SheetArea.viewport);
        //Set row headers width to allow 1048576 characters
        sheet.setColumnWidth(0, 60.0, GC.Spread.Sheets.SheetArea.rowHeader);
        //Allow users to enter mathematical formula.
        Utils.spread.options.allowUserEditFormula = true;
        Utils.spread.options.enableFormulaTextbox = true;

        if (useRibbon)
            designer.actions.isFileModified = false;
    }
});

window.onerror = function (message, source, lineno, colno, error) {
    Utils.logEvent({ type: 'error', data: { message, source, lineno, colno, error } });
};