import * as QS from "../QueryString.js";
const FULL_PATH_REGEXP = /^https?:\/\/.*?\//;
const OPTIONAL_PARAM_REGEXP = /(\/:[^/()]*?)\?(\/?)/;
/** @internal */
export const make = (options = {}) => new RouterImpl(options);
class RouterImpl {
  constructor(options = {}) {
    this.options = {
      ignoreTrailingSlash: true,
      ignoreDuplicateSlashes: true,
      caseSensitive: false,
      maxParamLength: 100,
      ...options
    };
  }
  options;
  routes = [];
  trees = {};
  on(method, path, handler) {
    const optionalParamMatch = path.match(OPTIONAL_PARAM_REGEXP);
    if (optionalParamMatch && optionalParamMatch.index !== undefined) {
      assert(path.length === optionalParamMatch.index + optionalParamMatch[0].length, "Optional Parameter needs to be the last parameter of the path");
      const pathFull = path.replace(OPTIONAL_PARAM_REGEXP, "$1$2");
      const pathOptional = path.replace(OPTIONAL_PARAM_REGEXP, "$2");
      this.on(method, pathFull, handler);
      this.on(method, pathOptional, handler);
      return;
    }
    if (this.options.ignoreDuplicateSlashes) {
      path = removeDuplicateSlashes(path);
    }
    if (this.options.ignoreTrailingSlash) {
      path = trimLastSlash(path);
    }
    const methods = typeof method === "string" ? [method] : method;
    for (const method of methods) {
      this._on(method, path, handler);
    }
  }
  all(path, handler) {
    this.on(httpMethods, path, handler);
  }
  _on(method, path, handler) {
    if (this.trees[method] === undefined) {
      this.trees[method] = new StaticNode("/");
    }
    let pattern = path;
    if (pattern === "*" && this.trees[method].prefix.length !== 0) {
      const currentRoot = this.trees[method];
      this.trees[method] = new StaticNode("");
      this.trees[method].staticChildren["/"] = currentRoot;
    }
    let parentNodePathIndex = this.trees[method].prefix.length;
    let currentNode = this.trees[method];
    const params = [];
    for (let i = 0; i <= pattern.length; i++) {
      if (pattern.charCodeAt(i) === 58 && pattern.charCodeAt(i + 1) === 58) {
        // It's a double colon
        i++;
        continue;
      }
      const isParametricNode = pattern.charCodeAt(i) === 58 && pattern.charCodeAt(i + 1) !== 58;
      const isWildcardNode = pattern.charCodeAt(i) === 42;
      if (isParametricNode || isWildcardNode || i === pattern.length && i !== parentNodePathIndex) {
        let staticNodePath = pattern.slice(parentNodePathIndex, i);
        if (!this.options.caseSensitive) {
          staticNodePath = staticNodePath.toLowerCase();
        }
        staticNodePath = staticNodePath.split("::").join(":");
        staticNodePath = staticNodePath.split("%").join("%25");
        // add the static part of the route to the tree
        currentNode = currentNode.createStaticChild(staticNodePath);
      }
      if (isParametricNode) {
        let isRegexNode = false;
        const regexps = [];
        let lastParamStartIndex = i + 1;
        for (let j = lastParamStartIndex;; j++) {
          const charCode = pattern.charCodeAt(j);
          const isRegexParam = charCode === 40;
          const isStaticPart = charCode === 45 || charCode === 46;
          const isEndOfNode = charCode === 47 || j === pattern.length;
          if (isRegexParam || isStaticPart || isEndOfNode) {
            const paramName = pattern.slice(lastParamStartIndex, j);
            params.push(paramName);
            isRegexNode = isRegexNode || isRegexParam || isStaticPart;
            if (isRegexParam) {
              const endOfRegexIndex = getClosingParenthensePosition(pattern, j);
              const regexString = pattern.slice(j, endOfRegexIndex + 1);
              regexps.push(trimRegExpStartAndEnd(regexString));
              j = endOfRegexIndex + 1;
            } else {
              regexps.push("(.*?)");
            }
            const staticPartStartIndex = j;
            for (; j < pattern.length; j++) {
              const charCode = pattern.charCodeAt(j);
              if (charCode === 47) break;
              if (charCode === 58) {
                const nextCharCode = pattern.charCodeAt(j + 1);
                if (nextCharCode === 58) j++;else break;
              }
            }
            let staticPart = pattern.slice(staticPartStartIndex, j);
            if (staticPart) {
              staticPart = staticPart.split("::").join(":");
              staticPart = staticPart.split("%").join("%25");
              regexps.push(escapeRegExp(staticPart));
            }
            lastParamStartIndex = j + 1;
            if (isEndOfNode || pattern.charCodeAt(j) === 47 || j === pattern.length) {
              const nodePattern = isRegexNode ? "()" + staticPart : staticPart;
              const nodePath = pattern.slice(i, j);
              pattern = pattern.slice(0, i + 1) + nodePattern + pattern.slice(j);
              i += nodePattern.length;
              const regex = isRegexNode ? new RegExp("^" + regexps.join("") + "$") : undefined;
              currentNode = currentNode.createParametricChild(regex, staticPart, nodePath);
              parentNodePathIndex = i + 1;
              break;
            }
          }
        }
      } else if (isWildcardNode) {
        // add the wildcard parameter
        params.push("*");
        currentNode = currentNode.createWildcardChild();
        parentNodePathIndex = i + 1;
        if (i !== pattern.length - 1) {
          throw new Error("Wildcard must be the last character in the route");
        }
      }
    }
    if (!this.options.caseSensitive) {
      pattern = pattern.toLowerCase();
    }
    if (pattern === "*") {
      pattern = "/*";
    }
    for (const existRoute of this.routes) {
      if (existRoute.method === method && existRoute.pattern === pattern) {
        throw new Error(`Method '${method}' already declared for route '${pattern}'`);
      }
    }
    const route = {
      method,
      path,
      pattern,
      params,
      handler
    };
    this.routes.push(route);
    currentNode.addRoute(route);
  }
  has(method, path) {
    const node = this.trees[method];
    if (node === undefined) {
      return false;
    }
    const staticNode = node.getStaticChild(path);
    if (staticNode === undefined) {
      return false;
    }
    return staticNode.isLeafNode;
  }
  find(method, path) {
    let currentNode = this.trees[method];
    if (currentNode === undefined) return undefined;
    if (path.charCodeAt(0) !== 47) {
      // 47 is '/'
      path = path.replace(FULL_PATH_REGEXP, "/");
    }
    // This must be run before sanitizeUrl as the resulting function
    // .sliceParameter must be constructed with same URL string used
    // throughout the rest of this function.
    if (this.options.ignoreDuplicateSlashes) {
      path = removeDuplicateSlashes(path);
    }
    let sanitizedUrl;
    let querystring;
    let shouldDecodeParam;
    try {
      sanitizedUrl = safeDecodeURI(path);
      path = sanitizedUrl.path;
      querystring = sanitizedUrl.querystring;
      shouldDecodeParam = sanitizedUrl.shouldDecodeParam;
    } catch (error) {
      return undefined;
    }
    if (this.options.ignoreTrailingSlash) {
      path = trimLastSlash(path);
    }
    const originPath = path;
    if (this.options.caseSensitive === false) {
      path = path.toLowerCase();
    }
    const maxParamLength = this.options.maxParamLength;
    let pathIndex = currentNode.prefix.length;
    const params = [];
    const pathLen = path.length;
    const brothersNodesStack = [];
    while (true) {
      if (pathIndex === pathLen && currentNode.isLeafNode) {
        const handle = currentNode.handlerStorage?.find();
        if (handle !== undefined) {
          return {
            handler: handle.handler,
            params: handle.createParams(params),
            searchParams: QS.parse(querystring)
          };
        }
      }
      let node = currentNode.getNextNode(path, pathIndex, brothersNodesStack, params.length);
      if (node === undefined) {
        if (brothersNodesStack.length === 0) {
          return undefined;
        }
        const brotherNodeState = brothersNodesStack.pop();
        pathIndex = brotherNodeState.brotherPathIndex;
        params.splice(brotherNodeState.paramsCount);
        node = brotherNodeState.brotherNode;
      }
      currentNode = node;
      // static route
      if (currentNode._tag === "StaticNode") {
        pathIndex += currentNode.prefix.length;
        continue;
      }
      if (currentNode._tag === "WildcardNode") {
        let param = originPath.slice(pathIndex);
        if (shouldDecodeParam) {
          param = safeDecodeURIComponent(param);
        }
        params.push(param);
        pathIndex = pathLen;
        continue;
      }
      if (currentNode._tag === "ParametricNode") {
        let paramEndIndex = originPath.indexOf("/", pathIndex);
        if (paramEndIndex === -1) {
          paramEndIndex = pathLen;
        }
        let param = originPath.slice(pathIndex, paramEndIndex);
        if (shouldDecodeParam) {
          param = safeDecodeURIComponent(param);
        }
        if (currentNode.regex !== undefined) {
          const matchedParameters = currentNode.regex.exec(param);
          if (matchedParameters === null) continue;
          for (let i = 1; i < matchedParameters.length; i++) {
            const matchedParam = matchedParameters[i];
            if (matchedParam.length > maxParamLength) {
              return undefined;
            }
            params.push(matchedParam);
          }
        } else {
          if (param.length > maxParamLength) {
            return undefined;
          }
          params.push(param);
        }
        pathIndex = paramEndIndex;
      }
    }
  }
}
class HandlerStorage {
  handlers = [];
  unconstrainedHandler;
  find() {
    return this.unconstrainedHandler;
  }
  add(route) {
    const handler = {
      params: route.params,
      handler: route.handler,
      createParams: compileCreateParams(route.params)
    };
    this.handlers.push(handler);
    this.unconstrainedHandler = this.handlers[0];
  }
}
class NodeBase {
  isLeafNode = false;
  routes;
  handlerStorage;
  addRoute(route) {
    if (this.routes === undefined) {
      this.routes = [route];
    } else {
      this.routes.push(route);
    }
    if (this.handlerStorage === undefined) {
      this.handlerStorage = new HandlerStorage();
    }
    this.isLeafNode = true;
    this.handlerStorage.add(route);
  }
}
class ParentNode extends NodeBase {
  staticChildren = {};
  findStaticMatchingChild(path, pathIndex) {
    const staticChild = this.staticChildren[path.charAt(pathIndex)];
    if (staticChild === undefined || !staticChild.matchPrefix(path, pathIndex)) {
      return undefined;
    }
    return staticChild;
  }
  getStaticChild(path, pathIndex = 0) {
    if (path.length === pathIndex) {
      return this;
    }
    const staticChild = this.findStaticMatchingChild(path, pathIndex);
    if (staticChild === undefined) {
      return undefined;
    }
    return staticChild.getStaticChild(path, pathIndex + staticChild.prefix.length);
  }
  createStaticChild(path) {
    if (path.length === 0) {
      return this;
    }
    let staticChild = this.staticChildren[path.charAt(0)];
    if (staticChild) {
      let i = 1;
      for (; i < staticChild.prefix.length; i++) {
        if (path.charCodeAt(i) !== staticChild.prefix.charCodeAt(i)) {
          staticChild = staticChild.split(this, i);
          break;
        }
      }
      return staticChild.createStaticChild(path.slice(i));
    }
    const label = path.charAt(0);
    this.staticChildren[label] = new StaticNode(path);
    return this.staticChildren[label];
  }
}
class StaticNode extends ParentNode {
  _tag = "StaticNode";
  constructor(prefix) {
    super();
    this.setPrefix(prefix);
  }
  prefix;
  matchPrefix;
  parametricChildren = [];
  wildcardChild;
  setPrefix(prefix) {
    this.prefix = prefix;
    if (prefix.length === 1) {
      this.matchPrefix = (_path, _pathIndex) => true;
    } else {
      const len = prefix.length;
      this.matchPrefix = function (path, pathIndex) {
        for (let i = 1; i < len; i++) {
          if (path.charCodeAt(pathIndex + i) !== this.prefix.charCodeAt(i)) {
            return false;
          }
        }
        return true;
      };
    }
  }
  getParametricChild(regex) {
    if (regex === undefined) {
      return this.parametricChildren.find(child => child.isRegex === false);
    }
    const source = regex.source;
    return this.parametricChildren.find(child => {
      if (child.regex === undefined) {
        return false;
      }
      return child.regex.source === source;
    });
  }
  createParametricChild(regex, staticSuffix, nodePath) {
    let child = this.getParametricChild(regex);
    if (child !== undefined) {
      child.nodePaths.add(nodePath);
      return child;
    }
    child = new ParametricNode(regex, staticSuffix, nodePath);
    this.parametricChildren.push(child);
    this.parametricChildren.sort((child1, child2) => {
      if (!child1.isRegex) return 1;
      if (!child2.isRegex) return -1;
      if (child1.staticSuffix === undefined) return 1;
      if (child2.staticSuffix === undefined) return -1;
      if (child2.staticSuffix.endsWith(child1.staticSuffix)) return 1;
      if (child1.staticSuffix.endsWith(child2.staticSuffix)) return -1;
      return 0;
    });
    return child;
  }
  createWildcardChild() {
    if (this.wildcardChild === undefined) {
      this.wildcardChild = new WildcardNode();
    }
    return this.wildcardChild;
  }
  split(parentNode, length) {
    const parentPrefix = this.prefix.slice(0, length);
    const childPrefix = this.prefix.slice(length);
    this.setPrefix(childPrefix);
    const staticNode = new StaticNode(parentPrefix);
    staticNode.staticChildren[childPrefix.charAt(0)] = this;
    parentNode.staticChildren[parentPrefix.charAt(0)] = staticNode;
    return staticNode;
  }
  getNextNode(path, pathIndex, nodeStack, paramsCount) {
    let node = this.findStaticMatchingChild(path, pathIndex);
    let parametricBrotherNodeIndex = 0;
    if (node === undefined) {
      if (this.parametricChildren.length === 0) {
        return this.wildcardChild;
      }
      node = this.parametricChildren[0];
      parametricBrotherNodeIndex = 1;
    }
    if (this.wildcardChild !== undefined) {
      nodeStack.push({
        paramsCount,
        brotherPathIndex: pathIndex,
        brotherNode: this.wildcardChild
      });
    }
    for (let i = this.parametricChildren.length - 1; i >= parametricBrotherNodeIndex; i--) {
      nodeStack.push({
        paramsCount,
        brotherPathIndex: pathIndex,
        brotherNode: this.parametricChildren[i]
      });
    }
    return node;
  }
}
class ParametricNode extends ParentNode {
  regex;
  staticSuffix;
  _tag = "ParametricNode";
  constructor(regex, staticSuffix, nodePath) {
    super();
    this.regex = regex;
    this.staticSuffix = staticSuffix;
    this.isRegex = !!regex;
    this.nodePaths = new Set([nodePath]);
  }
  isRegex;
  nodePaths;
  getNextNode(path, pathIndex) {
    return this.findStaticMatchingChild(path, pathIndex);
  }
}
class WildcardNode extends NodeBase {
  _tag = "WildcardNode";
  getNextNode(_path, _pathIndex, _nodeStack, _paramsCount) {
    return undefined;
  }
}
const assert = (condition, message) => {
  if (!condition) {
    throw new Error(message);
  }
};
function removeDuplicateSlashes(path) {
  return path.replace(/\/\/+/g, "/");
}
function trimLastSlash(path) {
  if (path.length > 1 && path.charCodeAt(path.length - 1) === 47) {
    return path.slice(0, -1);
  }
  return path;
}
function compileCreateParams(params) {
  const len = params.length;
  return function (paramsArray) {
    const paramsObject = {};
    for (let i = 0; i < len; i++) {
      paramsObject[params[i]] = paramsArray[i];
    }
    return paramsObject;
  };
}
function getClosingParenthensePosition(path, idx) {
  // `path.indexOf()` will always return the first position of the closing parenthese,
  // but it's inefficient for grouped or wrong regexp expressions.
  // see issues #62 and #63 for more info
  let parentheses = 1;
  while (idx < path.length) {
    idx++;
    // ignore skipped chars
    if (path[idx] === "\\") {
      idx++;
      continue;
    }
    if (path[idx] === ")") {
      parentheses--;
    } else if (path[idx] === "(") {
      parentheses++;
    }
    if (!parentheses) return idx;
  }
  throw new TypeError('Invalid regexp expression in "' + path + '"');
}
function trimRegExpStartAndEnd(regexString) {
  // removes chars that marks start "^" and end "$" of regexp
  if (regexString.charCodeAt(1) === 94) {
    regexString = regexString.slice(0, 1) + regexString.slice(2);
  }
  if (regexString.charCodeAt(regexString.length - 2) === 36) {
    regexString = regexString.slice(0, regexString.length - 2) + regexString.slice(regexString.length - 1);
  }
  return regexString;
}
function escapeRegExp(string) {
  return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
// It must spot all the chars where decodeURIComponent(x) !== decodeURI(x)
// The chars are: # $ & + , / : ; = ? @
function decodeComponentChar(highCharCode, lowCharCode) {
  if (highCharCode === 50) {
    if (lowCharCode === 53) return "%";
    if (lowCharCode === 51) return "#";
    if (lowCharCode === 52) return "$";
    if (lowCharCode === 54) return "&";
    if (lowCharCode === 66) return "+";
    if (lowCharCode === 98) return "+";
    if (lowCharCode === 67) return ",";
    if (lowCharCode === 99) return ",";
    if (lowCharCode === 70) return "/";
    if (lowCharCode === 102) return "/";
    return undefined;
  }
  if (highCharCode === 51) {
    if (lowCharCode === 65) return ":";
    if (lowCharCode === 97) return ":";
    if (lowCharCode === 66) return ";";
    if (lowCharCode === 98) return ";";
    if (lowCharCode === 68) return "=";
    if (lowCharCode === 100) return "=";
    if (lowCharCode === 70) return "?";
    if (lowCharCode === 102) return "?";
    return undefined;
  }
  if (highCharCode === 52 && lowCharCode === 48) {
    return "@";
  }
  return undefined;
}
function safeDecodeURI(path) {
  let shouldDecode = false;
  let shouldDecodeParam = false;
  let querystring = "";
  for (let i = 1; i < path.length; i++) {
    const charCode = path.charCodeAt(i);
    if (charCode === 37) {
      const highCharCode = path.charCodeAt(i + 1);
      const lowCharCode = path.charCodeAt(i + 2);
      if (decodeComponentChar(highCharCode, lowCharCode) === undefined) {
        shouldDecode = true;
      } else {
        shouldDecodeParam = true;
        // %25 - encoded % char. We need to encode one more time to prevent double decoding
        if (highCharCode === 50 && lowCharCode === 53) {
          shouldDecode = true;
          path = path.slice(0, i + 1) + "25" + path.slice(i + 1);
          i += 2;
        }
        i += 2;
      }
      // Some systems do not follow RFC and separate the path and query
      // string with a `;` character (code 59), e.g. `/foo;jsessionid=123456`.
      // Thus, we need to split on `;` as well as `?` and `#`.
    } else if (charCode === 63 || charCode === 59 || charCode === 35) {
      querystring = path.slice(i + 1);
      path = path.slice(0, i);
      break;
    }
  }
  const decodedPath = shouldDecode ? decodeURI(path) : path;
  return {
    path: decodedPath,
    querystring,
    shouldDecodeParam
  };
}
function safeDecodeURIComponent(uriComponent) {
  const startIndex = uriComponent.indexOf("%");
  if (startIndex === -1) return uriComponent;
  let decoded = "";
  let lastIndex = startIndex;
  for (let i = startIndex; i < uriComponent.length; i++) {
    if (uriComponent.charCodeAt(i) === 37) {
      const highCharCode = uriComponent.charCodeAt(i + 1);
      const lowCharCode = uriComponent.charCodeAt(i + 2);
      const decodedChar = decodeComponentChar(highCharCode, lowCharCode);
      decoded += uriComponent.slice(lastIndex, i) + decodedChar;
      lastIndex = i + 3;
    }
  }
  return uriComponent.slice(0, startIndex) + decoded + uriComponent.slice(lastIndex);
}
const httpMethods = ["ACL", "BIND", "CHECKOUT", "CONNECT", "COPY", "DELETE", "GET", "HEAD", "LINK", "LOCK", "M-SEARCH", "MERGE", "MKACTIVITY", "MKCALENDAR", "MKCOL", "MOVE", "NOTIFY", "OPTIONS", "PATCH", "POST", "PROPFIND", "PROPPATCH", "PURGE", "PUT", "REBIND", "REPORT", "SEARCH", "SOURCE", "SUBSCRIBE", "TRACE", "UNBIND", "UNLINK", "UNLOCK", "UNSUBSCRIBE"];
//# sourceMappingURL=router.js.map