const libFS   = require("fs");
const libUtil = require("util");

fcf.module({
  name:         "fcf:NSystem/builder.js",
  dependencies: ["fcf:NSystem/babel.js", "fcf:NRender/NDetails/Helper.js"],
  lazy:         [],
  module: function(babel, helper){
    var Namespace = fcf.prepareObject(fcf, "NSystem");

    function minify(a_content){
      let result = "";
      // state:
      //       0 - code
      //       1 - double quotes
      //       2 - quotes
      //       3 - //
      //       4 - /*
      //       5 - RegEx
      //       6 - RegEx [
      //       7 - RegEx (
      let state = 0;
      let spaceCounter = 0;
      let slashCounter = 0;

      function isRegEx(a_index){
        --a_index;
        while(a_index >= 0){
          let cn = a_content.charCodeAt(a_index);
          let c = a_content[a_index];
          if (cn <= 32){
            --a_index;
            continue;
          }
          if (c == "-" ||
              c == ":" ||
              c == "*" ||
              c == "+" ||
              c == "|" ||
              c == "&" ||
              c == "," ||
              c == "{" ||
              c == "(" ||
              c == "=") {
            return true;
          }
          break;
        }
        return false;
      }

      for(let i = 0; i < a_content.length; ++i) {
        let c = a_content.charCodeAt(i);
        if (a_content[i-1] == "\\")
          ++slashCounter;
        else
          slashCounter = 0;

        if (state == 0){
          if (c <= 32){
            if (spaceCounter == 0)
              result += a_content[i];
            else if (a_content[i] == "\n")
              result += a_content[i];
            ++spaceCounter;
          } else {
            spaceCounter = 0;
            if (a_content[i]=="/" && a_content[i+1]=="/"){
              state = 3;
              continue;
            } else if (a_content[i]=="/" && a_content[i+1]=="*"){
              state = 4;
              continue;
            } else {
              result += a_content[i];
            }
          }
          if (a_content[i] == "/" && isRegEx(i)){
            state = 5;
          } else if (a_content[i] == "\""){
            state = 1;
          } else if (a_content[i] == "'"){
            state = 2;
          }
        } else if (state == 1) {
          result += a_content[i];
          if (slashCounter%2 == 0 && a_content[i] == "\"")
            state = 0;
        } else if (state == 2) {
          result += a_content[i];
          if (slashCounter%2 == 0 && a_content[i] == "'")
            state = 0;
        } else if (state == 3) {
          if (a_content[i] == "\n")
            state = 0;
        } else if (state == 4) {
          if (a_content[i] == "*" && a_content[i+1] == "/"){
            ++i;
            state = 0;
          }
        } else if (state == 5) {
          result += a_content[i];
          if (slashCounter%2 == 0 && a_content[i] == "/")
            state = 0;
          else if (slashCounter%2 == 0 && a_content[i] == "[")
            state = 6;
          else if (slashCounter%2 == 0 && a_content[i] == "(")
            state = 7;
        } else if (state == 6) {
          result += a_content[i];
          if (slashCounter%2 == 0 && a_content[i] == "]")
            state = 5;
        } else if (state == 7) {
          result += a_content[i];
          if (slashCounter%2 == 0 && a_content[i] == ")")
            state = 5;
        }
      }
      return result;
    }

    class Builder {
      constructor() {
        this._watchFiles  = {};
        this._objects     = {};
        this._handlers    = {};
      }

      async get(a_type, a_uri){
        if (this._objects[a_uri] && !fcf.empty(this._objects[a_uri].result))
          return this._objects[a_uri].result;
        await this._build(a_type, a_uri);
        return this._objects[a_uri] && !fcf.empty(this._objects[a_uri].result) ? this._objects[a_uri].result : undefined;
      }

      async appendGroup(a_type, a_uri, a_files){
        await this._build(a_type, a_uri, a_files);
      }

      appendHandler(a_type, a_handler){
        this._handlers[a_type] = a_handler;
      }

      async _build(a_type, a_uri, a_files){
        let self = this;
        return fcf.application.getSystemActions()
        .then(()=>{
          fcf.setContext(fcf.application.getSystemContext());
          let uriItems  = Array.isArray(a_files) && !fcf.empty(a_files) ? a_files : [a_uri];
          let files     = fcf.array(uriItems, (k,v)=>{ return fcf.getPath(v); });
          if (!self._objects[a_uri]) {
            this._objects[a_uri] = {
              type:         a_type,
              actions:      fcf.actions(),
              directories:  {},
              dependencies: {},
              result:       {},
              files:        a_files,
            };
            for(let i = 0; i < files.length; ++i)
              this._objects[a_uri].directories[fcf.getDirectory(files[i])] = true;
          }
          let actions = fcf.actions();
          actions.then((a_res, a_act)=>{
            this._objects[a_uri].actions
            .then(async ()=>{
              try {
                await self._handlers[a_type](a_uri, uriItems, self._objects[a_uri].result, self._objects[a_uri].dependencies);
                a_act.complete();
              }catch(e){
                a_act.error(e);
              }
            });
          });
          fcf.each(uriItems, (k, a_uriItem)=>{
            this._watchFile(a_uriItem);
            this._watchDirectory(fcf.getDirectory(fcf.getPath(a_uriItem)));
          })
          return actions;
        });
      }

      _watchFile(a_uri){
        let self = this;
        let file = fcf.getPath(a_uri);
        if (file in this._watchFiles)
          return;
        this._watchFiles[file] = true;
        libFS.watchFile(file, {interval: 1000}, async ()=>{
          try {
            await self._updateFile(a_uri);
          } catch(e){
            fcf.log.err("FCF", `Failed update file ${a_uri}`, e);
          }
        });
      }

      _watchDirectory(a_directory) {
        let self = this;
        if (a_directory in this._watchFiles)
          return;
        this._watchFiles[a_directory] = true;

        libFS.watchFile(a_directory, {interval: 1000}, async ()=>{
          try {
            await self._updateDirectory(a_directory);
          } catch(e){
            fcf.log.err("FCF", `Failed update files from directory ${a_directory}`, e);
          }
        });
      }

      async _updateFile(a_uri) {
        if (a_uri in this._objects)
          await this._build(this._objects[a_uri].type, a_uri, this._objects[a_uri].files);
        for(let uri in this._objects){
          if (a_uri in this._objects[uri].dependencies){
            await this._build(this._objects[uri].type, uri, this._objects[uri].files);
          }
        }
      }

      async _updateDirectory(a_directory) {
        for(let uri in this._objects)
          if (a_directory in this._objects[uri].directories)
            await this._build(this._objects[uri].type, uri, this._objects[uri].files);
      }

      async _jsHandler(a_uri, a_uriItems, a_result, a_dependencies){
        let pos = a_uri.lastIndexOf(".wrapper.js");
        if (pos != -1 && pos == a_uri.length - ".wrapper.js".length) {
          return await this._wrapeprJSHandler(a_uri, a_uriItems, a_result, a_dependencies);
        } else {
          return await this._simpleJSHandler(a_uri, a_uriItems, a_result, a_dependencies);
        }
      }

      async _simpleJSHandler(a_uri, a_uriItems, a_result, a_dependencies){
        let compressFilePath = fcf.getPath(":cache/jscompress") + "/" + encodeURIComponent(a_uri);
        let es5FilePath      = fcf.getPath(":cache/jses5") + "/" + encodeURIComponent(a_uri);
        let resultFile       = a_uriItems.length == 1 && a_uriItems[0] == a_uri ? fcf.getPath(a_uri) : fcf.getPath(":cache/js") + "/" + encodeURIComponent(a_uri);

        try {
          let lastModifyTime = new Date(0);
          await fcf.each(a_uriItems, async (k, a_uriItem)=>{
            let stat = await libUtil.promisify(libFS.stat)(fcf.getPath(a_uriItem));
            lastModifyTime = fcf.max(stat.mtime, lastModifyTime);
          })
          let lasttm = undefined;
          try {
            lasttm = (await libUtil.promisify(libFS.stat)(compressFilePath)).mtime;
          } catch(e){
          }
          if (!!lasttm && lasttm > lastModifyTime){
            a_result.basic    = resultFile;
            a_result.compress = compressFilePath;
            a_result.es5      = es5FilePath;
            fcf.each(a_uriItems, (a_key, a_item)=>{
              a_dependencies[a_item] = true;
            })
            return;
          }
        } catch(e) {
        }

        fcf.log.log("FCF", `Build client file "${a_uri}"`);

        try {
          await libUtil.promisify(libFS.mkdir)(fcf.getPath(":cache/js"), { recursive: true });
        } catch(e){
        }
        try {
          await libUtil.promisify(libFS.mkdir)(fcf.getPath(":cache/jscompress"), { recursive: true });
        } catch(e){
        }
        try {
          await libUtil.promisify(libFS.mkdir)(fcf.getPath(":cache/jses5"), { recursive: true });
        } catch(e){
        }

        let fileContent = "";
        await fcf.each(a_uriItems, async (a_key, a_uri)=>{
          if (fcf.count(a_uriItems) > 1){
            fileContent += "//--------------------------------------------------//\n";
            fileContent += "//---------------------- FILE ----------------------//\n";
            fileContent += "//--------------------------------------------------//\n";
            fileContent += "//--- " + a_uri + "\n";
            fileContent += "//--------------------------------------------------//\n";
          }

          fileContent += await libUtil.promisify(libFS.readFile)(fcf.getPath(a_uri), {encoding: 'utf8'});
          fileContent += "\n\n\n";
        })

        if (a_uriItems.length != 1 || a_uriItems[0] != a_uri)
          await libUtil.promisify(libFS.writeFile)(resultFile, fileContent);



        let compressFileContent = minify(fileContent);
        await libUtil.promisify(libFS.writeFile)(compressFilePath, compressFileContent);

        let es5Content;
        try {
          es5Content = await babel.transform(fileContent);
        } catch(e){
          throw new fcf.Exception("ERROR", {error: `Incorrect process file '${a_uri}' in babel`}, e)
        }

        await libUtil.promisify(libFS.writeFile)(es5FilePath, es5Content);

        a_result.basic    = resultFile;
        a_result.compress = compressFilePath;
        a_result.es5      = es5FilePath;
        fcf.each(a_uriItems, (a_key, a_item)=>{
          a_dependencies[a_item] = true;
        })
      }

      async _wrapeprJSHandler(a_uri, a_uriItems, a_result, a_dependencies){
        // a_uriItems NO IMPL (NOT USING) !!!
        let self = this;
        let file         = fcf.getPath(a_uri);
        let templatePath =  a_uri.substr(0, a_uri.length - ".wrapper.js".length).split("+")[0];
        templatePath     += ".tmpl";
        let block        =  a_uri.substr(0, a_uri.length - ".wrapper.js".length).split("+")[1];

        let templateLoader  = fcf.application.getRender().getLoader();
        let templateInfo    = await templateLoader.loadInfo(templatePath, {theme: fcf.application.getTheme()}, "");

        let wrapperExists    = false;
        let lastModifyTime  = templateInfo.lastModifyTime;
        try {
          lastModifyTime = fcf.max((await libUtil.promisify(libFS.stat)(file)).mtime, lastModifyTime);
          wrapperExists = true;
        }catch(e){
        }

        let processedWrapperPath            = fcf.getPath(":cache/js") + "/" + encodeURIComponent(a_uri);
        let processedWrapperCompressPath    = fcf.getPath(":cache/jscompress") + "/" + encodeURIComponent(a_uri);
        let es5ProcessedWrapperCompressPath = fcf.getPath(":cache/jses5") + "/" + encodeURIComponent(a_uri);
        let processedWrapperEmptyFlagPath   = processedWrapperPath + "-empty";
        let processedWrapperIsEmpty         = false;
        let processedWrapperLastModifyTime  = undefined;
        try {
          processedWrapperIsEmpty = await libUtil.promisify(libFS.exists)(processedWrapperEmptyFlagPath);
        } catch(e) {
        }
        try {
          processedWrapperLastModifyTime = (await libUtil.promisify(libFS.stat)(processedWrapperPath)).mtime;
        }catch(e){
        }

        let needBuild = false;
        needBuild    |= wrapperExists != !processedWrapperIsEmpty;
        needBuild    |= !processedWrapperLastModifyTime;
        needBuild    |= processedWrapperLastModifyTime < lastModifyTime;

        a_dependencies[templatePath] = true;
        this._watchFile(templatePath);
        fcf.each(templateInfo.inheritance, (a_key, a_template)=>{
          a_dependencies[a_template] = true;
          this._watchFile(a_template);
        })

        if (!needBuild){
          a_result.basic               = processedWrapperPath;
          a_result.compress            = processedWrapperCompressPath;
          a_result.es5                 = es5ProcessedWrapperCompressPath;
          a_dependencies[templatePath] = true;
          return;
        }

        fcf.log.log("FCF", `Build client file "${a_uri}"`);

        try {
          await libUtil.promisify(libFS.mkdir)(fcf.getPath(":cache/js"), { recursive: true });
        } catch(e){
        }
        try {
          await libUtil.promisify(libFS.mkdir)(fcf.getPath(":cache/jscompress"), { recursive: true });
        } catch(e){
        }

        let wrapperCode = "";

        if (!wrapperExists){
          let superWrapper;
          let superTemplate = templateInfo.options.extends;
          if (typeof superTemplate === "string" && superTemplate[0] == "@") {
            let alias = superTemplate.substr(1);
            alias = fcf.application.getConfiguration().aliases[alias];
            if (alias)
              superTemplate = alias;
          }
          if (superTemplate && typeof superTemplate === "string"){
            let path = superTemplate.substr(0, superTemplate.length-".tmpl".length);
            if (block)
              path += "+" + block;
            superWrapper = path;
            superWrapper += ".wrapper.js";
          } else {
            superWrapper = "fcf:NClient/Wrapper.js";
          }
          wrapperCode += 'fcf.module({\n';
          wrapperCode += '  name: "' + a_uri + '",\n';
          wrapperCode += '  dependencies: ["' + superWrapper + '"],\n';
          wrapperCode += '  module: function(Wrapper){\n';
          wrapperCode += '    return class WrapperEx extends Wrapper {\n';
          wrapperCode += '      constructor(a_initializeOptions){\n';
          wrapperCode += '        super(a_initializeOptions);\n';
          wrapperCode += '      }\n';
          wrapperCode += '    }\n';
          wrapperCode += '  }\n';
          wrapperCode += '});\n';
        } else {
          wrapperCode += await libUtil.promisify(libFS.readFile)(file, {encoding: 'utf-8'});
        }

        wrapperCode += "\n";

        let subtemplate = block && templateInfo.templates[block] ? templateInfo.templates[block] :
                                                                   templateInfo.templates[""];

        fcf.each(subtemplate.hooksFiles, (a_key, a_file)=>{
          a_dependencies[a_file] = true;
          self._watchFile(a_file);
        })

        let templateBlockPath = templatePath + (block ? "+"+block : "");

        let options = !!block && templateInfo.templates[block] ? templateInfo.templates[block].options :
                      templateInfo.templates[""]               ? templateInfo.templates[""].options :
                                                                 templateInfo.options;
        let sblock = block === undefined ? "" : block;
        if (options.clientRendering)
          wrapperCode += "var lastCurrentTemplate = fcf.getContext().currentTemplate; fcf.getContext().currentTemplate = { id: undefined, template: undefined };\n";
        wrapperCode += "fcf.NDetails.renderInstructions[\"" + templateBlockPath + "\"]={\n";
        wrapperCode += "  options: " + JSON.stringify(options) + ",\n";
        let runtime = templateInfo.templates[sblock] && templateInfo.templates[sblock].runtime ? templateInfo.templates[sblock].runtime : {};
        wrapperCode += "  runtime: " + JSON.stringify(runtime) + ",\n";
        let runtimeArgs = templateInfo.templates[sblock] && templateInfo.templates[sblock].runtimeArgs ? templateInfo.templates[sblock].runtimeArgs : {};
        wrapperCode += "  runtimeArgs: " + JSON.stringify(runtimeArgs) + ",\n";
        wrapperCode += "};\n"

        if (options.clientRendering){
          let templateCode  = !block ? (templateInfo.templates[""] ? templateInfo.templates[""].template.template : "") :
                                       (templateInfo.templates[block] ? templateInfo.templates[block].template.template : "");
          let originPath    = !block ? (templateInfo.templates[""] ? templateInfo.templates[""].template.originPath : a_uri) :
                                       (templateInfo.templates[block] ? templateInfo.templates[block].template.originPath : a_uri);

          // renderFunction: ...
          let renderFunction = "async function(a_args){";
          for(let key in {render:1, args:1, decor:1, route:1, fcf:1})
            renderFunction += `let ${key} = a_args.${key};`;
          renderFunction += templateCode;
          renderFunction += "}";
          wrapperCode += `fcf.NDetails.renderInstructions["${templateBlockPath}"].renderFunction = ${renderFunction};\n`;
          wrapperCode += `fcf.NDetails.renderInstructions["${templateBlockPath}"].originPath = ${JSON.stringify(originPath)};\n`;


          // arguments: ...
          let argumnets = !block ? (templateInfo.templates[""] ? templateInfo.templates[""].rawArguments : "{}") :
                                   (templateInfo.templates[block] ? templateInfo.templates[block].rawArguments : "{}");
          let hardDependencies = !block ? (templateInfo.templates[""] ? templateInfo.templates[""].hardDependencies : "{}") :
                                   (templateInfo.templates[block] ? templateInfo.templates[block].hardDependencies : "{}");
          wrapperCode += `fcf.NDetails.renderInstructions["${templateBlockPath}"].arguments = fcf.append({}`;
          fcf.each(argumnets, (a_key, a_item)=>{
            wrapperCode += ",\n ";
            wrapperCode += a_item.data;
          });
          if ("wrapper" in options && !options.wrapper){
            wrapperCode += ",\n ";
            wrapperCode += "{ fcfWrapper: false }\n ";
          }
          wrapperCode += ");\n"

          // hardDependencies: ...
          wrapperCode += `fcf.NDetails.renderInstructions["${templateBlockPath}"].hardDependencies = ${JSON.stringify(hardDependencies)};\n`;

          // APPEND MODULE HOOKS
          let hooksFiles = [];

          async function getHookFilePath(a_template, a_block){
            let path = a_template.split("+")[0];
            if (path.substr(path.length - 5) == ".tmpl")
              path = path.substr(0, path.length - 5);
            if (!fcf.empty(a_block))
              path += "+" + a_block
            path += ".hooks.js";
            let fileExist = await libUtil.promisify(libFS.exists)(fcf.getPath(path));
            return fileExist ? path : undefined;
          }

          async function getExtendsHooksFiles(a_templateLoader, a_templatePath, a_block, a_dst){
            let hookFilePath = await getHookFilePath(a_templatePath, a_block);
            if (hookFilePath)
              a_dst.unshift(hookFilePath);
            let templateInfo = await a_templateLoader.loadInfo(a_templatePath, {theme: fcf.application.getTheme()}, "");
            if (templateInfo && !fcf.empty(templateInfo.options.extends))
              await getExtendsHooksFiles(a_templateLoader, templateInfo.options.extends, a_block, a_dst);
          }

          let currentHookFile = await getHookFilePath(templatePath, block);
          if (currentHookFile)
            hooksFiles.push(currentHookFile);
          if (!fcf.empty(templateInfo.options.extends))
            await getExtendsHooksFiles(templateLoader, templateInfo.options.extends, block, hooksFiles)

          for(let i = 0; i < hooksFiles.length; ++i) {
            let fileData = await libUtil.promisify(libFS.readFile)(fcf.getPath(hooksFiles[i]), 'utf8');
            wrapperCode += "\n";
            wrapperCode += helper.insertCallPositions(fileData, "hooks", "", 0);
            wrapperCode += "\n";
          }

          let moduleDependencies     = fcf.append([], hooksFiles);
          let moduleDependenciesVars = [];
          for(let i = 0; i < hooksFiles.length; ++i)
            moduleDependenciesVars.push(`___module${i+1}`);

          wrapperCode +=   `fcf.module({\n`+
                           `  name: "${a_uri+"---cri.js"}", \n`+
                           `  dependencies: ${JSON.stringify(moduleDependencies)}, \n`+
                           `  module: function (${moduleDependenciesVars.join(", ")}) {\n`;
          wrapperCode +=   `    let hooks = {\n`;
          wrapperCode +=   `      hooksBeforeArgument: {},\n`;
          wrapperCode +=   `      hooksAfterArgument: {},\n`;
          wrapperCode +=   `      hooksProgrammableArgument: {}\n`;
          wrapperCode +=   `    };\n`;
          wrapperCode +=   `    for(let i = 0; i < ${hooksFiles.length}; ++i){\n`;
          wrapperCode +=   `      let m = arguments[i];\n`;
          wrapperCode +=   `      for(let k in m){\n`;
          wrapperCode +=   `        if (fcf.find(["hooksBeforeArgument", "hooksAfterArgument", "hooksProgrammableArgument"], k) !== undefined ) {\n`;
          wrapperCode +=   `          fcf.append(hooks[k], m[k])\n`;
          wrapperCode +=   `        } else {\n`;
          wrapperCode +=   `          hooks[k] = m[k];\n`;
          wrapperCode +=   `        }\n`;
          wrapperCode +=   `      }\n`;
          wrapperCode +=   `    }\n`;
          wrapperCode +=   `  fcf.NDetails.renderInstructions["${templateBlockPath}"].hooks = hooks;\n`;
          wrapperCode +=   `  fcf.getContext().currentTemplate = lastCurrentTemplate;\n`;
          wrapperCode +=   `}});\n`;
        }

        let compressWrapperCode = minify(wrapperCode);

        let es5Content
        try {
          es5Content = await babel.transform(wrapperCode);
        } catch(e){
          throw new fcf.Exception("ERROR", {error: `Incorrect process file '${a_uri}' in babel`}, e)
        }

        await libUtil.promisify(libFS.writeFile)(es5ProcessedWrapperCompressPath, es5Content);
        await libUtil.promisify(libFS.writeFile)(processedWrapperPath, wrapperCode);
        await libUtil.promisify(libFS.writeFile)(processedWrapperCompressPath, compressWrapperCode);
        a_result.basic    = processedWrapperPath;
        a_result.compress = processedWrapperCompressPath;
        a_result.es5      = es5ProcessedWrapperCompressPath;

        if (!wrapperExists){
          await libUtil.promisify(libFS.writeFile)(processedWrapperEmptyFlagPath, "");
        } else {
          try {
            await libUtil.promisify(libFS.unlink)(processedWrapperEmptyFlagPath);
          } catch(e){ }
        }
      }

    };

    Namespace.builder = new Builder();
    Namespace.builder.appendHandler("js", async (a_uri, a_uriItems, a_result, a_dependencies)=>{ await Namespace.builder._jsHandler(a_uri, a_uriItems, a_result, a_dependencies); })

    return Namespace.builder;
  }
});
