if (fcf.isServer()){
  var libFS = require('fs');
  var libUtil = require('util');
}

fcf.module({
  name: "fcf:NRender/NDetails/Loader.js",
  dependencies: [],
  module: function() {
    var NDetails = fcf.prepareObject(fcf, "NRender.NDetails");
    fcf.addException("ERROR_RENDER_SERVER_ONLY_TEMPLATE",   "The '${{template}}$' template can only be rendered on the server");
    fcf.addException("ERROR_RENDER_TEMPLATE_NOT_FOUND",     "Element '${{template}}$' template not found");

    var innerTemplateStorage = {};
    var innerDependencies = {};


    NDetails.Loader = class Loader {

      constructor(a_options) {
        this._options = fcf.append({}, a_options);
      }

      setSettings(a_options) {
        fcf.append(this._options, a_options);
      }

      load(a_path, a_state, a_id, a_cb) {
        let date = new Date();
        if (typeof a_state === "function" || arguments.length == 1){
          a_cb = a_state;
          a_state = {
            theme: fcf.application.getThemes().getTheme("defaultTheme", true),
            include: {},
          }
        }
        a_path = a_path.split("+")[0];
        a_path = a_state.theme.resolveAlias(a_path);
        let arrPath = a_path.split("+");
        let path   = arrPath[0];

        return fcf.actions()
        .then(async ()=>{
          let template = await this._loadTemplateData(a_path, a_state, a_id, a_path);
          let lsttmpl = fcf.NDetails.currentTemplate;
          fcf.NDetails.currentTemplate = a_path;
          template = this._resolveTemplate(template, a_path);
          fcf.NDetails.currentTemplate = lsttmpl;
          let event = await fcf.application.getEventChannel().send("loading_template", {path: a_path, template: template})
          if (a_cb)
            a_cb(undefined, event.template);
          return event.template;
        })
        .catch((a_error)=>{
          if (a_cb)
            a_cb(a_error);
        });
      }

      loadInfo(a_path, a_state, a_id, a_cb) {
        let date = new Date();
        if (typeof a_state === "function" || arguments.length == 1){
          a_cb = a_state;
          a_state = {
            theme: fcf.application.getThemes().getTheme("defaultTheme", true),
            include: {},
          }
        }
        a_path = a_path.split("+")[0];
        a_path = a_state.theme.resolveAlias(a_path);
        let arrPath = a_path.split("+");
        let path   = arrPath[0];

        return fcf.actions()
        .then(async ()=>{
          let template = await this._loadTemplateData(a_path, a_state, a_id, a_path);
          if (a_cb)
            a_cb(undefined, template);
          return template;
        })
        .catch((a_error)=>{
          if (a_cb)
            a_cb(a_error);
        });
      }


      _resolveTemplate(a_template, a_path){
        let result = fcf.append({}, a_template);
        result.templates = fcf.append({}, result.templates);
        for(let block in result.templates) {
          result.templates[block] = fcf.append({}, result.templates[block]);

          let args = {};
          for(let i = 0; i < result.templates[block].rawArguments.length; ++i){
            let currentArgs = fcf.scriptExecutor.parse(result.templates[block].rawArguments[i].data, {}, result.templates[block].rawArguments[i].path, result.templates[block].rawArguments[i].startLine);
            fcf.append(args, currentArgs);
          }

          result.templates[block].arguments = args;
          result.templates[block].hardDependencies = {};
          for(let key in result.templates[block].arguments){
            if (!fcf.isArg(result.templates[block].arguments[key]))
              continue;
            if (!result.templates[block].arguments[key].hardDependencies)
              continue;
            if (!(key in result.templates[block].hardDependencies))
              result.templates[block].hardDependencies[key] = [];
            fcf.append(result.templates[block].hardDependencies[key], result.templates[block].arguments[key].hardDependencies);
          }
        }

        return result;
      }

      async _loadTemplateData(a_path, a_state, a_id, a_mainTemplate){
        let self = this;
        let originPath = a_path;
        a_path = fcf.getPath(a_path);

        if (innerTemplateStorage[a_path])
          return innerTemplateStorage[a_path];

        let data = await fcf.load({path: a_path, aliases: a_state.theme.getAliases()})
        let stat = await libUtil.promisify(libFS.stat)(a_path);
        let lastModifyTime = stat.mtime;

        //Loading template data
        let lstct = fcf.NDetails.currentTemplate;
        fcf.NDetails.currentTemplate = a_mainTemplate;
        let template = this._parseTemplate(data, a_path, originPath, a_state, a_id, fcf.getContext());
        template.lastModifyTime = lastModifyTime;
        fcf.NDetails.currentTemplate = lstct;

        //Loading template hooks
        let error = undefined;
        fcf.each(template.templates, (a_key, a_subTemplate)=>{
          let hookFileArr = a_path.split(".");
          hookFileArr.pop();
          if (a_key != "")
            hookFileArr[hookFileArr.length-1] += "+" + a_key;
          hookFileArr.push("hooks");
          hookFileArr.push("js");
          let hookFilePath = hookFileArr.join(".");
          fcf.requireEx([hookFilePath], {showError: false}, (error, hooks)=>{
            if (error && error.toString().indexOf("SyntaxError") !== -1){
              error = error;
              return false;
            }

            if (hooks){
              let nh = {};
              fcf.each(hooks.hooksProgramableArgument, (a_key, a_hook)=>{ 
                nh[a_key] = a_hook;
              });
              hooks.hooksProgramableArgument = nh;

              nh = {};
              fcf.each(hooks.hooksBeforeArgument, (a_key, a_hook)=>{ 
                nh[a_key] = a_hook;
              });
              hooks.hooksBeforeArgument = nh;

              nh = {};
              fcf.each(hooks.hooksAfterArgument, (a_key, a_hook)=>{ 
                nh[a_key] = a_hook;
              });
              hooks.hooksAfterArgument = nh;

              fcf.append(a_subTemplate.hooks, hooks);
            }
          })
        });

        if (error)
          throw error;

        //Apply the inherited functional
        if (template.options.extends){
          let extendsPath = fcf.getPath(template.options.extends);
          if (!innerDependencies[extendsPath])
            innerDependencies[extendsPath] = {};
          innerDependencies[extendsPath][a_path] = a_path;
          let etemplate = await this._loadTemplateData(template.options.extends, a_state, a_id, a_mainTemplate);
          template = this._inheritance(etemplate, template);
        }

        //Save into local buffer
        let initWatcher = !innerTemplateStorage[a_path];
        innerTemplateStorage[a_path] = template;

        //Set watch file
        if (initWatcher) {
          function clClearStorage(a_path){
            let obj = innerDependencies[a_path];
            delete innerDependencies[a_path];
            fcf.scriptExecutor.clear(a_path);
            delete innerTemplateStorage[a_path];
            fcf.each(obj, (a_depPath) => {
              clClearStorage(a_depPath);
            })
          }

          libFS.watchFile(a_path, {interval: 1000}, (a_eventType, a_filename)=>{
            clClearStorage(a_path);
          });
        }

        return template;
      }

      _inheritance(a_baseTemplate, a_template){
        let self = this;
        let result = {
          options: {},
          templates: {},
        };
        fcf.append(true, result.options, a_baseTemplate.options);
        fcf.append(true, result.options, a_template.options);
        let names = {};
        fcf.each(a_template.templates, function(k){names[k] = true;});
        fcf.each(a_baseTemplate.templates, function(k){names[k] = true;});
        for(let tname in names){
          if (!result.templates[tname])
            result.templates[tname] = {
              arguments:        {},
              rawArguments:     [],
              template:         { items: [], template: "" },
              hooks:            {},
              hardDependencies: {},
            };

          // build raw arguments
          let rawArgs = a_baseTemplate.templates[tname] && a_baseTemplate.templates[tname].rawArguments ? a_baseTemplate.templates[tname].rawArguments : [];
          fcf.append(result.templates[tname].rawArguments, rawArgs);
          rawArgs = a_template.templates[tname] && a_template.templates[tname].rawArguments ? a_template.templates[tname].rawArguments : [];
          fcf.append(result.templates[tname].rawArguments, rawArgs);

          // build hardDependencies
          let baseHardDependencies = a_baseTemplate.templates[tname] && a_baseTemplate.templates[tname].hardDependencies ? a_baseTemplate.templates[tname].hardDependencies : undefined;
          let hardDependencies     = a_template.templates[tname] && a_template.templates[tname].hardDependencies ? a_template.templates[tname].hardDependencies : undefined;
          fcf.append(result.templates[tname].hardDependencies, baseHardDependencies, hardDependencies);

          // build templates
          if (a_baseTemplate.templates[tname] && !fcf.empty(a_baseTemplate.templates[tname].template))
            result.templates[tname].template = a_baseTemplate.templates[tname].template;
          if (a_template.templates[tname] && !fcf.empty(a_template.templates[tname].template))
            result.templates[tname].template = a_template.templates[tname].template;

          // build hooks
          if (a_baseTemplate.templates[tname] && !fcf.empty(a_baseTemplate.templates[tname].hooks)){
            fcf.append(result.templates[tname].hooks, a_baseTemplate.templates[tname].hooks);
            if (!result.templates[tname].hooks.prototype)
              result.templates[tname].hooks.prototype = {};
            result.templates[tname].hooks.prototype = a_baseTemplate.templates[tname].hooks;
          }
          if (a_template.templates[tname] && !fcf.empty(a_template.templates[tname].hooks)){
            fcf.append(result.templates[tname].hooks, a_template.templates[tname].hooks);
            let dstHooks = result.templates[tname].hooks;
            let srcHooks = dstHooks.prototype;
            if (srcHooks) {
              if (!fcf.empty(srcHooks.hooksProgramableArgument)){
                for(let key in srcHooks.hooksProgramableArgument){
                  if (!(key in dstHooks.hooksProgramableArgument)){
                    dstHooks.hooksProgramableArgument[key] = srcHooks.hooksProgramableArgument[key];
                  }
                }
              }
              if (!fcf.empty(srcHooks.hooksBeforeArgument)){
                for(let key in srcHooks.hooksBeforeArgument)
                  if (!(key in dstHooks.hooksBeforeArgument))
                    dstHooks.hooksBeforeArgument[key] = srcHooks.hooksBeforeArgument[key];
              }
              if (!fcf.empty(srcHooks.hooksAfterArgument)){
                for(let key in srcHooks.hooksAfterArgument)
                  if (!(key in dstHooks.hooksAfterArgument))
                    dstHooks.hooksAfterArgument[key] = srcHooks.hooksAfterArgument[key];
              }
            }
          }
        }
        result.lastModifyTime = fcf.min(a_baseTemplate.lastModifyTime, a_template.lastModifyTime);

        return result;
      }

      _parseTemplate(a_ctxt, a_path, a_originPath, a_state, a_id, a_context) {
        let currentTemplate = fcf.NDetails.currentTemplate;
        var result = {
          options:   {},
          templates: {},
        };

        var blocksPositions = [];
        var lstpos = 0;
        do {
          var blockPositions = this._getBlockPositions(a_ctxt, lstpos);
          lstpos = blockPositions.dataEnd;
          if (lstpos) {
            blocksPositions.push(blockPositions);
          }
        } while (lstpos);

        for (var i = 0; i < blocksPositions.length; ++i) {
          this._parseBlockOptions(result, a_ctxt, blocksPositions[i], a_path);
        }

        let include = [];
        if (Array.isArray(result.options.include))
          include = result.options.include;
        else if (!fcf.empty(result.options.include))
          include = [result.options.include];

        let jsInclude = [];
        for(let i = 0; i < include.length; ++i) {
          let ext = fcf.getExtension(include[i]).toLowerCase();
          let filePath = include[i];
          if (filePath[0] != "/" && filePath.indexOf(":") == -1){
            filePath = fcf.getDirectory(a_originPath) + "/" + filePath;
            if (filePath[0] != "/" && filePath.indexOf(":") == -1)
              filePath = ":" + filePath;
          }
          include[i] = filePath;
          if (ext == "js"){
            jsInclude.push(include[i]);
          }
        }

        result.options.include = include;

        if (!fcf.empty(jsInclude)){
          fcf.require(jsInclude);
        }

        include = [];
        if (Array.isArray(result.options.webInclude))
          include = result.options.webInclude;
        else if (!fcf.empty(result.options.webInclude))
          include = [result.options.webInclude];
        for(let i = 0; i < include.length; ++i) {
          let ext = fcf.getExtension(include[i]).toLowerCase();
          let filePath = include[i];

          if (filePath[0] != "/" && filePath.indexOf(":") == -1){
            filePath = fcf.getDirectory(a_originPath) + "/" + filePath;
            if (filePath[0] != "/" && filePath.indexOf(":") == -1)
              filePath = ":" + filePath;
          }
          include[i] = filePath;
        }
        result.options.webInclude = include;

        fcf.NDetails.currentTemplate = currentTemplate;
        for (var i = 0; i < blocksPositions.length; ++i) {
          this._parseBlock(result, a_ctxt, blocksPositions[i], a_path);
        }

        return result;
      }

      _parseBlockOptions(a_dst, a_ctxt, a_blockPositions, a_path) {
        let headerLine = a_ctxt.substr(a_blockPositions.headerBeg+3, a_blockPositions.headerEnd - a_blockPositions.headerBeg-3);
        let arrHeaderLine = fcf.splitSpace(headerLine);
        let type = arrHeaderLine[0] ? arrHeaderLine[0].toUpperCase() : "";
        if (type == "OPTIONS") {
          let data = a_ctxt.substr(a_blockPositions.dataBeg, a_blockPositions.dataEnd - a_blockPositions.dataBeg);
          let stringNumber = this._getStringNumber(a_ctxt, a_blockPositions.dataBeg);
          let options = fcf.scriptExecutor.parse(data, {}, a_path, stringNumber);
          fcf.append(a_dst.options, options);
        }
      }

      _parseBlock(a_dst, a_ctxt, a_blockPositions, a_path) {
        let headerLine = a_ctxt.substr(a_blockPositions.headerBeg+3, a_blockPositions.headerEnd - a_blockPositions.headerBeg-3);
        let arrHeaderLine = fcf.splitSpace(headerLine);
        let type = arrHeaderLine[0] ? arrHeaderLine[0].toUpperCase() : "";
        let name = arrHeaderLine[1] ? arrHeaderLine[1] : "";
        if (!a_dst.templates[name]){
          a_dst.templates[name] = {
            arguments:        {},
            rawArguments:     [],
            template:         {},
            hooks:            {},
            hardDependencies: {},
          };
        }
        if (type == "ARGUMENTS") {
          let data = a_ctxt.substr(a_blockPositions.dataBeg, a_blockPositions.dataEnd - a_blockPositions.dataBeg);
          let stringNumber = this._getStringNumber(a_ctxt, a_blockPositions.dataBeg);
          a_dst.templates[name].rawArguments.push({data: data, path: a_path, startLine: stringNumber});
        } else if (type == "TEMPLATE") {
          let content = a_ctxt.substr(a_blockPositions.dataBeg, a_blockPositions.dataEnd - a_blockPositions.dataBeg);
          a_dst.templates[name].template.template = this._contentToJS(this._removeLastLF(content));
          a_dst.templates[name].template.path = a_path;
          a_dst.templates[name].template.stringNumber = this._getStringNumber(a_ctxt, a_blockPositions.dataBeg);
        }
      }

      _getStringNumber(a_txt, a_position){
        if (a_position === undefined)
          a_position = a_txt.length;
        let pos = undefined;
        let n   = 0;
        while(true) {
          pos = a_txt.indexOf("\n", pos !== undefined ? pos+1 : 0);
          if (pos == -1 || pos > a_position)
            return n;
          ++n;
        }
      }

      _getBlockPositions(a_ctxt, a_start) {
        var result = {
          headerBeg: undefined,
          headerEnd: undefined,
          dataBeg: undefined,
          dataEnd: undefined,
        };
        var pos;
        while(true) {
          pos = a_ctxt.indexOf("//~", a_start);
          if (pos != -1 && pos != 0 && a_ctxt[pos-1] != "\n") {
            a_start = pos;
            continue;
          }
          if (pos == -1)
            return result;
          var dpos = pos+3;
          if (dpos >= a_ctxt.length)
            return result;
          var epos = dpos;
          while (a_ctxt.charCodeAt(epos) > 32 && epos < a_ctxt.length)
            ++epos;
          if (epos >= a_ctxt.length)
            return result;
          var type = a_ctxt.substring(dpos, epos).toLowerCase();
          var validBlocks = {"template":true, "options":true, "arguments":true};
          if (!(type in validBlocks)){
            a_start = epos;
            continue;
          }
          break;
        }

        result.headerBeg = pos;
        if (result.headerBeg == -1){
          result.headerBeg = undefined;
          return result;
        }

        result.headerEnd = a_ctxt.indexOf("\n", pos);
        if (result.headerEnd == -1){
          result.headerBeg = undefined;
          result.headerEnd = undefined;
          return result;
        }

        result.dataBeg = (result.headerEnd + 1) > a_ctxt.length ? undefined
                                                                : result.headerEnd + 1;
        if (result.dataBeg === undefined){
          result.headerBeg = undefined;
          result.headerEnd = undefined;
          return result;
        }

        var lstpos = result.dataBeg;
        while (true) {
          pos = a_ctxt.indexOf("//~", lstpos);
          if (pos != -1 && a_ctxt[pos-1] != "\n") {
            lstpos = pos;
            continue;
          }
          break;
        }
        result.dataEnd = pos != -1 ? pos : a_ctxt.length;

        return result;
      }

      _removeLastLF(a_content){
        if (a_content.length >= 2 && a_content.charAt(a_content.length-2) == "\r" && a_content.charAt(a_content.length-1) == "\n")
          return a_content.substr(0, a_content.length-2);
        if (a_content.length >= 1 && a_content.charAt(a_content.length-1) == "\n")
          return a_content.substr(0, a_content.length-1);
        return a_content;
      }

      _contentToJS(a_content) {
        var items = [];
        var pos = 0;
        var startPos = 0;
        var type = "content";
        while(pos != -1) {
          var pos = a_content.indexOf("{{", pos);

          if (pos !== -1 && pos !== 0 && a_content[pos-1] == "$") {
            if (pos - startPos - 1)
              items.push({type: "content", data: a_content.substr(startPos, pos - startPos - 1)});
            var endPos = a_content.indexOf("}}$", pos);
            if (endPos != -1) {
              items.push({type: "variable", data: fcf.trim(a_content.substr(pos+2, endPos - pos - 2))});
              pos = endPos+3;
              startPos = pos;
            } else {
              pos = -1;
            }
          } else if (pos !== -1 && pos !== 0 && a_content[pos-1] == "#") {
            if (pos - startPos - 1)
              items.push({type: "content", data: a_content.substr(startPos, pos - startPos - 1)});
            var endPos = a_content.indexOf("}}#", pos);
            if (endPos != -1) {
              items.push({type: "variable", data: fcf.trim(a_content.substr(pos+2, endPos - pos - 2))});
              pos = endPos+3;
              startPos = pos;
            } else {
              pos = -1;
            }
          } else if (pos !== -1 && pos !== 0 && a_content[pos-1] == "@") {
            if (pos - startPos - 1)
              items.push({type: "content", data: a_content.substr(startPos, pos - startPos - 1)});
            let endPos = undefined;
            let fpos = pos;
            let counter = 0;
            while(true){
              let startNextBlock = a_content.indexOf("@{{", fpos);  
              endPos = a_content.indexOf("}}@", fpos); 
              if (!counter && (startNextBlock > endPos || startNextBlock == -1 || endPos == -1) )
                break;
              if (startNextBlock > endPos || (startNextBlock === -1 && endPos !== -1)){
                fpos = endPos+3;
                ++counter;
              } else if (endPos == -1){
                break;
              } else {
                fpos = startNextBlock+3;
                --counter;
              }
            }
            if (endPos != -1) {
              items.push({type: "calculation", data: fcf.trim(fcf.trim(a_content.substr(pos+2, endPos - pos - 2)), [";"])});
              pos = endPos+3;
              startPos = pos;
            } else {
              pos = -1;
            }
          } else if (pos !== -1 && pos !== 0 && a_content[pos-1] == "!") {
            if (pos - startPos - 1)
              items.push({type: "content", data: a_content.substr(startPos, pos - startPos - 1)});
            var endPos = a_content.indexOf("}}!", pos);
            if (endPos != -1) {
              items.push({type: "translate", data: fcf.trim(a_content.substr(pos+2, endPos - pos - 2))});
              pos = endPos+3;
              startPos = pos;
            } else {
              pos = -1;
            }
          } else if (pos !== -1 && pos !== 0 && a_content[pos-1] == "%") {
            if (pos - startPos - 1)
              items.push({type: "content", data: a_content.substr(startPos, pos - startPos - 1)});
            var endPos = a_content.indexOf("}}%", pos);
            if (endPos != -1) {
              items.push({type: "code", data: a_content.substr(pos+2, endPos - pos - 2)});
              pos = endPos+3;
              startPos = pos;
            } else {
              pos = -1;
            }
          } else if (pos == -1) {
            if (a_content.length - startPos)
              items.push({type: "content", data: a_content.substr(startPos, a_content.length - startPos)});
            pos = -1;
          } else {
            pos = pos+2;
          }
        }

        var result = "var _2318115_block_env={args: args, route: route, decor: decor};";
        for (var i = 0; i < items.length; ++i) {
          if (items[i].type == "code"){
            result += items[i].data;
            result += ";";
          } else if (items[i].type == "variable") {
            result += "render.write(fcf.resolve(_2318115_block_env, " + JSON.stringify(items[i].data) + ") );";
          } else if (items[i].type == "calculation") {
            result += "render.write(" + items[i].data + ");";
          } else if (items[i].type == "translate") {
            //result += "render.write(fcf.t(\"" + fcf.escapeQuotes(items[i].data, undefined, true) + "\", undefined, true));";
            result +="(()=>{";
            result +="  let content947339 = fcf.t(" + JSON.stringify(items[i].data) + ");";
            result +="  let contentIndex947339 = 0;";
            result +="  while(contentIndex947339 < content947339.length && contentIndex947339 != -1) {";
            result +="    let posStart947339 = content947339.indexOf('@{{', contentIndex947339);";
            result +="    render.write(";
            result +="      content947339.substr(contentIndex947339, posStart947339 != -1 ? (posStart947339 - contentIndex947339) : (content947339.length - contentIndex947339))";
            result +="    );";
            result +="    let posEnd947339 = content947339.indexOf('}}@', posStart947339);";
            result +="    if (posStart947339 == -1 || posEnd947339 == -1)";
            result +="      break;";
            result +="    render.write(eval(fcf.trim(content947339.substr(posStart947339 + 3, (posEnd947339 - posStart947339) - 3), ';')));";
            result +="    contentIndex947339 = posEnd947339 + 3;";
            result +="  }";
            result +="})();";
          } else if (items[i].type == "content") {
            result += "render.write(" + JSON.stringify(items[i].data) + ");";
            let lines  = this._getStringNumber(items[i].data);
            for(let i = 0; i < lines; ++i)
              result += "\n";
          }
        }

        return result;
      }
    }

    return NDetails.Loader;
  }
});
