Source: marginalCalc.js

"use strict";
/**
 * Innehåller defintionen för {@link marginalCalc}
 * @author Hanif Bali
 * @file
 * @version 0.3.5
 */

/**
 * Detta är hjärtat i kalkylatorn, här laddar du och kör dina uträkningar.
 * @namespace marginalCalc
 */
var marginalCalc = {
    loaded: false,
    /**
     * Kalkylatorn använder olika "moduler" för att räkna ut olika bidrag eller skatter. Skapa gärna egna moduler.
     * @interface  Module
     */

    /**
     * Moduler som vill påverka summan av {@link marginalCalc.totalCalc} måste ha denna metod,
     * den returnerar antingen ett positivt eller negativt tal, ett bidrag eller avdrag ger tex ett positivt tal,
     * medan en skatt genererar ett negativt tal.
     * @name Module.#totalCalc
     * @method
     * @returns {Number}
     */

    /**
     * Array med alla moduler registrerade av {@link marginalCalc.addModule}
     * @type {Module[]}
     */
    modules: [],
    modulePriority: [],
    moduleCache: [],

    /**
     * Registrerar modulen för att kunna bli kallad av kalkylatorn. Den känner
     * även av om modulen har en totalCalc funktion som kan köras senare för att
     * räkna ihop summan av samtliga moduler.
     *
     * @example var nyaModulen = function() {
     *              this.totalCalc = function() {
     *                  return -200;
     *              }
     *          }
     * {@link marginalCalc}.addModule("Nymodul", nyaModulen, 200)
     *             .addModule("Modul2",
     *                         function() {
     *                          this.totalCalc = function() {
     *                              return 500;
     *                          }
     *                         },201);
     *
     * marginalCalc.totalCalc() //-> 300
     * @param {String} moduleName name of the Module, referenced function parameters
     * @param {Module} module The module function
     * @param {Number} [priority = 100] Defines in what order the priority the module shall be calculated.
     * @returns {marginalCalc} Den returnerar sig själv för att kunna kedja anrop.
     */

    addModule: function (moduleName, module, priority) {

        this.modules[moduleName] = module;
        var tempObj = new module;

        if (tempObj.hasOwnProperty("totalCalc")) {
            this.modulePriority.push({'priority': priority ? priority : 100, 'moduleName': moduleName});
        }

        return this;
    },
    /**
     * Denna metod sparar den medskickade funktionen och väntar på att scriptloadern ska ge grönt ljus.
     * Denna metod hittar automatiskt vilka moduler funktionen vill
     * ha tillgång till genom att scanna alla argument till funktionen, argumenten ska heta samma sak som
     * man angivit som moduleName till {@link marginalCalc.addModule}.
     * Den initierar automatiskt de moduler som funktionen är beroende av. Den initerar även modulernas beroenden.
     * När den initerat klart anropar den först funktionen och därefter metoden ready() på funktionen om den är definierad.
     * Det kan vara så att man inte behöver lägga sin kod i ready() då alla moduler bör vara laddade vid körning.
     *
     * @example marginalCalc.run(function(socialBidrag){
     *      var soc = {@link socialBidrag}.totalCalc(); //-> ger socialbidraget;
     *      this.ready = function(){
     *          var readySoc = {@link socialBidrag}.totalCalc()
     *          console.log(readySoc === soc); //-> true
     *      }
     * });
     *
     * @param {function} fn Funktionen som ska köras
     */
    run: function (func) {
        this.modules = [];
        this.modules["$Main"] = func;
        this.waitForLoad();
    },
    /**
     * Waits for nod from scriptloader.
     * @private
     */
    waitForLoad: function () {
        var that = this;
        if (this.scriptLoader.complete === true) {

            this.runMain(this.modules["$Main"]);
            return;
        }
        console.log("skipping");
        this.timeout = setTimeout(function () {
            that.waitForLoad()
        }, 1000);
    },
    /**
     * Runs the main function, may be used to skip the nod from {Scriptloader}.
     * @private
     * @param fn
     * */
    runMain: function (fn) {
        this.execModule("$Main", fn);

        if (this.moduleCache["$Main"].hasOwnProperty("ready")) {
            this.moduleCache["$Main"].ready();
        }

    },
    /**
     * Initiates the function, injects the arguments defined fromm the list of loaded modules
     * it also saves the initiated function.
     * @param moduleName Name of the module its loading
     * @param func function body of the module
     * @private
     * @returns {Object} returns the Module
     */
    execModule: function (moduleName, func) {

        if (typeof this.moduleCache[moduleName] !== "undefined") {
            return this.moduleCache[moduleName];
        }

        var obj = new func;
        var dependencies = this.resolveDependencies(func);
        func.apply(obj, dependencies);
        return this.moduleCache[moduleName] = obj;

    },
    /**
     * Finds and loads the dependecies of the function
     * @param func Function to find the dependecies of
     * @private
     * @returns {Array} of modules
     */
    resolveDependencies: function (func) {
        var args = this.getArguments(func);
        var dependencies = [];
        for (var i = 0; i < args.length; i++) {
            if (args[i].length) {
                if (typeof this.modules[args[i]] === "undefined") {
                    throw new Error("Module (" + args[i] + ") is not registered, requested by module");
                }
                dependencies.push(this.execModule(args[i], this.modules[args[i]]));
            }

        }
        return dependencies;
    },
    /**
     * Extracts the arguments from a function
     * @param func
     * @private
     * @returns {Array} of arguments
     */
    getArguments: function (func) {
        //This regex is from require.js
        var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m;
        var args = func.toString().match(FN_ARGS)[1].split(',');
        args = args.map(function (arg) {
            return arg.trim();
        });
        return args;
    },
    /**
     * Kör metoden {@link Module.totalCalc} på varenda registrerad modul som har den metoden, den kör metoden
     * i den prioritet som man definierade när man registrerade modulen.
     * Antingen får man tillbaka en array med resultatet per modul, eller totalsumman.
     * @param {Boolean} [returnArr = false] False returnerar summan, true returnerar en array med separerade resultat per modul.
     *
     * @returns {Number|Array} Total sum of modules.
     */
    totalCalc: function (returnArr) {

        if (returnArr) {
            return this.$totalCalc();
        } else {
            return this.$totalCalc()["totalCalc"];
        }


    },
    /**
     * Returnerar resultatet av samtliga modulers totalCalc() i en array, använd totalCalc(true) istället.
     * @returns {Array}
     * @private
     */
    $totalCalc: function () {
        var totalCalc = [];

        var totalCalcModules = this.modulePriority.sort(function (a, b) {
            return a.priority - b.priority
        });

        var totalSum = 0;
        for (var i = 0; i < totalCalcModules.length; i++) {
            var moduleName = totalCalcModules[i].moduleName;
            var module = this.execModule(moduleName, this.modules[moduleName]);
            totalSum += totalCalc[moduleName] = module.totalCalc(totalSum);
        }
        totalCalc["totalCalc"] = totalSum;
        return totalCalc;

    },
    /**
     * Räknar ut kvoten av förändringen.
     * @example // Räkna ut marginaleffekter på hyreshöjning med 100 kr från 0 - 7000.
     * household.getHouse().setRent(0); // sätt hyran på noll
     *
     * var hyreshojning = 100;
     * var oldval = marginalCalc.totalCalc(); // ta reda på hur mkt man får från början i bidrag.
     *
     * // Börja mäta förändringar
     *      for(var i = 1; i < 7000 / increment; i++){
     *
     *          household.getHouse().setRent(i * hyreshojning); //100 kr -> 7000 kr
     *          var newVal = marginalCalc.totalCalc(); // få resultat av ex bostadsbidrag och socialbidrag
     *          console.log(marginalCalc.marginalEff(hyreshojning, newVal, oldVal)) // få marginaleffekten
     *          oldVal = newVal // spara den nuvarande summan för att jämföra med loop.
     *       }
     * @param increment Ökningen av att mäta marginaleffekten på
     * @param newVal Det nya värdet
     * @param oldVal Det tidigare värdet
     * @param notInverted inverterar resultatet, 0.75 blir 0.25
     * @returns {Number}
     */
    marginalEff: function (increment, newVal, oldVal, notInverted) {

        if (notInverted) {
            return ((newVal - oldVal) / increment) * 100;
        }
        return (1 - ((newVal - oldVal) / increment)) * 100;

    },


    config: {
        /**
         * Url för scriptloader, ändra denna om du tex vill en annan url för dina filer.
         * @memberof! marginalCalc
         * @default
         * @alias marginalCalc.config.url
         * @type {String}
         */
        url: "",
        /**
         * Namnet på mappen för dina moduler.
         * @memberof! marginalCalc
         * @default
         * @alias marginalCalc.config.url
         * @type {String}
         */
        moduleFolder: "modules/"
    },

    scriptLoader: {
        complete: true,
        promisedFiles: [],
        loaded: [],
        /**
         * Denna funktion bör anropas av varje fil som ska laddas av addScripts. Om inte detta görs så kör kommer kalkylatorn inte börja initiera modulerna.
         * @example // /modules/mina_nya_moduler.js
         * var nyaModulen = function() {
         *      this.totalCalc = function() {
         *          return -200;
         *      }
         * }
         * marginalCalc.addModule("NyModul", nyaModulen);
         * marginalCalc.scriptLoader.loadComplete("mina_nya_moduler");
         * @param {String} file Name of the file that has loaded
         * @alias marginalCalc.scriptLoader.loadComplete
         * @memberof! marginalCalc
         */
        loadComplete: function (file) {
            this.loaded.push(file);
            if (this.promisedFiles.length === this.loaded.length) {
                this.complete = true;
            }
        },
        /**
         * Laddar in javascript filer för användning i marginalCalc
         * @example marginalCalc.scriptLoader.addScripts(["socialbidrag", "bostadsbidrag"])
         * // -> laddar /modules/socialbidrag.js och /modules/bostadsbidrag.js
         * @alias marginalCalc.scriptLoader.addScripts
         * @memberof! marginalCalc
         * @param {Array} modules An array of filenames to load from the modules folder
         */
        addScripts: function (files) {
            this.promisedFiles = files;
            this.complete = false;
            var that = this;
            for (var i = 0; i < files.length; i++) {
                var file = files[i];
                var fileref = document.createElement('script');
                fileref.type = "text/javascript";
                fileref.src = marginalCalc.config.url + marginalCalc.config.moduleFolder + file + ".js";
                fileref.async = true;
                document.getElementsByTagName("head")[0].appendChild(fileref);
            }
        }
    }
};