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: [],
  module: function() {

    var NServer = fcf.prepareObject(fcf, "NServer");
    /**
    *@class fcf::NServer::Router
    *lng_en @brief <b>[server only]</b> Application router class
    *lng_ru @brief <b>[server only]</b> Класс маршрутизатора приложения
    **/
    NServer.Router = function() {
      this._root = {
        parts:  {},
        args:   [],
        all:    undefined,

        title       : "",
        parent      : undefined,
        uri         : "",
        part        : "",
        endpoint    : undefined,
        getItems    : [],
        subPath     : "",
      };

      this._unsetNodes = {};

      this.getNode = function(a_path, a_requestChilds, a_staticNode, a_resultOriginObject){
        return this._getNode(a_path, a_requestChilds);
      }

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

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

        let languageIdentification = fcf.getSystemVariable("fcf:languageIdentification");
        if (!languageIdentification)
          languageIdentification = {};
        let avalibleLanguages      = fcf.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("/");
          }
        }

        let parts = fcf.trim(a_path.split("?")[0].split("#")[0], "/").split("/");
        let contexts = [];

        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 (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 eventData = {
            uri:             a_path[0] != "/" ? "/"+ a_path : a_path,
            parts:           parts,
            endpoint:        undefined,
            parent:          undefined,
            args:            {},
            userImportance:  false,
          };
          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;
              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;
                    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: {
                  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.childs)
            bestContext.node.endpoint.childs = [];
          let eventData = {
            uri:             bestContext.node.uri,
            parts:           fcf.trim(bestContext.node.uri.split("?")[0].split("#")[0], "/").split("/"),
            childs:          []
          };
          return fcf.application.getEventChannel().send("find_route_childs", eventData)
          .then((a_event)=>{
            self._appendRoute(bestContext.node, a_event.childs, false, true);
            return fcf.actions().each(bestContext.node.parts, async (a_key, a_part)=>{
              let requestChilds = typeof a_requestChilds == "number" ? a_requestChilds - 1 : a_requestChilds;
              let nodeInfo = await self._getNode(a_part.uri, requestChilds, a_staticNode);
              if (!nodeInfo || !nodeInfo.node)
                return;
              bestContext.node.parts[a_key] = nodeInfo.node;
            })
            .then(()=>{
              if (bestContext.node.endpoint) {
                bestContext.node.endpoint.childs = [];
                fcf.each(bestContext.node.parts, (k, part)=>{
                  bestContext.node.endpoint.childs.push(part);
                })
              }
            })
          })
        })
        .then(()=>{
          if (bestContext === undefined)
            return;
          bestContext.args = fcf.append({}, bestContext.args, bestContext.node.endpoint.routeArgs);
          return {
            args:       bestContext.args,
            subUri:     bestContext.subPath,
            node:       fcf.pattern(bestContext.node, bestContext.args),
            originNode: bestContext.node,
            userImportance: !!bestContext.userImportance,
          }
        })
      }

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

      this.appendRouteLevelByQuery = function(a_info){
        let size = fcf.count(a_info.path);
        fcf.application.getEventChannel().on("find_route", (a_event)=>{
          if (a_event.parts.length != size)
            return;
          let data = {multiMode: false};
          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;
          }

          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;
          let data = {multiMode: true};
          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;
          }

          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.multiRequest.args)
            args = fcf.tokenize(a_info.multiRequest.args, data);
          let records = await fcf.application.getStorage().query(a_info.multiRequest.query, args);

          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){
        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);
          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.trim(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:      {},
                args:       [],
                all:        true,
                title:      a_route.title,
                parent:     parentNode.uri,
                uri:        uri,
                part:       "*",
                endpoint:   a_route,
              };
              node.all = element;
              isBreak = true;
              node.parts["*"] = element;
            } else if (isArg) {
              element = {
                parts:      {},
                args:       [],
                all:        undefined,
                title:      "",
                parent:     parentNode.uri,
                uri:        uri,
                part:       part.substr(2, part.length-3),
                endpoint:   undefined,
              };
              node.args.push(element);
              node.parts[part] = element;
            } else if (!node.parts[part]) {
              element = {
                parts:  {},
                args:   [],
                all:    undefined,
                title       : "",
                parent      : parentNode.uri,
                uri         : uri,
                part        : part,
                endpoint    : undefined,
              };
              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;
              break;
            }
          }

          delete self._unsetNodes[a_route.parent];

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

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

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


        })
      }

    }

    return NServer.Router;
  }
});
