import std/[macros, strutils, tables]
import db_connector/db_common

import ./db_types
import ./parsesql_tmp

const
  FileHeader* = """
# Generated by ormin_importer. DO NOT EDIT.

type
  Attr = object
    name: string
    tabIndex: int
    typ: DbTypekind
    key: int   # 0 nothing special,
               # +1 -- primary key
               # -N -- references attribute N
"""

type
  DbColumn* = object
    name*: string
    tableName*: string
    typ*: DbType
    primaryKey*: bool
    refs*: (string, string)
  DbColumns* = seq[DbColumn]

  KnownTables* = OrderedTable[string, DbColumns]
  ImportTarget* = enum
    postgre, sqlite, mysql, baradb

proc hasAttribute(colDesc: SqlNode; k: set[SqlNodeKind]): bool =
  for i in 2 ..< colDesc.len:
    if colDesc[i].kind in k:
      return true

proc hasRefs(colDesc: SqlNode): (string, string) =
  for i in 2 ..< colDesc.len:
    let c = colDesc[i]
    if c.kind == nkReferences:
      if c[0].kind == nkCall:
        return (c[0][0].strVal, c[0][1].strVal)
      elif c[0].kind == nkIdent:
        return ($c[0], "id")
  ("", "")

proc getType(n: SqlNode): DbType =
  var it = n
  if it.kind == nkCall:
    it = it[0]
  if it.kind == nkEnumDef:
    result.kind = dbEnum
    result.validValues = @[]
    for i in 0 ..< it.len:
      assert it[i].kind == nkStringLit
      result.validValues.add it[i].strVal
  elif it.kind in {nkIdent, nkStringLit}:
    result.kind = dbTypFromName(it.strVal)
    result.name = it.strVal

proc collectTables*(n: SqlNode; t: var KnownTables) =
  if n.isNil:
    return
  case n.kind
  of nkCreateTable, nkCreateTableIfNotExists:
    let tableName = n[0].strVal
    var cols: DbColumns = @[]
    for i in 1 ..< n.len:
      let it = n[i]
      if it.kind == nkColumnDef:
        var typ = getType(it[1])
        if hasAttribute(it, {nkNotNull}):
          typ.notNull = true
        cols.add DbColumn(
          name: it[0].strVal,
          tableName: tableName,
          typ: typ,
          primaryKey: hasAttribute(it, {nkPrimaryKey}),
          refs: hasRefs(it)
        )
    for i in 1 ..< n.len:
      let it = n[i]
      if it.kind == nkForeignKey:
        var refNode: SqlNode = nil
        var localCols: seq[string] = @[]
        for j in 0 ..< it.len:
          let child = it[j]
          if child.kind == nkReferences:
            refNode = child
          elif child.kind == nkIdent:
            localCols.add(child.strVal)
        if refNode.isNil:
          continue
        var r = refNode[0]
        var refTable = ""
        var refCols: seq[string] = @[]
        if r.kind == nkColumnReference or r.kind == nkCall:
          refTable = r[0].strVal
          for k in 1 ..< r.len:
            refCols.add(r[k].strVal)
        let pairCount = min(localCols.len, refCols.len)
        for k in 0 ..< pairCount:
          let localName = localCols[k]
          let targetCol = refCols[k]
          for c in mitems(cols):
            if cmpIgnoreCase(c.name, localName) == 0:
              c.refs = (refTable, targetCol)
              break
    t[tableName] = cols
  else:
    for i in 0 ..< n.len:
      collectTables(n[i], t)

proc attrToKey(a: DbColumn; t: KnownTables): int =
  if a.primaryKey:
    return 1
  if a.refs[0].len > 0:
    var i = 0
    for k, v in pairs(t):
      for b in v:
        if cmpIgnoreCase(k, a.refs[0]) == 0 and cmpIgnoreCase(b.name, a.refs[1]) == 0:
          return -i - 1
        inc i
  0

proc renderModelCode(schemaSql, schemaPath: string; target: ImportTarget; includeStatic = false): string =
  discard target
  let sql = parseSql(schemaSql, schemaPath)
  var knownTables = initOrderedTable[string, DbColumns]()
  collectTables(sql, knownTables)

  result.add FileHeader
  result.add "const tableNames = ["
  var i = 0
  for k in keys(knownTables):
    if i > 0:
      result.add ",\n  "
    else:
      result.add "\n  "
    result.add escape(k.toLowerAscii)
    inc i
  result.add "\n]\n"

  i = 0
  var j = 0
  result.add "\nconst attributes = ["
  for _, v in mpairs(knownTables):
    for a in v:
      if j > 0:
        result.add ",\n  "
      else:
        result.add "\n  "
      result.add "Attr(name: "
      result.add escape(a.name.toLowerAscii)
      result.add ", tabIndex: "
      result.add $i
      result.add ", typ: "
      result.add $a.typ.kind
      result.add ", key: "
      result.add $attrToKey(a, knownTables)
      result.add ")"
      inc j
    inc i
  result.add "\n]\n"

  if includeStatic:
    result.add "\nconst sqlSchema* = staticLoad("
    result.add escape(schemaPath)
    result.add ")\n"

proc generateModelCode*(schemaSql, schemaPath: string; target: ImportTarget; includeStatic = false): string =
  renderModelCode(schemaSql, schemaPath, target, includeStatic)

macro generateModelCode*(schemaSql: static[string], schemaPath: static[string],
                         target: static[ImportTarget], includeStatic: static[bool] = false): untyped =
  parseStmt(renderModelCode(schemaSql, schemaPath, target, includeStatic))
