fcf.module({
  name: "fcf:NClient/Application.js",
  dependencies: [ "fcf:NTheme/Themes.js",
                  "fcf:NTheme/Theme.js",
                  "fcf:NRender/Render.js",
                  "fcf:NServer/Configuration.js",
                  "fcf:NClient/LocalData.js",
                ],
  debug: true,
  module: function(Themes, Theme, Render, Configuration, LocalData) {
    var NClient = fcf.prepareObject(fcf, "NClient");

    window.addEventListener('popstate', function(event) {
      let currentURL = fcf.getContext().route.url.split("#")[0];
      let newURL     = window.location.href.split("#")[0];
      if (currentURL == newURL){
        fcf.getContext().route.url = window.location.href;
        return;
      }
      fcf.application._setLocation(window.location.href, false);
    });

    NClient.Application = function() {
      var self = this;
      this._configuration = new Configuration();
      this._themes = new Themes({}, this._configuration);
      this._localData = new LocalData();
      this._lastURL = window.location.href;
      this._delayedUpdate = false;

      this._settings = {
        fileСaching:         true,
        clientRenderingMode: "server",
        defaultTheme:        "defaultTheme",
        renderStorage:       {},
        renderRestore:       {},
      };

      this._eventChannel = fcf.NDetails.eventChannel;
      this._eventChannel.setOwner(this);

      this._render = new Render({
                            application: this,
                            fileСaching: this._settings.fileСaching,
                          });

      this.initialize = function(){
        this._systemWrapper  = new fcf.NClient.Wrapper({}, true);
      }

      this.render = function(a_options) {
        let parent = a_options.parent;
        let owner = a_options.owner;
        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 = owner;
          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();
        }

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

          function clCompleteRender(a_error, a_template){
            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;
            }

            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;
              fcf.application.getLocalData().setObject(id, args);
              fcf.application.getLocalData().setSourceObject(id, sources);
              fcf.application.getLocalData().setOriginSourcesKeys(id, originSourcesKeys);
            }

            for(let id in a_template.state.resets){
              for(let arg in a_template.state.resets[id]){
                let value = fcf.resolve(fcf.application.getLocalData().getObject(id), arg);
                fcf.application.getLocalData().setItem(id, arg, value);
              }
            }

            function clCreate() {
              if (owner !== false) {
                owner = typeof owner == "string"    ? fcf.select(owner)[0] : 
                               owner === undefined  ? document.body :
                                                      owner;
                let div = document.createElement("div");
                if (!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 && a_template.args.fcfWrapper) {
                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}), clCreate);
            else
              clCreate();
          }

          let args = {};
          if (parent)
            args.fcfParent = parent;
          fcf.append(args, a_options.args);
          fcf.loadObject({
            path:  "@url:fcfRender",
            post: { template: a_options.template, args: args, url: window.location.href, externalArgs: a_options.externalArgs },
            onResult: function(a_error, a_response) {
              clCompleteRender(a_error, a_response);
            }
          });
        })
        .catch((a_error)=>{
          fcf.application.getEventChannel().send("error", {error: a_error});
        })
      }

      this.getSettings = function(){
        this._settings.defaultTheme = this._themes.getDefaultThemeName();
        return this._settings;
      }

      this.setSettings = function(a_options){
        if (typeof a_options == "string"){
          a_options = fcf.base64Decode(a_options);
          a_options = eval("(" + a_options + ")");
        }

        if (a_options.defaultTheme) {
          this._themes.setDefaultThemeName(a_options.defaultTheme);
          this._settings.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.fileСaching !== undefined) {
          this._settings.fileСaching = !!a_options.fileСaching;
          this._render.setFileСaching(!!a_options.fileСaching);
        }

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

        if (a_options.root !== undefined) {
          this._settings.root = a_options.root;
        }

        return this;
      }

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

      this._setLocation = function(a_url, a_setUrl/* = true*/, a_editor){
        let self = this;

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

        let lastRouteInfo = fcf.clone(fcf.getContext().route);

        return fcf.actions()
        .then(() => {
          return fcf.application.getEventChannel().send("set_location_before", { url: a_url });
        })
        .then(() => {
          return fcf.loadObject({
            path:  "/fcfpackages/fcf/route",
            post: { url: a_url }
          })
        })
        .then((a_routeInfo)=>{
          fcf.getContext().route = new fcf.RouteInfo(a_routeInfo.route.url);
          fcf.append(fcf.getContext().route.args, a_routeInfo.route.args);
          if (a_setUrl)
            window.history.pushState(undefined, undefined, fcf.getContext().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;
            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" && lastRouteInfo.url != a_routeInfo.route.url)
                return true;
              if (parts.length == 2 && parts[0] == "route" && parts[1] == "uri" && lastRouteInfo.uri != a_routeInfo.route.uri)
                return true;
              if (parts.length == 2 && parts[0] == "route" && parts[1] == "referer" && lastRouteInfo.referer != a_routeInfo.route.referer)
                return true;
              if (parts.length == 3 && parts[0] == "route" && parts[1] == "args" && lastRouteInfo.args[parts[2]] != a_routeInfo.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] != a_routeInfo.route.args[srcArg.arg]){
                  wrapper.setArg(fcfRouteArgs[i], 
                                 a_routeInfo.route.args[srcArg.arg], 
                                 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)=>{
              let titlecontent = a_updateInfo.page.header.title || a_routeInfo.route.title;
              if (titlecontent){
                if (fcf.select("head>title")[0]){
                  fcf.select("head>title")[0].innerHTML = titlecontent;
                } else {
                  let title = document.createElement("title");
                  title.innerHTML = titlecontent;
                  fcf.select("head")[0].appendChild(title);
                }
              }
            });
          })
        })
        .then(() => {
          return fcf.application.getEventChannel().send("set_location_after", { url: a_url });
        })
        .catch((a_error)=>{
          fcf.log.err("FCF", "Failed set route: ", a_url);
        })
      }

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

      this._setUrlArg = function(a_name, a_value, a_editor){
        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, route.url);

        window.history.pushState(undefined, undefined, 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,
                               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);
        })
      }

      this.getSystemWrapper = function(){
        return this._systemWrapper;
      }

      this.getRootWrapper = function(){
        return fcf.getWrapper(this._settings.root.id);
      }

      this.getThemes = function() {
        return this._themes;
      }

      this.getRender = function() {
        return this._render;
      }

      this.getEventChannel = function() {
        return this._eventChannel;
      }


      this.getConfiguration = function() {
        return this._configuration;
      }

      this.appendPackage = function(a_packageName){
        var path = "/fcfpackages/" + a_packageName + "/" + a_packageName + ".package";
        fcf.load({
          path: path,
          async: false,
          onResult: function(a_error, a_data){
            if (a_error) {
              fcf.log.err("FCF", a_error.message, a_error);
              return;
            }
            var info = fcf.scriptExecutor.parse(a_data, {}, path, 0);
            self._configuration.appendConfiguration(info);
          }
        });
      }

      this.appendPackageRawObject = function(a_packageName, a_packageObject) {
        self._configuration.appendConfiguration(a_packageObject);
      }

      this.appendThemes = function(a_themeNames) {
        if (!Array.isArray(a_themeNames))
          return;
        for(var i = 0; i < a_themeNames.length; ++i)
          this.appendTheme(a_themeNames[i]);
      }

      this.appendTheme = function(a_themeName){
        var path = "/fcfpackages/" + a_themeName + "/" + a_themeName + ".theme";
        fcf.load({
          path: path,
          async: false,
          onResult: function(a_error, a_data){
            if (a_error) {
              fcf.log.err("FCF", a_error.message, a_error);
              return;
            }
            var info = fcf.scriptExecutor.parse(a_data, {}, path, 0);
            var theme = new Theme({package: a_themeName, info: info, configuration: self._configuration});
            fcf.actions()
            .then(()=>{
              return theme.initialize();
            })
            .then(()=>{
              self._themes.attachTheme(a_themeName, theme);
            });
          }
        });
      }

      this.appendThemeRawObject = function(a_themeName, a_object){
        var theme = new Theme({package: a_themeName, info: a_object, configuration: self._configuration});
        fcf.actions()
        .then(()=>{
          return theme.initialize();
        })
        .then(()=>{
          self._themes.attachTheme(a_themeName, theme);
        });

      }

      this.getLocalData = function(){
        return this._localData;
      }

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

    }

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

    return fcf.NClient.application;
  }
});
