fcf.addException("ERROR_ROUTER_URL_NOTFULL",   "An incomplete URL path was specified (${{url}}$)");
fcf.addException("ERROR_ROUTER_URL_INCORRECT", "An incorrect URL path was specified (${{url}}$))");
fcf.addException("ERROR_ROUTER_URL_NOT_FOUND", "The requested address \"${{url}}$\" was not found");

const express = require('express');
fcf.module({
  name: "fcf:NServer/Router.js",
  dependencies: ["fcf:NSystem/cache.js"],
  module: function(cache) {
    var NServer = fcf.prepareObject(fcf, "NServer");

    function appendProjectionToCache(a_dstArr, a_sourceArr){
      fcf.each(a_sourceArr, (a_key, a_projectionInfo)=>{
        let foundKey = fcf.find(
                        a_dstArr,
                        (a_key, a_value) => {
                          let name = typeof a_projectionInfo === "object" ? a_projectionInfo.name : a_projectionInfo;
                          return typeof a_value === "object" ? a_value.name == name : a_value == name;
                        }
                       );

        if (foundKey === undefined){
          a_dstArr.push(a_projectionInfo);
        } else if (typeof a_dstArr[foundKey] !== "object" && typeof a_projectionInfo === "object") {
          a_dstArr[foundKey] = a_projectionInfo;
        }
      });
    }


    NServer.Router = function() {
      this._root = {
        parts:       {},
        partsOrder:  [],
        args:        [],
        all:         undefined,
        title:       "",
        parent:      undefined,
        uri:         "",
        part:        "",
        endpoint:    undefined,
        getItems:    [],
        subPath:     "",
      };

      this._unsetNodes = {};

      this._unsetMenuNodes = {};
      this._menuRoot       = {
        "/": {
          title:        "",
          parent:       undefined,
          uri:          "/",
          endpoint:     undefined,
          childs:       {},
          childsOrder:  [],
        },
      };
      this._menuLink = {
        "/": this._menuRoot["/"],
      };


      this.getMenuNode = function(a_path, a_requestChilds, a_useEmptyPaths, a_queryOptions){
        let self = this;
        if (a_requestChilds === true)
          a_requestChilds = 1000;

        let cacheInfo = {
          enable: -1,
          projections: [],
        }

        let cachePath = `${a_path}:${a_requestChilds}:${a_useEmptyPaths}`;
        let cacheValue = cache.get("fcf_get_menu_node", cachePath);
        if (cacheValue){
          return fcf.actions().then(()=>{ return cacheValue; });
        }

        let result;
        return this.getNode(a_path, a_requestChilds ? 1 : 0, !!a_useEmptyPaths, a_queryOptions)
        .then((a_node) => {
          if (typeof a_useEmptyPaths == "number" &&  a_useEmptyPaths !== 0)
            --a_useEmptyPaths;

          if (!a_node || !a_node.node)
            return;

          cacheInfo.enable &= a_node.cache.enable;
          appendProjectionToCache(cacheInfo.projections, a_node.cache.projections);

          result = {
            title:        a_node.node.title,
            parent:       a_node.node.parent,
            uri:          a_node.node.uri,
            endpoint:     a_node.node.endpoint,
            childs:       {},
            childsOrder:  [],
            cache:        cacheInfo,
          };

          if (self._menuLink[result.uri]) {
            fcf.each(self._menuLink[result.uri].childsOrder, (a_key, a_alias) => {
              let child = self._menuLink[result.uri].childs[a_alias];
              if (child.uri.indexOf("*") != -1 || child.uri.indexOf("$") != -1)
                return;
              if (!self._isMenuNode(child))
                return;
              if (!result.childs[child.uri])
                result.childsOrder.push(child.uri);
              result.childs[child.uri]              = fcf.translate(child);
              result.childs[child.uri].childs       = {};
              result.childs[child.uri].childsOrder  = [];
            });
          } else {
            fcf.each(a_node.node.partsOrder, (a_key, a_alias) => {
              let child = a_node.node.parts[a_alias];
              if (child.uri.indexOf("*") != -1 || child.uri.indexOf("$") != -1)
                return;
              if (!self._isMenuNode(child))
                return;

              let item = {
                title:        child.title,
                parent:       child.parent,
                uri:          child.uri,
                endpoint:     child.endpoint,
                childs:       {},
                childsOrder:  [],
              };
              item = fcf.translate(item);
              if (!result.childs[item.uri])
                result.childsOrder.push(item.uri);
              result.childs[item.uri] = item;
            });
          }

          fcf.each(a_node.node.partsOrder, (a_key, a_alias) => {
            let part = a_node.node.parts[a_alias];
            if (part.parent != a_node.originNode.uri)
              return;

            if (part.uri.indexOf("*") != -1 || part.uri.indexOf("$") != -1)
              return;
            if (a_alias.indexOf("*") != -1 || a_alias.indexOf("$") != -1)
              return;
            if (!self._isMenuNode(part))
              return;

            if (!result.childs[part.uri])
              result.childsOrder.push(part.uri);

            if (!result.childs[part.uri])
              result.childs[part.uri] = { childs: {}, childsOrder: [] };

            result.childs[part.uri].title    = part.title;
            result.childs[part.uri].parent   = part.parent;
            result.childs[part.uri].uri      = part.uri;
            result.childs[part.uri].endpoint = part.endpoint;

          });

          return result;
        })
        .then(()=>{
          let reorder = false;
          for(let key in result.childs)
            if (result.childs[key].endpoint && result.childs[key].endpoint.order !== undefined)
              reorder = true;
          if (!reorder)
            return;

          result.childsOrder.sort((a_left, a_right)=>{
            if ((!result.childs[a_left].endpoint || result.childs[a_left].endpoint.order === undefined) &&
                (!result.childs[a_right].endpoint || result.childs[a_right].endpoint.order === undefined)) {
              return 0;
            }
            if (!result.childs[a_left].endpoint || result.childs[a_left].endpoint.order === undefined) {
              return 1;
            }
            if (!result.childs[a_right].endpoint || result.childs[a_right].endpoint.order === undefined) {
              return -1;
            }
            let lo = result.childs[a_left].endpoint.order;
            let ro = result.childs[a_right].endpoint.order;
            return lo < ro ? -1 :
                   lo > ro ? 1 :
                             0;
          });
          let childs = {};
          for(let i = 0; i < result.childsOrder.length; ++i)
            childs[result.childsOrder[i]] = result.childs[result.childsOrder[i]];
          result.childs = childs;
        })
        .then(()=>{
          if (!result)
            return result;

          if (a_requestChilds <= 1)
            return result;

          return fcf.actions()
          .each(result.childs, (a_key, a_child)=>{
            return self.getMenuNode(a_child.uri, a_requestChilds-1, a_useEmptyPaths, a_queryOptions)
            .then((a_menuNode)=>{
              if (!a_menuNode)
                return;

              cacheInfo.enable &= a_menuNode.cache.enable;
              appendProjectionToCache(cacheInfo.projections, a_menuNode.cache.projections);
              fcf.each(a_menuNode.childsOrder, (a_key, a_alias)=>{
                let subchild = a_menuNode.childs[a_alias];
                if (!a_child.childs[subchild.uri])
                  a_child.childsOrder.push(subchild.uri);
                a_child.childs[subchild.uri] = subchild;
              })
            })
          })
          .then(()=>{
            if (!!cacheInfo.enable){
              cache.append({
                part:         "fcf_get_menu_node",
                name:         cachePath,
                projections:  cacheInfo.projections,
                value:        result,
                variant:      ["roles", "context.language"],
              })
            }

            return result;
          })
        })
      }


      this.getNode = function(a_path, a_requestChilds, a_useEmptyPaths, a_queryOptions){
        return this._getNode(a_path, a_requestChilds, false, false, a_useEmptyPaths, a_queryOptions);
      }

      this._isMenuNode = function(a_node){
        return a_node.endpoint && a_node.endpoint.controller == "fcf:NServer/NControllers/Page.js" && !a_node.endpoint.ignoreMenu;
      }

      this._getNode = function(a_path, a_requestChilds, a_staticNode, a_resultOriginObject, a_useEmptyPaths, a_queryOptions){
        let self = this;

        if (!a_requestChilds)
          a_requestChilds = 0;
        else if (a_requestChilds === true)
          a_requestChilds = 1000;

        if (a_path.indexOf("://") != -1) {
          let match = a_path.match(/([^/]*)\/\/([^\/]*)(.*)/i);
          if (match)
            a_path = match[3];
        }

        let languageIdentification = fcf.application.getSystemVariable("fcf:languageIdentification");
        if (!languageIdentification)
          languageIdentification = {};
        let avalibleLanguages      = fcf.application.getSystemVariable("fcf:languages");
        if (languageIdentification.byPrefix) {
          let parts = a_path.split("/");
          if (parts[0] === "")
            parts.shift();
          if (parts[0] && parts[0].length === 2 && parts[0] in avalibleLanguages){
            parts.shift();
            a_path = "/" + parts.join("/");
          }
        }
        if (a_path[0] != "/")
          a_path = "/" + a_path;
        let parts = fcf.rtrim(a_path.split("?")[0].split("#")[0], "/").split("/");
        let contexts = [];

        let cachePath = `get_node:${a_path}:${a_requestChilds}:${a_staticNode}`;
        let cacheInfo = {
          enable: -1,
          projections: [],
        };

        let cacheValue = cache.get("fcf_get_node", cachePath);
        if (cacheValue) {
          return fcf.actions().then(()=>{ return cacheValue });
        }

        let clFind = (a_node, a_parts, a_start, a_context) => {
          if (a_context == undefined){
            a_context            = {};
            a_context.args       = {};
            a_context.argCounter = 0;
            a_context.all        = false;
            a_context.node       = undefined;
          }

          let isArg = !!a_node.args.length;
          let isAll = !!a_node.all;

          if (a_node.parts[a_parts[a_start]]){
            if ((a_start + 1) == a_parts.length ){
              let context = fcf.clone(a_context);
              context.node = a_node.parts[a_parts[a_start]];
              if (a_useEmptyPaths || context.node.endpoint)
                contexts.push(context)
              return;
            } else if(a_node.parts[a_parts[a_start]]){
              return clFind(a_node.parts[a_parts[a_start]], a_parts, a_start + 1, a_context)
            }
          } else if(isArg){
            for(var argIndex = 0; argIndex < a_node.args.length; ++argIndex){
              let context = fcf.clone(a_context);
              context.args[a_node.args[argIndex].part] = decodeURIComponent(a_parts[a_start]);
              ++context.argCounter;
              context.node = a_node.args[argIndex];
              if ((a_start + 1) == a_parts.length){
                if (context.node.endpoint)
                  contexts.push(context)
              } else {
                return clFind(a_node.args[argIndex], a_parts, a_start + 1, context)
              }
            }
            if ((a_start + 1) == a_parts.length)
              return;
          } else if(isAll){
            let context  = fcf.clone(a_context);
            context.all  = true;
            context.node = a_node.all;
            context.subPath = fcf.trim(a_parts.slice(a_start).join("/"), "/");
            if (context.node.endpoint)
              contexts.push(context)
            return;
          }
        }

        let bestContext = undefined;
        return fcf.actions()
        .then(()=>{
          if (a_staticNode)
            return;
          let eventParts = fcf.clone(parts);
          eventParts.shift();
          let eventData = {
            uri:             a_path[0] != "/" ? "/"+ a_path : a_path,
            parts:           eventParts,
            endpoint:        undefined,
            parent:          undefined,
            args:            {},
            userImportance:  false,
            queryOptions:    a_queryOptions,
            cache:           cacheInfo,
          };
          return fcf.application.getEventChannel().send("find_route", eventData)
          .then(async (a_event)=>{
            if (a_event.endpoint){
              let uri = a_path[0] != "/" ? "/"+ a_path : a_path;
              let parent = a_event.parent ? fcf.rtrim(a_event.parent, "/") : a_event.parent;
              if (parent === undefined) {
                let uriArr = uri.split("/");
                uriArr.pop();
                while(uriArr.length){
                  let testParent = uriArr.join("/");
                  let nodeInfo = await self._getNode(testParent);
                  if (nodeInfo){
                    parent = testParent ? fcf.rtrim(testParent, "/") : testParent;
                    break;
                  }
                  uriArr.pop();
                }
              }

              a_event.parent       = fcf.trim(a_event.parent, "/");
              a_event.endpoint.uri = fcf.trim(a_event.endpoint.uri, "/");
              bestContext = {
                args: a_event.args,
                subPath: "",
                userImportance: a_event.userImportance,
                node: {
                  partsOrder: [],
                  parts:      {},
                  args:       [],
                  title:      a_event.endpoint.title ? a_event.endpoint.title : "",
                  all:        undefined,
                  parent:     parent ? parent : "",
                  uri:        uri,
                  part:       parts[parts.length-1],
                  endpoint:   a_event.endpoint
                }
              };
            }
          })
        })
        .then(()=>{
          if (bestContext)
            return;

          clFind(self._root, parts, 0);
          for(let i = 0; i < contexts.length; ++i){
            if (bestContext == undefined){
              bestContext = contexts[i];
              continue;
            }
            if ((bestContext.argCounter > contexts[i].argCounter) && !contexts[i].all){
              bestContext = contexts;
            }
          }
        })
        .then(()=>{
          if (bestContext === undefined)
            return;

          if (!a_resultOriginObject)
            bestContext = fcf.append(true, {}, bestContext);

          if (!a_requestChilds)
            return;

          if (bestContext.node.endpoint && !bestContext.node.endpoint.childs)
            bestContext.node.endpoint.childs = [];
          let eventData = {
            uri:             bestContext.node.uri,
            parts:           fcf.trim(bestContext.node.uri.split("?")[0].split("#")[0], "/").split("/"),
            childs:          [],
            queryOptions:    a_queryOptions,
            cache:           cacheInfo,
          };
          return fcf.application.getEventChannel().send("find_route_childs", eventData)
          .then((a_event)=>{
            self._appendRoute(bestContext.node, a_event.childs, false, true, true);
            return fcf.actions().each(bestContext.node.parts, async (a_key, a_part) => {
              let requestChilds = typeof a_requestChilds == "number" ? a_requestChilds - 1 : a_requestChilds;
              if (!requestChilds)
                return;
              let nodeInfo = await self._getNode(a_part.uri, requestChilds, a_staticNode);
              if (!nodeInfo || !nodeInfo.node)
                return;
              cacheInfo.enable &= nodeInfo.cache.enable;
              appendProjectionToCache(cacheInfo.projections, nodeInfo.cache.projections);
              if (!bestContext.node.parts[a_key])
                bestContext.node.partsOrder.push(a_key);
              bestContext.node.parts[a_key] = nodeInfo.node;
            });
          })
        })
        .then(()=>{
          if (bestContext === undefined)
            return;
          bestContext.args = fcf.append({}, bestContext.args, bestContext.node.endpoint ? bestContext.node.endpoint.routeArgs : undefined);
          let resultNode = fcf.translate(bestContext.node);
          if (resultNode) {
            resultNode.uri = fcf.pattern(resultNode.uri, bestContext.args);
            if (resultNode.endpoint)
              resultNode.endpoint.uri = fcf.pattern(resultNode.endpoint.uri, bestContext.args);
          }

          let cacheEnable = !bestContext.argCounter && !bestContext.all && !!cacheInfo.enable;

          let result = {
            args:       bestContext.args,
            subUri:     bestContext.subPath,
            node:       resultNode,
            originNode: bestContext.node,
            userImportance: !!bestContext.userImportance,
            cache:      {
              enable:      cacheEnable,
              projections: cacheInfo.projections,
            }
          };

          if (cacheEnable){
            cache.append({
              part:         "fcf_get_node",
              name:         cachePath,
              projections:  cacheInfo.projections,
              value:        result,
              variant:      ["roles", "context.language"],
            })
          }

          return result;
        })
      }

      this.append = function(a_routes) {
        this._appendRoute(this._root, a_routes, true);
      }

      this.appendRouteByQuery = function(a_info){
        let path = Array.isArray(a_info.path) ? a_info.path : [];
        for(let i = 0; i < path.length; ++i)
          if (!Array.isArray(path[i]))
            path[i] = [path[i]];

        let cacheInfo = undefined;

        if (!("cache" in a_info))
          a_info.cache = true;

        if (a_info.cache){
          let query = a_info.singleQuery && a_info.singleQuery.query  ? a_info.singleQuery.query :
                      a_info.multiQuery && a_info.multiQuery.query    ? a_info.multiQuery.query :
                                                                        false;
          if (query)
          cacheInfo = {projections: fcf.application.getStorage().getProjectionsByQuery(query)};
          if (typeof a_info.cache === "object") {
            if (!cacheInfo)
              cacheInfo = {projections: []};
            appendProjectionToCache(cacheInfo.projections, a_info.cache.projections)
          }
        }

        fcf.application.getEventChannel().on("find_route", async (a_event)=>{
          if (path.length >= a_event.parts)
            return;

          for(let i = 0; i < path.length; ++i)
            if (path[i][0] != a_event.parts[i])
              return;

          let subUri = "";
          for(let i = path.length; i < a_event.parts.length; ++i)
            subUri += (i == path[0].length ? "" : "/") + a_event.parts[i];

          let data = {multiMode: false, context: fcf.getContext(), uri: a_event.uri, subUri: subUri};
          let counter = 0;
          for(let i = 0; i < path.length; ++i){
            let itm = path[i]
            if (itm[0] != "*" && itm[0] != a_event.parts[counter])
              return;
            if (itm[1])
              data[itm[1]] = decodeURIComponent(a_event.parts[counter]);
            ++counter;
          }

          if (a_info.singleQuery) {
            a_event.cache.enable &= !!cacheInfo;
            if (cacheInfo)
              appendProjectionToCache(a_event.cache.projections, cacheInfo.projections);

            let args = [];
            if (a_info.singleQuery.args)
              args = fcf.tokenize(a_info.singleQuery.args, data);
            let roles = fcf.append(["menu"], a_info.singleQuery.roles);
            data.record = (await fcf.application.getStorage().query(a_info.singleQuery.query, args, fcf.append({roles: roles}, a_event.queryOptions)))[0][0];
            if (!data.record)
              return;
          } else {
            data.record = {};
          }

          let endpoint = fcf.tokenize(a_info.endpoint, data);
          endpoint.uri = a_event.uri;
          a_event.endpoint = endpoint;
        });

        fcf.application.getEventChannel().on("find_route_childs", async (a_event)=>{
          if (path.length > a_event.parts)
            return;
          if (!a_info.multiQuery)
            return;
          for(let i = 0; i < path.length; ++i)
            if (path[i][0] != a_event.parts[i])
              return;


          let subUri = "";
          for(let i = path.length; i < a_event.parts.length; ++i)
            subUri += (i == path[0].length ? "" : "/") + a_event.parts[i];

          let data = {multiMode: true, context: fcf.getContext(), uri: a_event.uri, subUri: subUri};
          let counter = 0;
          for(let i = 0; i < (a_info.path.length-1); ++i){
            let itm = a_info.path[i]
            if (itm[0] != "*" && itm[0] != a_event.parts[counter])
              return;
            if (itm[1])
              data[itm[1]] = decodeURIComponent(a_event.parts[counter]);
            ++counter;
          }

          a_event.cache.enable &= !!cacheInfo;
          if (cacheInfo)
            appendProjectionToCache(a_event.cache.projections, cacheInfo.projections);

          let args = [];
          if (a_info.multiQuery.args)
            args = fcf.tokenize(a_info.multiQuery.args, data);
          let roles = fcf.append(["menu"], a_info.multiQuery.roles);
          let records = await fcf.application.getStorage().query(a_info.multiQuery.query, args, fcf.append({roles: roles}, a_event.queryOptions));

          for(let i = 0; i < records[0].length; ++i){
            data.record = records[0][i];
            let endpoint = fcf.tokenize(a_info.endpoint, data);
            a_event.childs.push(endpoint);
          }
          data.record = records[0]
        });
      }

      this.appendRouteLevelByQuery = function(a_info){
        for(let i = 0; i < a_info.path.length; ++i)
          if (!Array.isArray(a_info.path[i]))
            a_info.path[i] = [a_info.path[i]];

        let cacheInfo = undefined;

        if (!("cache" in a_info))
          a_info.cache = true;

        if (a_info.cache){
          let query = a_info.singleQuery && a_info.singleQuery.query  ? a_info.singleQuery.query :
                      a_info.multiQuery && a_info.multiQuery.query    ? a_info.multiQuery.query :
                                                                        false;
          if (query)
            cacheInfo = {projections: fcf.application.getStorage().getProjectionsByQuery(query)};
          if (typeof a_info.cache === "object") {
            if (!cacheInfo)
              cacheInfo = {projections: []};
            appendProjectionToCache(cacheInfo.projections, a_info.cache.projections);
          }
        }


        let size = fcf.count(a_info.path);
        fcf.application.getEventChannel().on("find_route", async (a_event)=>{
          if (a_event.parts.length != size)
            return;

          let data = {multiMode: false, context: fcf.getContext(), uri: a_event.uri};
          let counter = 0;
          for(let i = 0; i < a_info.path.length; ++i){
            let itm = a_info.path[i]
            if (itm[0] != "*" && itm[0] != a_event.parts[counter])
              return;
            if (itm[1])
              data[itm[1]] = decodeURIComponent(a_event.parts[counter]);
            ++counter;
          }

          if (a_info.singleQuery) {
            a_event.cache.enable &= !!cacheInfo;
            if (cacheInfo)
              appendProjectionToCache(a_event.cache.projections, cacheInfo.projections);

            let args = [];
            if (a_info.singleQuery.args)
              args = fcf.tokenize(a_info.singleQuery.args, data);
            let roles = fcf.append(["menu"], a_info.singleQuery.roles);
            data.record = (await fcf.application.getStorage().query(a_info.singleQuery.query, args, fcf.append({roles: roles}, a_event.queryOptions)))[0][0];
            if (fcf.empty(data.record))
              return;
          } else {
            data.record = {};
          }

          let endpoint = fcf.tokenize(a_info.endpoint, data);
          endpoint.uri = a_event.uri;
          a_event.endpoint = endpoint;
        });

        fcf.application.getEventChannel().on("find_route_childs", async (a_event)=>{
          if (a_event.parts.length != size-1)
            return;
          if (!a_info.multiQuery)
            return;
          let data = {multiMode: true, context: fcf.getContext(), uri: a_event.uri};
          let counter = 0;
          for(let i = 0; i < (a_info.path.length-1); ++i){
            let itm = a_info.path[i]
            if (itm[0] != "*" && itm[0] != a_event.parts[counter])
              return;
            if (itm[1])
              data[itm[1]] = decodeURIComponent(a_event.parts[counter]);
            ++counter;
          }

          a_event.cache.enable &= !!cacheInfo;
          if (cacheInfo)
            appendProjectionToCache(a_event.cache.projections, cacheInfo.projections);

          if (a_info.path[a_info.path.length-1][1])
            data[a_info.path[a_info.path.length-1][1]] = undefined;

          let args = [];
          if (a_info.multiQuery.args)
            args = fcf.tokenize(a_info.multiQuery.args, data);
          let roles = fcf.append(["menu"], a_info.multiQuery.roles);
          let records = await fcf.application.getStorage().query(a_info.multiQuery.query, args, fcf.append({roles: roles}, a_event.queryOptions));

          for(let i = 0; i < records[0].length; ++i){
            data.record = records[0][i];
            let endpoint = fcf.tokenize(a_info.endpoint, data);
            a_event.childs.push(endpoint);
          }
          data.record = records[0]
        });
      }


      this._appendRoute = function(a_root, a_route, a_fullPath, a_addToRoot, a_custom){
        let self = this;
        if (fcf.empty(a_route))
          return;
        if (Array.isArray(a_route)){
          for(let i = 0; i < a_route.length; ++i)
            this._appendRoute(a_root, a_route[i], a_fullPath, a_addToRoot, a_custom);
          return;
        }

        a_route = fcf.append(true, {}, a_route);

        if (a_fullPath && a_route.uri[0] != "/")
          a_route.uri = "/" + a_route.uri;

        if (a_addToRoot){
          if (a_route.uri.indexOf(a_root.uri) == 0)
            a_route.uri = a_route.uri.substr(a_root.uri.length);
          a_route.uri = fcf.trim(a_route.uri, "/");
        }

        let realParentUri = undefined;

        fcf.actions()
        .then(()=>{
          if (a_addToRoot){
            return a_root;
          } else if (!fcf.empty(a_route.parent)){
            return self._getNode(a_route.parent, false, true, true).then((a_nodeInfo)=>{ return a_nodeInfo ? a_nodeInfo.node : undefined})
          } else {
            if (!a_fullPath)
              return a_root;
            let root      = undefined;
            let parentArr = fcf.trim(a_route.uri, "/").split("?")[0].split("/");
            parentArr.pop();
            while(parentArr.length){
              let nodeInfo = undefined;
              self._getNode("/"+parentArr.join("/"), false, true, true)
              .then((a_nodeInfo)=>{
                nodeInfo = a_nodeInfo;
              });
              if (nodeInfo) {
                root = nodeInfo.node;
                break;
              }
              parentArr.pop();
            }
            if (root)
              realParentUri = root.uri;
            return a_root;
          }
        })
        .then((node)=>{
          if (node == undefined){
            if (self._unsetNodes[a_route.parent])
              self._unsetNodes[a_route.parent] = [];
            self._unsetNodes[a_route.parent].push([{root: a_root, route: a_route, fullPath: a_fullPath}]);
            return;
          }

          let parentNode = node;

          parts = fcf.rtrim(a_route.uri, "/").split("?")[0].split("/");
          let isFullPath = a_route.uri[0] == "/";
          if (isFullPath)
            node = self._root;

          for(let i = 0; i < parts.length; ++i) {
            let part = parts[i];
            let selection = undefined;
            let isArg = part.charAt(0) == "$" && part.charAt(1) == "{";
            let isAll = part == "*";
            let element = undefined;
            let isBreak = false;
            let uri = !isFullPath ? node.uri + "/" + parts[i] : parts.slice(0, i + 1).join("/");
            if (uri[0] != "/")
              uri = "/" + uri;
            a_route.uri = uri;

            if (isAll){
              element = {
                parts:      {},
                partsOrder: [],
                args:       [],
                all:        true,
                title:      a_route.title,
                parent:     parentNode.uri,
                uri:        uri,
                part:       "*",
                endpoint:   a_route,
              };
              node.all = element;
              isBreak = true;
              if (!node.parts["*"])
                node.partsOrder.push("*");
              node.parts["*"] = element;

            } else if (isArg) {
              element = {
                parts:      {},
                partsOrder: [],
                args:       [],
                all:        undefined,
                title:      "",
                parent:     parentNode.uri,
                uri:        uri,
                part:       part.substr(2, part.length-3),
                endpoint:   undefined,
              };
              node.args.push(element);
              if (!node.parts[part])
                node.partsOrder.push(part);
              node.parts[part] = element;
            } else if (!node.parts[part]) {
              element = {
                parts:      {},
                partsOrder: [],
                args:       [],
                all:        undefined,
                title:      "",
                parent:     uri != "/" ? parentNode.uri : undefined,
                uri:        uri,
                part:       part,
                endpoint:   undefined,
              };
              if (!node.parts[part])
                node.partsOrder.push(part);
              node.parts[part] = element;
            } else {
              element = node.parts[part];
            }
            node = element;

            if (i+1 == parts.length || isBreak) {
              element.title     = !fcf.empty(a_route.title) ? fcf.str(a_route.title) : "";
              element.endpoint  = a_route.controller ? a_route : undefined;
              if (realParentUri !== undefined)
                element.parent = realParentUri ? fcf.rtrim(realParentUri, "/") : realParentUri;
              break;
            }
          }

          if (!a_custom)
            this._appendMenuItem(node);

          delete self._unsetNodes[a_route.parent];

          if (!fcf.empty(a_route.childs)){
            self._appendRoute(node, a_route.childs, false, a_addToRoot, a_custom);
          }

          if (!fcf.empty(self._unsetNodes)){
            for (let parentUri in self._unsetNodes){
              let infs = self._unsetNodes[parentUri];
              for(var i = 0; i < infs.length; ++i)
                self._appendRoute(infs[i].root, infs[i].route, infs[i].fullPath, a_addToRoot, a_custom);
            }
          }

          if (node.endpoint) {
            node.endpoint.childs = [];
            fcf.each(node.parts, (k, part)=>{
              node.endpoint.childs.push(part);
            })
          }


        })
      }

      this._appendMenuItem = function(a_node){
        let element = {
          args:         a_node.args,
          all:          a_node.all,
          title:        a_node.title,
          parent:       !a_node.parent && a_node.uri != "" && a_node.uri != "/" ? "/" :
                        !a_node.parent                                          ? undefined :
                                                                                  a_node.parent,
          uri:          a_node.uri,
          endpoint:     a_node.endpoint,
          childs:       {},
          childsOrder:  [],
        };

        let parts  = (fcf.trim(element.uri, "/")).split("/");
        if (parts.length == 1 && parts[0] == "") {
          this._menuRoot["/"].args        = element.args;
          this._menuRoot["/"].all         = element.all;
          this._menuRoot["/"].title       = element.title;
          this._menuRoot["/"].parent      = element.parent;
          this._menuRoot["/"].uri         = element.uri;
          this._menuRoot["/"].endpoint    = element.endpoint;
          fcf.append(this._menuRoot["/"].childs, element.childs);
          fcf.append(this._menuRoot["/"].childsOrder, element.childsOrder);
        } else if (element.parent in this._menuLink){
          this._menuLink[element.uri] = element;
          if (!this._menuLink[element.parent].childs[element.uri])
            this._menuLink[element.parent].childsOrder.push(element.uri);
          this._menuLink[element.parent].childs[element.uri] = element;
        } else {
          this._unsetMenuNodes[element.uri] = element;
          return;
        }

        delete this._unsetMenuNodes[element.uri];

        for (let uri in this._unsetMenuNodes)
          this._appendMenuItem(this._unsetMenuNodes[uri]);
      }

    }

    return NServer.Router;
  }
});
