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

      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) {
        return this._render.render(a_options);
      }

      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.maxReqursionRender)
          this.getConfiguration().maxReqursionRender = a_options.maxReqursionRender;

        if (a_options.renderStorage){
          this._localData.setOriginData(a_options.renderStorage);
          this._localData.setData(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("fcf_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("fcf_set_location_after", { url: a_url });
        })
        .catch((a_error)=>{
          fcf.log.err("FCF", "Failed set route: ", a_url, a_error);
          setTimeout(()=>{
            window.location.href = a_url;
          }, fcf.getContext().debug ? 5000 : 0);
        })
      }

      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, a_safeMode) {
        return this._loadTheme(a_name, a_safeMode);
      }

      _loadTheme(a_name, a_safeMode){
        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) {
          if (a_safeMode && this._configuration.defaultTheme in this._themes) {
            return this._themes[this._configuration.defaultTheme];
          }
          throw error;
        }

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

    }

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

    return fcf.NClient.application;
  }
});
