# Copyright (C) 2012-2018 Dominik Picheta
# MIT License - Look at license.txt for details.
import parseutils, tables
type
  NodeType* = enum
    NodeText, NodeField
  Node* = object
    typ*: NodeType
    text*: string
    optional*: bool
  
  Pattern* = seq[Node]

#/show/@id/?
proc parsePattern*(pattern: string): Pattern =
  result = @[]
  template addNode(result: var Pattern, theT: NodeType, theText: string,
                   isOptional: bool): typed =
    block:
      var newNode: Node
      newNode.typ = theT
      newNode.text = theText
      newNode.optional = isOptional
      result.add(newNode)

  template `{}`(s: string, i: int): char =
    if i >= len(s):
      '\0'
    else:
      s[i]

  var i = 0
  var text = ""
  while i < pattern.len():
    case pattern[i]
    of '@':
      # Add the stored text.
      if text != "":
        result.addNode(NodeText, text, false)
        text = ""
      # Parse named parameter.
      inc(i) # Skip @
      var nparam = ""
      i += pattern.parseUntil(nparam, {'/', '?'}, i)
      var optional = pattern{i} == '?'
      result.addNode(NodeField, nparam, optional)
      if pattern{i} == '?': inc(i) # Only skip ?. / should not be skipped.
    of '?':
      var optionalChar = text[^1]
      setLen(text, text.len-1) # Truncate ``text``.
      # Add the stored text.
      if text != "":
        result.addNode(NodeText, text, false)
        text = ""
      # Add optional char.
      inc(i) # Skip ?
      result.addNode(NodeText, $optionalChar, true)
    of '\\':
      inc i # Skip \
      if pattern[i] notin {'?', '@', '\\'}:
        raise newException(ValueError, 
                "This character does not require escaping: " & pattern[i])
      text.add(pattern{i})
      inc i # Skip ``pattern[i]``
    else:
      text.add(pattern{i})
      inc(i)
  
  if text != "":
    result.addNode(NodeText, text, false)

proc findNextText(pattern: Pattern, i: int, toNode: var Node): bool =
  ## Finds the next NodeText in the pattern, starts looking from ``i``.
  result = false
  for n in i..pattern.len()-1:
    if pattern[n].typ == NodeText:
      toNode = pattern[n]
      return true

proc check(n: Node, s: string, i: int): bool =
  let cutTo = (n.text.len-1)+i
  if cutTo > s.len-1: return false
  return s.substr(i, cutTo) == n.text

proc match*(pattern: Pattern, s: string):
      tuple[matched: bool, params: Table[string, string]] =
  var i = 0 # Location in ``s``.

  result.matched = true
  result.params = initTable[string, string]()

  for ncount, node in pattern:
    case node.typ
    of NodeText:
      if node.optional:
        if check(node, s, i):
          inc(i, node.text.len) # Skip over this optional character.
        else:
          # If it's not there, we have nothing to do. It's optional after all.
          discard
      else:
        if check(node, s, i):
          inc(i, node.text.len) # Skip over this
        else:
          # No match.
          result.matched = false
          return
    of NodeField:
      var nextTxtNode: Node
      var stopChar = '/'
      if findNextText(pattern, ncount, nextTxtNode):
        stopChar = nextTxtNode.text[0]
      var matchNamed = ""
      i += s.parseUntil(matchNamed, stopChar, i)
      result.params[node.text] = matchNamed
      if matchNamed == "" and not node.optional:
        result.matched = false
        return

  if s.len != i:
    result.matched = false

when true:
  let f = parsePattern("/show/@id/test/@show?/?")
  doAssert match(f, "/show/12/test/hallo/").matched
  doAssert match(f, "/show/2131726/test/jjjuuwąąss").matched
  doAssert(not match(f, "/").matched)
  doAssert(not match(f, "/show//test//").matched)
  doAssert(match(f, "/show/asd/test//").matched)
  doAssert(not match(f, "/show/asd/asd/test/jjj/").matched)
  doAssert(match(f, "/show/@łę¶ŧ←/test/asd/").params["id"] == "@łę¶ŧ←")

  let f2 = parsePattern("/test42/somefile.?@ext?/?")
  doAssert(match(f2, "/test42/somefile/").params["ext"] == "")
  doAssert(match(f2, "/test42/somefile.txt").params["ext"] == "txt")
  doAssert(match(f2, "/test42/somefile.txt/").params["ext"] == "txt")
  
  let f3 = parsePattern(r"/test32/\@\\\??")
  doAssert(match(f3, r"/test32/@\").matched)
  doAssert(not match(f3, r"/test32/@\\").matched)
  doAssert(match(f3, r"/test32/@\?").matched)
