#
#
#            Nimrod's Runtime Library
#        (c) Copyright 2012 Andreas Rumpf
#
#    See the file "copying.txt", included in this
#    distribution, for details about the copyright.
#

## This module implements a higher level wrapper for `mongodb`:idx:. Example:
##
## .. code-block:: nimrod
##
##    import mongo, db_mongo, oids, json
##
##    var conn = db_mongo.open()
##
##    # construct JSON data:
##    var data = %{"a": %13, "b": %"my string value", 
##                 "inner": %{"i": %71} }
##
##    var id = insertID(conn, "test.test", data)
##
##    for v in find(conn, "test.test", "this.a == 13"):
##      print v
##
##    delete(conn, "test.test", id)
##    close(conn)

import mongo, oids, json

type
  EDb* = object of EIO ## exception that is raised if a database error occurs
  TDbConn* = TMongo    ## a database connection; alias for ``TMongo``

  FDb* = object of FIO ## effect that denotes a database operation
  FReadDb* = object of FDB   ## effect that denotes a read operation
  FWriteDb* = object of FDB  ## effect that denotes a write operation

proc dbError*(db: TDbConn, msg: string) {.noreturn.} = 
  ## raises an EDb exception with message `msg`.
  var e: ref EDb
  new(e)
  if db.errstr[0] != '\0':
    e.msg = $db.errstr
  else:
    e.msg = $db.err & " " & msg
  raise e

proc Close*(db: var TDbConn) {.tags: [FDB].} = 
  ## closes the database connection.
  disconnect(db)
  destroy(db)

proc Open*(host: string = defaultHost, port: int = defaultPort): TDbConn {.
  tags: [FDB].} =
  ## opens a database connection. Raises `EDb` if the connection could not
  ## be established.
  init(result)
  
  let x = connect(result, host, port.cint)
  if x != 0'i32:
    dbError(result, "cannot open: " & host)

proc jsonToBSon(b: var TBSon, key: string, j: PJsonNode) =
  case j.kind
  of JString:
    add(b, key, j.str)
  of JInt:
    add(b, key, j.num)
  of JFloat:
    add(b, key, j.fnum)
  of JBool:
    addBool(b, key, ord(j.bval))
  of JNull:
    addNull(b, key)
  of JObject:
    addStartObject(b, key)
    for k, v in items(j.fields):
      jsonToBSon(b, k, v)
    addFinishObject(b)
  of JArray:
    addStartArray(b, key)
    for i, e in pairs(j.elems):
      jsonToBSon(b, $i, e)
    addFinishArray(b)

proc jsonToBSon*(j: PJsonNode, oid: TOid): TBSon =
  ## converts a JSON value into the BSON format. The result must be
  ## ``destroyed`` explicitely!
  init(result)
  assert j.kind == JObject
  add(result, "_id", oid)
  for key, val in items(j.fields):
    jsonToBSon(result, key, val)
  finish(result)

proc `[]`*(obj: var TBSon, fieldname: cstring): TBSon =
  ## retrieves the value belonging to `fieldname`. Raises `EInvalidKey` if
  ## the attribute does not exist.
  var it = initIter(obj)
  let res = find(it, result, fieldname)
  if res == bkEOO:
    raise newException(EInvalidIndex, "key not in object")

proc getId*(obj: var TBSon): TOid =
  ## retrieves the ``_id`` attribute of `obj`.
  var it = initIter(obj)
  var b: TBSon
  let res = find(it, b, "_id")
  if res == bkOID:
    result = oidVal(it)[]
  else:
    raise newException(EInvalidIndex, "_id not in object")

proc insertID*(db: var TDbConn, namespace: string, data: PJsonNode): TOid {.
  tags: [FWriteDb].} =
  ## converts `data` to BSON format and inserts it in `namespace`. Returns
  ## the generated OID for the ``_id`` field.
  result = genOid()
  var x = jsonToBSon(data, result)
  insert(db, namespace, x)
  destroy(x)

proc insert*(db: var TDbConn, namespace: string, data: PJsonNode) {.
  tags: [FWriteDb].} =
  ## converts `data` to BSON format and inserts it in `namespace`.  
  discard InsertID(db, namespace, data)

proc update*(db: var TDbConn, namespace: string, obj: var TBSon) {.
  tags: [FReadDB, FWriteDb].} =
  ## updates `obj` in `namespace`.
  var cond: TBson
  init(cond)
  cond.add("_id", getId(obj))
  finish(cond)
  update(db, namespace, cond, obj, ord(UPDATE_UPSERT))
  destroy(cond)

proc update*(db: var TDbConn, namespace: string, oid: TOid, obj: PJsonNode) {.
  tags: [FReadDB, FWriteDb].} =
  ## updates the data with `oid` to have the new data `obj`.
  var a = jsonToBSon(obj, oid)
  Update(db, namespace, a)
  destroy(a)

proc delete*(db: var TDbConn, namespace: string, oid: TOid) {.
  tags: [FWriteDb].} =
  ## Deletes the object belonging to `oid`.
  var cond: TBson
  init(cond)
  cond.add("_id", oid)
  finish(cond)
  discard remove(db, namespace, cond)
  destroy(cond)

proc delete*(db: var TDbConn, namespace: string, obj: var TBSon) {.
  tags: [FWriteDb].} =
  ## Deletes the object `obj`.
  delete(db, namespace, getId(obj))

iterator find*(db: var TDbConn, namespace: string): var TBSon {.
  tags: [FReadDB].} =
  ## iterates over any object in `namespace`.
  var cursor: TCursor
  init(cursor, db, namespace)
  while next(cursor) == mongo.OK:
    yield bson(cursor)[]
  destroy(cursor)

iterator find*(db: var TDbConn, namespace: string, 
               query, fields: var TBSon): var TBSon {.tags: [FReadDB].} =
  ## yields the `fields` of any document that suffices `query`.
  var cursor = find(db, namespace, query, fields, 0'i32, 0'i32, 0'i32)
  if cursor != nil:
    while next(cursor[]) == mongo.OK:
      yield bson(cursor[])[]
    destroy(cursor[])

proc setupFieldnames(fields: varargs[string]): TBSon =
  init(result)
  for x in fields: add(result, x, 1'i32)
  finish(result)

iterator find*(db: var TDbConn, namespace: string, 
               query: var TBSon, fields: varargs[string]): var TBSon {.
               tags: [FReadDB].} =
  ## yields the `fields` of any document that suffices `query`. If `fields` 
  ## is ``[]`` the whole document is yielded.
  var f = setupFieldnames(fields)
  var cursor = find(db, namespace, query, f, 0'i32, 0'i32, 0'i32)
  if cursor != nil:
    while next(cursor[]) == mongo.OK:
      yield bson(cursor[])[]
    destroy(cursor[])
  destroy(f)

proc setupQuery(query: string): TBSon =
  init(result)
  add(result, "$where", query)
  finish(result)

iterator find*(db: var TDbConn, namespace: string, 
               query: string, fields: varargs[string]): var TBSon {.
               tags: [FReadDB].} =
  ## yields the `fields` of any document that suffices `query`. If `fields` 
  ## is ``[]`` the whole document is yielded.
  var f = setupFieldnames(fields)
  var q = setupQuery(query)
  var cursor = find(db, namespace, q, f, 0'i32, 0'i32, 0'i32)
  if cursor != nil:
    while next(cursor[]) == mongo.OK:
      yield bson(cursor[])[]
    destroy(cursor[])
  destroy(q)
  destroy(f)

when false:
  # this doesn't work this way; would require low level hacking
  iterator fieldPairs*(obj: var TBSon): tuple[key: cstring, value: TBSon] =
    ## iterates over `obj` and yields all (key, value)-Pairs.
    var it = initIter(obj)
    var v: TBSon
    while next(it) != bkEOO:
      let key = key(it)
      discard init(v, value(it))
      yield (key, v)
