/**
* @namespace fcf::NRender
*lng_en @brief Rendering namespace
*lng_ru @brief Пространство имен рендеринга
**/


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

    NRender.Render = function(a_initializeOptions) {
      var self = this;

      this._options = fcf.append({}, a_initializeOptions);
      this._options.template = this._options.template;

      this._loader = new Loader();

      this.setFileСaching = function(a_value){
        this._options.fileСaching = a_value;
        this._loader.setSettings({fileСaching: a_value});
      }

      /*
      * @fn void render(object a_options)
      * @brief Выполняет рендеринг шаблона
      * @param object a_options
      *     - string template
      *     - object args [optional]
      *     - bool   update [optional] флаг обновления повторной отрисовки на клиенте
      *     - object state             внутренняя информация рекурсивного рендеринга
      *     - function onResult(a_error, a_template)
      **/
      this.render = function(a_options) {
        let oldContext = fcf.getContext();
        if (a_options.context)
          fcf.setContext(a_options.context);

        if (a_options.args){
          let fcfReferences = this._getReferences(a_options.args);
          if (!fcf.empty(fcfReferences))
            a_options.args.fcfReferences = fcfReferences;
        }

        if (a_options.template[0] == ":")
          a_options.template = a_options.template.substr(1);

        var argsBuilder       = new ArgsBuilder({});
        var templateProcessor = new TemplateProcessor();

        var fcfId        = typeof a_options.args === "object" && a_options.args.fcfId ? a_options.args.fcfId :
                                                                                        fcf.id();
        fcf.getContext().currentTemplate = { id: fcfId };
        var needOnServer = !fcf.isServer() && fcf.getContext().get("needBabel");
        needOnServer = !fcf.isServer();
        var part = a_options.template.split("+")[1] ? a_options.template.split("+")[1] : "";
        var resultContext = "";
        var fullTemplate = undefined;
        var rawTemplate = undefined;
        var resultArgs = {};
        var resultSources = {};
        var srcArgs = {};
        var taskInfo = undefined;
        var defaultTheme     = fcf.application.getTheme(a_options.theme , true);
        var externalArgs     = typeof a_options.externalArgs == "object" ? a_options.externalArgs : {};
        var isExternalEmpty = true;
        for(let k in externalArgs){
          externalArgs[k].__innerFcfPostRemove = true;
          isExternalEmpty = false;
        }
        var isRootCall       = !a_options.state;
        var state            = a_options.state ? a_options.state : {
                                                                      id:                 fcfId,
                                                                      root:               {id: ""},
                                                                      theme:              defaultTheme,
                                                                      defaultTheme:       defaultTheme,
                                                                      themes:             {},
                                                                      include:            {},
                                                                      reqursionCounter:   0,
                                                                      args:               externalArgs,
                                                                      sources:            {},
                                                                      resets:             {},
                                                                      originSourcesKeys:  {},
                                                                      cpMap:              {},
                                                                      page:               {
                                                                                            header: {
                                                                                              title:        a_options.pageHeader && a_options.pageHeader.title ?  a_options.pageHeader.title :
                                                                                                            a_options.route && a_options.route.title           ?  a_options.route.title :
                                                                                                                                                                  "",
                                                                                              description:  a_options.pageHeader && a_options.pageHeader.description ? a_options.pageHeader.description :
                                                                                                            a_options.route && a_options.route.description           ? a_options.route.description :
                                                                                                                                                                       "",
                                                                                              include:      [],
                                                                                              keywords:     [],
                                                                                              header:       []
                                                                                            }
                                                                                          },
                                                                    };
        if (a_options.root)
          state.root = { template: a_options.template, id: fcfId };

        var action = new fcf.Actions();

        var theme = undefined;

        //loading template
        action.then(function(a_res, a_act) {
          if (needOnServer){
            a_act.complete();
            return;
          }

          theme = state.theme;
          self._loader.load(a_options.template, state, fcfId, function(a_error, a_fullTemplate) {
            if (a_error) {
              a_act.error(a_error);
              return;
            }

            fullTemplate = a_fullTemplate;
            rawTemplate  = fullTemplate.templates[part];

            for(let i = 0; i < rawTemplate.options.include.length; ++i)
              state.include[rawTemplate.options.include[i]] = rawTemplate.options.include[i];
            for(let i = 0; i < rawTemplate.options.clientInclude.length; ++i)
              state.include[rawTemplate.options.clientInclude[i]] = rawTemplate.options.clientInclude[i];

            if (!rawTemplate){
              a_act.error(new fcf.Exception("ERROR_RENDER_TEMPLATE_NOT_FOUND", {template: a_options.template}));
              return;
            }

            a_act.complete();
          });
        });

        //build arguments
        action.then(function(a_result, a_act) {
          if ("fcfId" in rawTemplate.arguments){
            fcfId      = rawTemplate.arguments.fcfId;
            fcf.getContext().currentTemplate.id = fcfId;
            if (a_options.root)
              state.root.id = fcfId;
          }

          var args = typeof a_options.args == "object" ? a_options.args : {};
          argsBuilder.build({
            id:           fcfId,
            context:      a_options.context ? a_options.context : fcf.getContext(),
            state:        state,
            reqursion:    a_options.reqursion,
            templatePath: a_options.template,
            route:        a_options.route ? a_options.route : fcf.getContext().route,
            args:         rawTemplate.arguments,
            template:     rawTemplate,
            inputArgs:    args,
            onResult:     function (a_error, a_args, a_sources, a_taskInfo) {
              if (a_error) {
                a_act.error(a_error);
                return;
              }
              resultArgs = a_args;
              resultSources = a_sources;
              taskInfo = a_taskInfo;
              a_act.complete();
            }
          });

        });

        //build template
        action.then(function(a_result, a_act) {
          var params = {
            options:          fullTemplate.options,
            context:          a_options.context ? a_options.context : fcf.getContext(),
            template:         a_options.template,
            rawTemplate:      rawTemplate,
            route:            a_options.route ? a_options.route : fcf.getContext().route,
            args:             resultArgs,
            sources:          resultSources,
            state:            state,
            onResult:         function(a_error, a_content) {
              if (a_error) {
                a_act.error(a_error)
                return;
              }
              resultContext = a_content;
              a_act.complete();
            },
          };
          templateProcessor.build(params);
        });

        //create result template
        action.then(async function(a_result, a_act) {
          if (isRootCall) {
            for(let id in state.args) {
              if (!isExternalEmpty && state.args[id].__innerFcfPostRemove){
                delete state.args[id];
                continue;
              }

              if ("fcfWrapper" in state.args[id] && !state.args[id].fcfWrapper){
                if (!state.args[id].fcfBR) {
                  delete state.args[id];
                  delete state.sources[id];
                  delete state.originSourcesKeys[id];
                  delete state.resets[id];
                  continue;
                } else {
                  let objArgs    = {fcfId: state.args[id].fcfId, fcfParent: state.args[id].fcfParent, fcfBR: state.args[id].fcfBR, fcfWrapper: false};
                  let objSources = {fcfId: state.args[id].fcfId, fcfParent: state.args[id].fcfParent, fcfBR: state.args[id].fcfBR, fcfWrapper: false};
                  for(let arg in objArgs.fcfBR) {
                    objArgs[arg]    = state.args[id][arg];
                    objSources[arg] = state.sources[id][arg];
                  }
                  let objOSK = [];
                  for(let arg in objArgs) {
                    if (fcf.find(state.originSourcesKeys[id], arg) !== undefined)
                      objOSK.push(arg);
                  }
                  state.args[id]              = objArgs;
                  state.sources[id]           = objSources;
                  state.originSourcesKeys[id] = objOSK;
                  delete state.resets[id];
                }
              }

              for(let key in state.args[id]) {
                if (fcf.isArg(state.sources[id][key]) && (state.sources[id][key].type === "reference" || state.sources[id][key].type === "template" || state.sources[id][key].ignore)) {
                  delete state.args[id][key];
                }
              }
            }
          }

          delete resultArgs.fcfChildsArgs;

          if (rawTemplate.hooks.hookAfterRender)
            await rawTemplate.hooks.hookAfterRender(taskInfo);


          var resultTemplate = new Template({
                                    content: resultContext,
                                    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){
            if (a_options.onResult)
              a_options.onResult(undefined, resultTemplate);

            a_act.complete(resultTemplate);
          } else {
            a_act.error(resultTemplate.args.fcfError);
          }
        });

        action.catch(function(a_error){
          if (a_options.onResult)
            a_options.onResult(a_error, new Template({}));
        });

        action.finally(()=>{
          fcf.setContext(oldContext);
        })

        return action;
      }

      this.getLoader = function(){
        return this._loader;
      }

      this._getReferences = function(a_args){
        let map = {};

        for(let key in a_args){
          if (key == "fcfChildsArgs")
            continue;
          this._fillReferences(a_args[key], map);
        }
        let result = [];
        for(let k in map)
          result.push(map[k]);

        return result;
      }

      this._fillReferences = function(a_arg, a_dst){
        if (typeof a_arg == "object") {
          if (fcf.isArg(a_arg) && a_arg.type == "reference"){
            a_dst[a_arg.object.id + "->" + a_arg.arg] = [a_arg.object.id, a_arg.arg];
          } else {
            if (Array.isArray(a_arg)){
              for(let i = 0; i < a_arg.length; ++i)
                this._fillReferences(a_arg[i], a_dst);
            } else {
              for(let k in a_arg)
                this._fillReferences(a_arg[k], a_dst);
            }
          }
        }
      }

    }

    return NRender.Render;
  }
});
