fcf.module({
  name: "fcf:NClient/Application.js",
  dependencies: [ "fcf:NSystem/NPackage/Theme.js",
                  "fcf:NSystem/Configuration.js",
                  "fcf:NClient/LocalData.js",
                  "fcf:NClient/Render.js",
                ],
  module: function(Theme, Configuration, LocalData, ClientRender) {
    var NClient = fcf.prepareObject(fcf, "NClient");

    function getWindowScrollY(){
      var supportPageOffset = window.pageXOffset !== undefined;
      var isCSS1Compat = ((document.compatMode || "") === "CSS1Compat");
      return supportPageOffset ? window.pageYOffset : isCSS1Compat ? document.documentElement.scrollTop : document.body.scrollTop;
    }

    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;
    }

    window.addEventListener('popstate', function(event) {
      if (event.cancelable)
        return;
      if (event.state && typeof event.state == "object" && event.state.fcfSetLocation){
        fcf.application._setLocation(document.location.href, false, event.state.fcfScrollY);
      } else {
        document.location.reload();
      }
    });

    function discardHost(a_url){
      let pos = a_url.indexOf("://");
      if (pos == -1)
        return a_url;
      pos = a_url.indexOf("/", pos+3);
      if (pos == -1)
        return "";
      return a_url.substr(pos);
    }

    NClient.Application = class Application {

      constructor(){
        this._configuration   = new Configuration();
        this._themes          = {};
        this._localData       = new LocalData();
        this._lastURL         = window.location.href;
        this._eventChannel    = fcf.NDetails.eventChannel;
        this._isAvailable     = false;
        this._isRun           = false;
        this._render          = new ClientRender();
        this._eventChannel.setOwner(this);

      }

      initialize() {
        this._systemWrapper = new fcf.NClient.Wrapper({}, true);
        this._themes[this._configuration.defaultTheme] = new Theme(this._configuration.defaultTheme, this._configuration);
        this._isAvailable = true;
      }

      isAvailable(){
        return this._isAvailable;
      }

      isRun(){
        return this._isRun;
      }

      getRender(){
        return this._render;
      }

      render(a_options) {
        let self = this;
        let parent = a_options.parent;
        let owner = a_options.owner;
        if (parent === undefined && owner === undefined) {
          owner = false;
        } else if (typeof owner === "object" && typeof owner._id == "string" && fcf.getWrapper(owner._id)) {
          parent = owner._id;
          owner  = owner.getDomElement();
        } else if (typeof owner === "string" && fcf.getWrapper(owner)) {
          parent = fcf.getWrapper(owner).getId();
          owner  = fcf.getWrapper(owner).getDomElement();
        } else if (typeof parent === "object" && typeof parent._id == "string" && fcf.getWrapper(parent._id)) {
          if (!owner)
            owner  = parent.getDomElement();
          parent = parent._id;
        } else if (typeof parent === "string" && fcf.getWrapper(parent)) {
          if (!owner)
            owner  = fcf.getWrapper(parent).getDomElement();
        }

        a_options.template = resolveTemplatePath(a_options.template);

        return fcf.actions()
        .then(async function(a_res, a_act){

          function clCompleteRender(a_error, a_template, a_clientRendering){
            if (a_error) {
              fcf.log.err("FCF", a_error.message, a_error);
              if (a_options.onResult)
                a_options.onResult(a_error, a_template);
              a_act.error(a_error);
              return;
            }


            if (!a_options.secondary) {
              for(var id in a_template.state.args){
                let args    = a_template.state.args[id];
                let sources    = a_template.state.sources[id];
                let originSourcesKeys = a_template.state.originSourcesKeys[id];
                delete args.fcfChildsArgs;
                delete sources.fcfChildsArgs;

                if ("fcfWrapper" in args && !args.fcfWrapper){
                  let parentId = args.fcfParent;
                  let foundParent = false;
                  while(!!parentId) {
                    if (a_template.state.args[parentId]){
                      if (!("fcfWrapper" in a_template.state.args[parentId]) || a_template.state.args[parentId].fcfWrapper){
                        foundParent = true;
                        break;
                      } else {
                        parentId = a_template.state.args[parentId].fcfParent;
                      }
                    } else {
                      let obj = fcf.application.getLocalData().getOriginObject(parentId);
                      if (obj && (!("fcfWrapper" in obj) || obj.fcfWrapper)){
                        foundParent = true;
                        break;
                      }
                    }
                  };
                  if (foundParent) {
                    if (!fcf.NDetails._noWrapperChilds[parentId])
                      fcf.NDetails._noWrapperChilds[parentId] = {};
                    fcf.NDetails._noWrapperChilds[parentId][id] = fcf.NDetails._attachWrapperCounter[parentId] ? fcf.NDetails._attachWrapperCounter[parentId] : 0;
                  } else {
                    fcf.log.wrn("FCF:RENDER", "Memory leak! Using no wrapper template without parent")
                  }
                }
                for(let key in sources) {
                  if (fcf.isArg(sources[key]) && sources[key].type === "reference")
                    delete args[key];
                }
                fcf.application.getLocalData().setObject(id, args);
                fcf.application.getLocalData().setSourceObject(id, sources);
                fcf.application.getLocalData().setOriginSourcesKeys(id, originSourcesKeys);
              }

              function clCreate() {
                if (owner !== false) {
                  owner = typeof owner == "string"    ? fcf.select(owner)[0] :
                                 owner === undefined  ? document.body :
                                                        owner;
                  let div = document.createElement("div");
                  if ("fcfWrapper" in a_template.args && !a_template.args.fcfWrapper) {
                    div.innerHTML = a_template.content;
                    a_template.domElements = [];
                    for(var i = 0; i < div.children.length; ++i) {
                      var el = div.children[i];
                      a_template.domElements.push(el);
                      if (owner)
                        owner.appendChild(el);
                    }
                  } else {
                    div.innerHTML = a_template.content;
                    a_template.domElement = div.firstChild;
                    a_template.domElements = [a_template.domElement];
                    if (owner)
                      owner.appendChild(a_template.domElement);
                  }

                  fcf.each(fcf.select(div, "script"), (a_key, a_script)=>{
                    eval(a_script.innerHTML);
                  });
                }

                var enableWrapper = a_options.wrapper !== undefined ? a_options.wrapper : true;

                if (enableWrapper && (!("fcfWrapper" in a_template.args) || a_template.args.fcfWrapper) && a_template.domElement) {
                  fcf.liven(a_template.domElement, function() {
                    a_template.wrapper = fcf.NDetails._wrappers[a_template.id];
                    if (a_options.onResult)
                      a_options.onResult(a_error, a_template);
                    a_act.complete(a_template);
                  });
                } else {
                  if (a_options.onResult)
                    a_options.onResult(a_error, a_template);
                  a_act.complete(a_template);
                }
              }
              if (!fcf.empty(a_template.state.include)){
                fcf.include(fcf.array(a_template.state.include, (k)=>{return k}), ()=>{
                  if (!fcf.empty(a_template.state.page.include)){
                    fcf.include(fcf.array(a_template.state.page.include, (k)=>{return k}), clCreate);
                  } else {
                    clCreate();
                  }
                });
              } else {
                clCreate();
              }
            } else {
              if (!a_clientRendering && a_options.state){
                fcf.append(a_options.state.args, a_template.state.args);
                fcf.append(a_options.state.include, a_template.state.include);
                fcf.append(a_options.state.originSourcesKeys, a_template.state.originSourcesKeys);
                fcf.append(a_options.state.page.header, a_template.state.page.header);
                fcf.append(a_options.state.page.include, a_template.state.page.include);
                fcf.append(a_options.state.sources, a_template.state.sources);
              }
              if (a_options.onResult)
                a_options.onResult(a_error, a_template);
              a_act.complete(a_template);
            }
          }

          let renderInscrtutions = fcf.NDetails.renderInstructions[a_options.template];
          if (!renderInscrtutions){
            let template = a_options.template[0] == "@" ? self.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";
            try {
              await fcf.require([wrapper]);
              renderInscrtutions = fcf.NDetails.renderInstructions[template];
            } catch(e){
            }
          }
          if (renderInscrtutions && renderInscrtutions.renderFunction && !renderInscrtutions.hooks){
            let template = a_options.template[0] == "@" ? self.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";
            try {
              await fcf.require([wrapper+"---cri.js"]);
            }catch(e){
              fcf.log.err(`Can't get hooks info for rendering. Template: ${a_options.template}`)
            }
          }

          let clientRendering = renderInscrtutions && renderInscrtutions.options && "clientRendering" in renderInscrtutions.options ? renderInscrtutions.options.clientRendering : undefined;
          if (clientRendering) {
            if (a_options.args && "fcfClientRendering" in a_options.args)
              clientRendering = a_options.args.fcfClientRendering;
            else if (renderInscrtutions.arguments && "fcfClientRendering" in renderInscrtutions.arguments)
              clientRendering = renderInscrtutions.arguments.fcfClientRendering;
          }
          let updateMode = a_options.args && a_options.args.fcfId && !!fcf.getWrapper(a_options.args.fcfId);

          if (clientRendering === true || clientRendering === "all" || (updateMode && (clientRendering === "update" || clientRendering === "update_np"))) {
            let template = a_options.template.split("+")[0][0] == "@" ? self.getTheme().getAliases()[a_options.template.substr(1)] : a_options.template;
            let args = fcf.append({}, a_options.args);
            if (parent)
              args.fcfParent = parent;
            args.fcfId = args.fcfId                                       ? args.fcfId :
                        !args.fcfId && renderInscrtutions.arguments.fcfId ? renderInscrtutions.arguments.fcfId :
                                                                            fcf.id();
            for(let key in args)
              if (fcf.isArg(args[key]) && args[key].type == "reference" && !args[key].object.id)
                args[key].object.id = args.fcfId;

            self._render.render({
              template:     a_options.template,
              args:         args,
              state:        a_options.state,
              url:          a_options.url || window.location.href,
              secondary:    a_options.secondary,
              onResult:     function(a_error, a_result) {
                clCompleteRender(a_error, a_result, true);
              }
            });
          } else {
            let args = {};
            if (parent)
              args.fcfParent = parent;
            fcf.append(args, a_options.args);


            let externalArgs = typeof a_options.externalArgs == "object" ? a_options.externalArgs : {};
            function fillExternalArgs(a_object) {
              if (fcf.isArg(a_object) && a_object.type == "reference"){
                let wrp = fcf.getWrapper(a_object.object.id);
                if (wrp){
                  let value = wrp.getArg(a_object.arg);
                  if (!(a_object.object.id in externalArgs))
                    externalArgs[a_object.object.id] = {};
                  let arg = a_object.arg;
                  if (arg.indexOf("{{") != -1){
                    let wrpArgs = wrp.getArgs();
                    let prtArgs = wrp.getParent() ? wrp.getParent().getArgs() : {};
                    arg = fcf.tokenize(arg, {args: wrpArgs, parent: prtArgs});
                  }
                  let ptr = fcf.resolveEx(externalArgs[a_object.object.id], arg, true);
                  ptr.object[ptr.key] = value;
                }
              } else if (typeof a_object === "object"){
                fcf.each(a_object, (a_key, a_item)=>{
                  fillExternalArgs(a_item);
                });
              }
            }
            for(let key in args)
              if (fcf.isArg(args[key]))
                fillExternalArgs(args[key]);

            fcf.loadObject({
              path:  "@url:fcfRender",
              post: { template: a_options.template, args: args, url: a_options.url || window.location.href, externalArgs: externalArgs },
              onResult: function(a_error, a_response) {
                clCompleteRender(a_error, a_response, false);
              }
            });
          }
        })
        .then((a_template)=>{
          if (a_options.secondary || a_options.update || !a_template.domElement)
            return a_template;
          return fcf.application.getEventChannel().send("render", {element: a_template.domElement})
          .then(()=>{
            return a_template;
          })
        })
        .catch((a_error)=>{
          fcf.application.getEventChannel().send("error", {error: a_error});
        })
      }

      setSettings(a_options){
        if (typeof a_options == "string"){
          a_options = decodeURIComponent(a_options);
          a_options = eval("(" + a_options + ")");
        }
        if (a_options.defaultTheme)
          this.getConfiguration().defaultTheme = a_options.defaultTheme;

        if (a_options.renderStorage)
          this._localData.setOriginData(a_options.renderStorage);

        if (a_options.originSourcesKeys)
          this._localData.setOriginSourcesKeysData(a_options.originSourcesKeys);

        if (a_options.renderRestore)
          this._localData.setSourcesData(a_options.renderRestore);

        if (a_options.context !== undefined) {
          fcf.setContext(new fcf.Context(a_options.context));
          fcf.saveContext();
        }

        if (a_options.root !== undefined)
          this.getConfiguration().root = a_options.root;

        return this;
      }

      setLocation(a_url){
        return this._setLocation(a_url);
      }

      _setLocation(a_url, a_setUrl/* = true*/, a_scrollY){
        let self = this;

        a_setUrl = a_setUrl !== undefined ? a_setUrl : true;

        let anchor = a_url.split("#")[1];
        let routeInfo = undefined;
        let title     = undefined;
        let lastRouteInfo = fcf.clone(fcf.getContext().route);
        let scrollY = getWindowScrollY();

        if (anchor && a_url.split("#")[0] == lastRouteInfo.url.split("#")[0]){
          fcf.getContext().route.anchor = anchor;
          fcf.getContext().route.url = a_url;
          if (a_setUrl){
            let state = window.history.state;
            if (state && typeof state === "object" && state.fcfSetLocation){
              state = fcf.clone(state);
            } else {
              state = {fcfSetLocation: true};
            }
            state.fcfScrollY = scrollY;
            window.history.replaceState(state, document.title);
            window.history.pushState({fcfSetLocation: true}, undefined, discardHost(fcf.getContext().route.url));
          }
          let top = a_scrollY !== undefined ? a_scrollY : 0;
          if (a_scrollY === undefined) {
            let element = fcf.select(`a[name="${anchor}"]`)[0];
            if (element)
              top = element.offsetTop;
          }
          window.scroll(0, top);
          return;
        }

        function displayLocation(a_url, a_routeInfo, a_title, a_setUrl){
          if (a_routeInfo){
            fcf.getContext().route = a_routeInfo;
          } else {
            fcf.getContext().route = new fcf.RouteInfo(a_url);
            fcf.getContext().route.title = a_routeInfo.route.title;
          }
          if (a_title){
            fcf.getContext().route.title = a_title;
            if (fcf.select("head>title")[0]){
              fcf.select("head>title")[0].innerHTML = a_title;
            } else {
              let title = document.createElement("title");
              title.innerHTML = a_title;
              fcf.select("head")[0].appendChild(title);
            }
          }
          anchor = fcf.getContext().route.anchor;
          if (a_setUrl) {
            let state = window.history.state;
            if (state && typeof state === "object" && state.fcfSetLocation){
              state = fcf.clone(state);
            } else {
              state = {fcfSetLocation: true};
            }
            state.fcfScrollY = scrollY;
            window.history.replaceState(state, document.title);
            window.history.pushState({fcfSetLocation: true}, undefined, discardHost(fcf.getContext().route.url));
          }
        }

        return fcf.actions()
        .then(() => {
          return fcf.application.getEventChannel().send("set_location_before", { url: a_url });
        })
        .then(()=>{
          let elements = fcf.select("[fcftemplate]");
          let wrappers = [];
          for(let i = 0; i < elements.length; ++i) {
            let wrapper = fcf.getWrapper(elements[i]);
            if (!wrapper)
              continue;
            let foundUpdate   = false;
            let foundReload   = false;
            let fcfUpdate     = wrapper.getArg("fcfUpdate");
            let fcfReload     = wrapper.getArg("fcfReload");
            let fcfRouteArgs  = wrapper.getArg("fcfRouteArgs");
            function check(a_item) {
              let parts = fcf.parseObjectAddress(a_item);
              if (parts.length == 1 && parts[0] == "route")
                return true;
              if (parts.length == 2 && parts[0] == "route" && parts[1] == "url")
                return true;
              if (parts.length == 2 && parts[0] == "route" && parts[1] == "uri")
                return true;
              if (parts.length == 2 && parts[0] == "route" && parts[1] == "referer")
                return true;
              if (parts.length == 3 && parts[0] == "route" && parts[1] == "args")
                return true;
              return false;
            }
            if (Array.isArray(fcfReload)) {
              for(let i = 0; i < fcfReload.length; ++i) {
                if (check(fcfReload[i])){
                  foundReload = true;
                  break;
                }
              }
            }
            if (!foundReload && Array.isArray(fcfUpdate)) {
              for(let i = 0; i < fcfUpdate.length; ++i) {
                if (check(fcfUpdate[i])){
                  foundUpdate = true;
                  break;
                }
              }
            }
            if (Array.isArray(fcfRouteArgs) && !fcf.empty(fcfRouteArgs)) {
              foundUpdate = true;
            }
            if (!foundUpdate && !foundReload)
              continue;
            wrappers.push({wrapper: wrapper, reload: foundReload});
          }
          return fcf.actions()
          .then(async ()=>{
            if (fcf.empty(wrappers)){
              let routeInfo = await fcf.loadObject({ path:  "/fcfpackages/fcf/route", post: { url: a_url } });
              displayLocation(routeInfo.url, routeInfo, routeInfo.title, a_setUrl);
            }
          })
          .asyncEach(wrappers, (a_key, a_wrapperInfo)=>{
            return fcf.actions()
            .then(()=>{
              if (a_wrapperInfo.reload)
                return a_wrapperInfo.wrapper.reload({url: a_url});
              else
                return a_wrapperInfo.wrapper.update({url: a_url});
            })
            .then((a_updateInfo)=>{
              if (!routeInfo){
                routeInfo = a_updateInfo.route;
                if (routeInfo) {
                  a_url = routeInfo.url;
                  if (routeInfo.title)
                    title = routeInfo.title;
                }
                if (a_updateInfo.page.header.title)
                  title = a_updateInfo.page.header.title;
              }
            });
          })
        })
        .then(() => {
          displayLocation(a_url, routeInfo, title, a_setUrl);
          let top = a_scrollY !== undefined ? a_scrollY : 0;
          if (a_scrollY === undefined) {
            if (!fcf.empty(anchor)){
              let element = fcf.select(`a[name="${anchor}"]`)[0];
              if (element)
                top = element.offsetTop;
            }
          }
          window.scroll(0, top);
          return fcf.application.getEventChannel().send("set_location_after", { url: a_url });
        })
        .catch((a_error)=>{
          fcf.log.err("FCF", "Failed set route: ", a_url);
          window.location.href = a_url;
        })
      }

      setUrlArg(a_name, a_value, a_editor){
        return this._setUrlArg(a_name, a_value, a_editor);
      }

      _setUrlArg(a_name, a_value, a_editor){
        let self = this;
        let lastRouteInfo = fcf.clone(fcf.getContext().route);
        let route = fcf.getContext().route;
        if (a_value !== undefined) {
          route.args[a_name]    = a_value;
          route.urlArgs[a_name] = a_value;
        } else {
          delete route.args[a_name];
          delete route.urlArgs[a_name];
        }
        route.url             = fcf.buildUrl(route.referer, route.args);
        fcf.getContext().route = route;
        window.history.pushState(undefined, undefined, discardHost(route.url));

        let elements = fcf.select("[fcftemplate]");
        let wrappers = [];
        for(let i = 0; i < elements.length; ++i) {
          let wrapper = fcf.getWrapper(elements[i]);
          if (!wrapper)
            continue;
          if (wrapper === a_editor)
            continue;
          let foundUpdate   = false;
          let foundReload   = false;
          let fcfUpdate     = wrapper.getArg("fcfUpdate");
          let fcfReload     = wrapper.getArg("fcfReload");
          let fcfRouteArgs  = wrapper.getArg("fcfRouteArgs");
          function check(a_item) {
            let parts = fcf.parseObjectAddress(a_item);
            if (parts.length == 3 && parts[0] == "route" && parts[1] == "args" && lastRouteInfo.args[parts[2]] != route.args[parts[2]])
              return true;
            return false;
          }
          if (Array.isArray(fcfReload)) {
            for(let i = 0; i < fcfReload.length; ++i) {
              if (check(fcfReload[i])){
                foundReload = true;
                break;
              }
            }
          }
          if (!foundReload && Array.isArray(fcfUpdate)) {
            for(let i = 0; i < fcfUpdate.length; ++i) {
              if (check(fcfUpdate[i])){
                foundUpdate = true;
                break;
              }
            }
          }
          if (Array.isArray(fcfRouteArgs)){
            for(let i = 0; i < fcfRouteArgs.length; ++i){
              let srcArg = fcf.application.getLocalData().getSourceItem(wrapper.getId(), fcfRouteArgs[i]);
              if (lastRouteInfo.args[srcArg.arg] != route.args[srcArg.arg]){
                wrapper.setArg(fcfRouteArgs[i],
                               route.args[srcArg.arg],
                               false,
                               false,
                               false,
                               self._systemWrapper,
                               true);
              }
            }
          }
          if (!foundUpdate && !foundReload)
            continue;
          wrappers.push({wrapper: wrapper, reload: foundReload});
        }

        return fcf.actions()
        .asyncEach(wrappers, (a_key, a_wrapperInfo)=>{
          return fcf.actions()
          .then(()=>{
            if (a_wrapperInfo.reload)
              return a_wrapperInfo.wrapper.reload();
            else
              return a_wrapperInfo.wrapper.update();
          })
          .then((a_updateInfo)=>{
            if (a_updateInfo.page.header.title){
              if (fcf.select("head>title")[0]){
                fcf.select("head>title")[0].innerHTML = a_updateInfo.page.header.title;
              } else {
                let title = document.createElement("title");
                title.innerHTML = a_updateInfo.page.header.title;
                fcf.select("head")[0].appendChild(title);
              }
            }
          });
        })
        .catch((a_error)=>{
          fcf.log.err("FCF", "Failed set route argument: ", a_url);
        })
      }

      getRootWrapper(){
        return fcf.getWrapper(this.getConfiguration().root.id);
      }

      getEventChannel() {
        return this._eventChannel;
      }

      getConfiguration() {
        return this._configuration;
      }

      setEnvironment(a_packageObject) {
        this._configuration.appendConfiguration(a_packageObject);
      }

      getLocalData(){
        return this._localData;
      }

      getInheritanceModes() {
        return this._configuration.inheritanceModes;
      }

      getTheme(a_name) {
        return this._loadTheme(a_name);
      }

      _loadTheme(a_name){
        a_name = fcf.empty(a_name) ? this._configuration.defaultTheme : a_name;
        if (this._themes[a_name])
          return this._themes[a_name];

        let themeInfo = undefined;
        let error = undefined;
        fcf.loadObject({
          path:   "/fcfpackages/fcf/theme",
          get:    { theme: a_name },
          async:  false,
        })
        .then((a_themeInfo)=>{
          themeInfo = a_themeInfo;
        })
        .catch((e)=>{
          fcf.log.err("FCF", `Can't load theme "${a_name}": `, e);
          error = e;
        });

        if (error)
          throw error;

        this._themes[a_name] = new Theme(a_name, themeInfo);
      }

    }

    fcf.NClient.application = new NClient.Application();
    fcf.application = fcf.NClient.application;

    return fcf.NClient.application;
  }
});
