fcf.module({
  name:         "fcf:NClient/Render.js",
  dependencies: ["fcf:NRender/NDetails/ArgsBuilder.js", "fcf:NRender/Template.js"],
  lazy:         [],
  module: function(ArgsBuilder, Template){
    var Namespace = fcf.prepareObject(fcf, "NClient");

    function resolveTemplatePath(a_template){
      let arr = a_template.split("+");
      let template = arr[0][0] == "@" ? fcf.application.getTheme().getAliases()[arr[0].substr(1)] : arr[0];
      return arr[1] ? template + "+" + arr[1] : template;
    }

    function resolveWrapperPath(a_template){
      a_template = resolveTemplatePath(a_template);
      let arr = a_template.split("+");
      arr[0] = arr[0].substr(arr[0].length - ".tmpl".length, ".tmpl".length) == ".tmpl" ? arr[0].substr(0, arr[0].length - ".tmpl".length) : arr[0];
      return arr[0] + (arr[1] ? "+" + arr[1] : "") + ".wrapper.js"
    }

    async function getTemplateInfo(a_template){
      let template = resolveTemplatePath(a_template);
      if (fcf.NDetails.renderInstructions[template])
        return fcf.NDetails.renderInstructions[template];
      let wrapper = resolveWrapperPath(a_template);
      await fcf.require([wrapper]);
      return fcf.NDetails.renderInstructions[template];
    }

    function appendChildInfo(a_dstArgs, a_cp, a_chlidInfo){
      if (!a_chlidInfo || !a_chlidInfo.childs)
        return;
      if (!a_cp)
        return;
      if (a_chlidInfo.childs[a_cp]) {
        fcf.append(a_dstArgs, a_chlidInfo.childs[a_cp].args);
        a_dstArgs.fcfChildsArgs = a_chlidInfo.childs[a_cp];
      }
    };

    class TemplateRender{
      constructor(a_initializeOptions){
        this._buffer = [];
        this._callMap = {};
        this._initializeOptions = a_initializeOptions;
      }

      getTheme(){
        return fcf.application.getTheme();
      }

      write(a_content){
        this._buffer.push(a_content);
      }

      async build(){
        let self = this;
        let result = "";
        let map = {};
        for(let i = 0; i < this._buffer.length; ++i){
          if (this._buffer[i] instanceof Promise || this._buffer[i] instanceof fcf.Actions)
            result += fcf.str(await this._buffer[i]);
          else
            result += fcf.str(this._buffer[i]);
        }
        return result;
      }

      template(a_options, a_args, a_callPosition){
        let self = this;
        if (typeof a_options === "string"){
          a_options = {
            template: a_options,
            args: a_args,
          };
        }
        if (typeof a_args === "number")
          a_callPosition = a_args;

        let cp      = this._getCallPosition(a_callPosition);
        let externalArgs = {};
        this._fillExternalArgs(externalArgs, this.id, a_options.args);

        return fcf.actions().then(async ()=>{
          let templatePath = a_options.template;
          if (templatePath[0] == "+"){
            let fcftemplate = resolveTemplatePath(self._initializeOptions.template).split("+")[0];
            templatePath = fcftemplate + templatePath;
          }

          a_options.args.fcfCP = cp;
          a_options.args.fcfParent = self._initializeOptions.args.fcfId;

          appendChildInfo(a_options.args, cp, self._initializeOptions.args.fcfChildsArgs);

          let templateInfo = await getTemplateInfo(templatePath);

          let ri = fcf.NDetails.renderInstructions[self._initializeOptions.template]
          if (ri && ri.hooks && ri.hooks.hookRender){
            await ri.hooks.hookRender({
                args:            self._initializeOptions.args,
                renderedOptions: templateInfo.options,
                renderedArgs:    a_options.args,
                renderedPath:    templatePath,
              });
          }

          let template = await fcf.application.render({
            template:     templatePath,
            parent:       self._initializeOptions.fcfId,
            args:         a_options.args,
            externalArgs: externalArgs,
            secondary:    true,
            state:        self._initializeOptions.state,
          });

          if (!templateInfo.options.clientRendering){
            fcf.append(self._initializeOptions.state.args, template.state.args)
            fcf.append(self._initializeOptions.state.include, template.state.include)
            fcf.append(self._initializeOptions.state.originSourcesKeys, template.state.originSourcesKeys)
            fcf.append(self._initializeOptions.state.header, template.state.header);
            fcf.append(self._initializeOptions.state.include, template.state.include);
            fcf.append(self._initializeOptions.state.keywords, template.state.keywords);
            if (template.state.title)
              self._initializeOptions.state.title = template.state.title;
            fcf.append(self._initializeOptions.state.resets, template.state.resets);
            fcf.append(self._initializeOptions.state.sources, template.state.sources);
          }
          return template.content;
        });
      }
      view(a_options, a_callPosition){
        let self = this;
        let cp   = this._getCallPosition(a_callPosition)

        return fcf.actions().then(async ()=>{
          let args = fcf.append({}, a_options.args, { fcfCP: cp, fcfParent: self._initializeOptions.args.fcfId });

          let mode = a_options.mode ?                         a_options.mode :
                     a_options.view && a_options.view.mode ?  a_options.view.mode :
                                                              "read";
          let view = fcf.buildView(a_options.view, mode);

          for(let key in view)
            if (key != "args" && key != "template")
              args[key] = view[key];

          fcf.append(args, view.args);
          fcf.append(args, a_options.args);

          if (view.alias)
            args.fcfAlias = view.alias;

          if ("value" in a_options){
            args.value = a_options.value;
          } else if ("value" in a_options.view) {
            args.value = a_options.view.value;
          } else if (a_options.args && "value" in a_options.args) {
            args.value = a_options.args.value;
          } else if (!fcf.isArg(a_options.record) && args.fcfAlias in a_options.record) {
            args.value = a_options.record[args.fcfAlias];
          } else if (fcf.isArg(a_options.record) && a_options.record.type == "reference" && args.fcfAlias) {
            args.value = fcf.clone(a_options.record);
            args.value.arg += `[${JSON.stringify(args.fcfAlias)}]`;
          }


          if (!args.fcfId)
            args.fcfId = fcf.genId();

          if (mode.split(".")[0] == "add" && view.default !== undefined) {
            if (args.value == undefined){
              args.value = view.default;
              args.defaultFlag = true;
            } else if (fcf.isArg(args.value) && args.value.type == "reference"){
              let id    = args.value.object.id;
              let arg   = args.value.arg;
              let ptr   = fcf.resolveEx(self._initializeOptions.state.args[id], arg, true);
              if (ptr.object[ptr.key] === undefined) {
                ptr.object[ptr.key] = view.default;
                ptr.object[ptr.key].defaultFlag = true;
              }
            }
          }

          let templatePath  = fcf.application.getTheme().getTemplate(mode, a_options.view).template;

          if (!args.mode)
            args.mode = mode;

          for(let key in args){
            if (fcf.isArg(args[key]) && args[key].type == "recordreference"){
              if (fcf.isArg(a_options.record) && a_options.record.type == "reference") {
                args[key].type   = "reference";
                args[key].object = a_options.record.object;
                args[key].arg    = a_options.record.arg + `[${JSON.stringify(args[key].arg)}]`;
              }
            }
          }

          let externalArgs = {};

          let templateInfo = await getTemplateInfo(templatePath);
          self._fillExternalArgs(externalArgs, self._initializeOptions.args.fcfId, args);

          appendChildInfo(args, cp, self._initializeOptions.args.fcfChildsArgs);

          let ri = fcf.NDetails.renderInstructions[self._initializeOptions.template]
          if (ri && ri.hooks && ri.hooks.hookRender){
            await ri.hooks.hookRender({
                args:            self._initializeOptions,
                renderedOptions: templateInfo.options,
                renderedArgs:    args,
                renderedPath:    templatePath,
              });
          }

          let template = await fcf.application.render({
            template:     templatePath,
            args:         args,
            externalArgs: externalArgs,
            state:        self._initializeOptions.state,
            secondary:    true,
          });

          fcf.append(self._initializeOptions.state.include, fcf.map(templateInfo.options.clientInclude, (k,v)=>{ return [v,v]}));
          fcf.append(self._initializeOptions.state.include, fcf.map(templateInfo.options.include, (k,v)=>{ return [v,v]}));

          if (!templateInfo.options.clientRendering){
            fcf.append(self._initializeOptions.state.args, template.state.args)
            fcf.append(self._initializeOptions.state.include, template.state.include)
            fcf.append(self._initializeOptions.state.originSourcesKeys, template.state.originSourcesKeys)
            fcf.append(self._initializeOptions.state.header, template.state.header);
            fcf.append(self._initializeOptions.state.include, template.state.include);
            fcf.append(self._initializeOptions.state.keywords, template.state.keywords);
            if (template.state.title)
              self._initializeOptions.state.title = template.state.title;
            fcf.append(self._initializeOptions.state.resets, template.state.resets);
            fcf.append(self._initializeOptions.state.sources, template.state.sources);
          }

          return template.content;
        });
      }

      _getCallPosition(a_callPosition){
        var cp = "render:"+a_callPosition;
        if (!(cp in this._callMap))
          this._callMap[cp] = 0;
        cp += "-" + (++this._callMap[cp]);
        return cp;
      }

      _fillExternalArgs(a_dst, a_ownerId, a_args){
        if (typeof a_args != "object")
          return;
        if (fcf.isArg(a_args) && a_args.type=="reference") {
          let tokenizeId  = a_args.object.id.indexOf("{{") != -1;
          let tokenizeArg = a_args.arg.indexOf("{{") != -1;
          let refId = a_args.object.id;
          let refArg = a_args.arg;
          if (tokenizeId || tokenizeArg) {
            let parentArgs  = a_args.fcfParent && this._initializeOptions.state.args[a_args.fcfParent] ? a_initializeOptions.state.args[a_args.fcfParent] :
                              a_args.fcfParent && fcf.getWrapper(a_args.fcfParent)                 ? fcf.getWrapper(a_args.fcfParent).getArgs() :
                                                                                                     {};
            refId   = tokenizeId ? fcf.tokenize(a_args.object.id, {args: this._initializeOptions.args, parent: parentArgs}) : a_args.object.id;
            refArg  = tokenizeArg ? fcf.tokenize(a_args.arg, {args: this._initializeOptions.args, parent: parentArgs}) : a_args.arg;
          }
          if (!a_dst[refId])
            a_dst[refId] = {};
          let refDst = fcf.resolveEx(a_dst[refId], refArg, true);
          let refArgs = this._initializeOptions.state.args[refId] ? this._initializeOptions.state.args[refId] :
                        fcf.getWrapper(refId)                 ? fcf.getWrapper(refId).getArgs() :
                                                                undefined;
          if (refArgs !== undefined)
            refDst.object[refDst.key] = fcf.resolve(refArgs, refArg);
        } else if (Array.isArray(a_args)) {
          for(let i = 0; i < a_args.length; ++i)
            this._fillExternalArgs(a_dst, a_ownerId, a_args[i]);
        } else {
          for(let k in a_args)
            this._fillExternalArgs(a_dst, a_ownerId, a_args[k]);
        }
      }

    }

    class Render {
      constructor(){
        this._argBuilder = new ArgsBuilder();
      }

      render(a_options){
        a_options.template = resolveTemplatePath(a_options.template);

        let self = this;
        let isRootCall       = !a_options.state;
        let state;
        let renderInscrtutions = fcf.NDetails.renderInstructions[a_options.template]
        let fcfId;
        let resultArgs;
        let resultSources;
        let resultConetent;

        return fcf.actions()
        .then(async ()=>{
          if (!renderInscrtutions){
            let template = a_options.template[0] == "@" ? fcf.application.getTheme().getAliases()[a_options.template.substr(1)] : a_options.template;
            let templArr = template.split("+");
            let block    = templArr[1];
            let templateShort = templArr[0];
            let wrapper  = templateShort.substr(0, templateShort.length - ".tmpl".length) + (block ? "+" + block : "") + ".wrapper.js";
            await fcf.require([wrapper]);
            renderInscrtutions = fcf.NDetails.renderInstructions[template];
          }

          let args = a_options.args ? a_options.args : {};
          fcfId = args.fcfId                                       ? args.fcfId :
                 !args.fcfId && renderInscrtutions.arguments.fcfId ? renderInscrtutions.arguments.fcfId :
                                                                     fcf.genId();

          state  = a_options.state
                  ? a_options.state
                  : {
                      id:                 fcfId,
                      root:               {id: fcf.application.getRootWrapper().getId()},
                      theme:              fcf.application.getTheme(),
                      defaultTheme:       undefined,
                      themes:             {},
                      include:            {},
                      renderRestore:      {},
                      renderStorage:      {},
                      reqursionCounter:   0,
                      args:               {},
                      sources:            {},
                      resets:             {},
                      originSourcesKeys:  {},
                      page:               {
                                            header: {
                                              include:  [],
                                              title:    "",
                                              keywords: [],
                                              header:   []
                                            }
                                          }
                   };

          if (renderInscrtutions.options && renderInscrtutions.options.include){
            let include = Array.isArray(renderInscrtutions.options.include) ? renderInscrtutions.options.include : [renderInscrtutions.options.include];
            for(let i = 0; i < include.length; ++i)
              state.include[include[i]] = include[i];
          }
          if (renderInscrtutions.options && renderInscrtutions.options.clientInclude){
            let include = Array.isArray(renderInscrtutions.options.clientInclude) ? renderInscrtutions.options.clientInclude : [renderInscrtutions.options.clientInclude];
            for(let i = 0; i < include.length; ++i)
              state.include[include[i]] = include[i];
          }
        })
        .then((a_res, a_act)=>{
          self._argBuilder.build({
            id:           fcfId,
            context:      fcf.getContext(),
            state:        state,
            reqursion:    !!a_options.reqursion,
            templatePath: a_options.template,
            route:        fcf.getContext().route,
            args:         renderInscrtutions.arguments,
            template:     renderInscrtutions,
            inputArgs:    a_options.args,
            onResult:     function (a_error, a_args, a_sources) {
              if (a_error) {
                a_act.error(a_error);
                return;
              }
              resultArgs = a_args;
              resultSources = a_sources;
              a_act.complete();
            }
          });
        })
        .then(async ()=>{
          let templateRender = new TemplateRender({template: a_options.template, state: state, args: resultArgs});
          let lct = fcf.getContext().currentTemplate;
          fcf.getContext().currentTemplate = {id: fcfId};
          let libFCF = {};
          libFCF.__proto__ = fcf;
          libFCF.argRef = function(){
            if (arguments.length == 1)
              return fcf.argRef(fcfId, arguments[0]);
            else
              return fcf.argRef(arguments[0], arguments[1]);
          }
          await renderInscrtutions.renderFunction({
            decor:  fcf.application.getTheme().getDecor(),
            route:  fcf.getContext().route,
            args:   resultArgs,
            render: templateRender,
            fcf:    libFCF,
          });
          fcf.getContext().currentTemplate = lct;
          resultConetent = await templateRender.build()
        })
        .then(()=>{
          var content = "<div ";
          content += "fcftemplate=\"" + state.theme.resolveAlias(a_options.template) + "\" ";
          content += " id='" + resultArgs.fcfId + "' ";
          content += " class=' fcfwrapper " + fcf.str(resultArgs.fcfClassInner) + " " + fcf.str(resultArgs.fcfClass) + "' "
          content += resultArgs.fcfEvntid ? " fcfevntid='" + resultArgs.fcfEvntid + "'" : "";

          if (resultArgs.fcfStyle || resultArgs.fcfStyleInner)
            content += " style='" + fcf.str(resultArgs.fcfStyleInner) + ";" + fcf.str(resultArgs.fcfStyle) + "' ";
          if (typeof resultArgs.fcfAttributes == "object"){
            for(var k in resultArgs.fcfAttributes)
              content += " " + k + "=\"" + resultArgs.fcfAttributes[k] + "\" ";
          }
          if (resultArgs.fcfParent)
            content += "fcfparent=\"" + resultArgs.fcfParent + "\" ";
          if (resultArgs.fcfAlias)
            content += "fcfalias=\"" + resultArgs.fcfAlias + "\" ";
          content +=">" + resultConetent + "</div>";
          return content;
        })
        .then((a_content)=>{
          var resultTemplate = new Template({
                                    content: a_content,
                                    id:      resultArgs.fcfId,
                                    args:    resultArgs,
                                    state: {
                                      args:              state.args,
                                      sources:           state.sources,
                                      originSourcesKeys: state.originSourcesKeys,
                                      include:           state.include,
                                      resets:            state.resets,
                                      page:              state.page,
                                    }
                                  });

          if (!resultTemplate.args.fcfError){
            a_options.onResult(undefined, resultTemplate);
          } else {
            a_options.onResult(resultTemplate.args.fcfError, resultTemplate);
            throw resultTemplate.args.fcfError;
          }
        })
        .catch(function(a_error){
          fcf.log.err("FCF:RENDER", a_error);
          a_options.onResult(a_error, new Template({}));
        });


      }
    };

    Namespace.Render = Render;

    return Namespace.Render;
  }
});
