import
  re, json, strutils, tables, math, os, math_helpers,
  sg_packets, md5, zlib_helpers

when defined(NoSFML):
  import server_utils
  type TVector2i = object
    x*, y*: int32
  proc vec2i(x, y: int32): TVector2i =
    result.x = x
    result.y = y
else:
  import sfml, sfml_audio, sfml_stuff
when not defined(NoChipmunk):
  import chipmunk

type
  TChecksumFile* = object
    unpackedSize*: int
    sum*: MD5Digest
    compressed*: string
  PZoneSettings* = ref TZoneSettings
  TZoneSettings* = object
    vehicles: seq[PVehicleRecord]
    items: seq[PItemRecord]
    objects: seq[PObjectRecord]
    bullets: seq[PBulletRecord]
    levelSettings: PLevelSettings
  PLevelSettings* = ref TLevelSettings
  TLevelSettings* = object
    size*: TVector2i
    starfield*: seq[PSpriteSheet]
  PVehicleRecord* = ref TVehicleRecord
  TVehicleRecord* = object
    id*: int16
    name*: string
    playable*: bool
    anim*: PAnimationRecord
    physics*: TPhysicsRecord
    handling*: THandlingRecord
  TItemKind* = enum
    Projectile, Utility, Ammo
  PObjectRecord* = ref TObjectRecord
  TObjectRecord* = object
    id*: int16
    name*: string
    anim*: PAnimationRecord
    physics*: TPhysicsRecord
  PItemRecord* = ref TItemRecord
  TItemRecord* = object
    id*: int16
    name*: string
    anim*: PAnimationRecord
    physics*: TPhysicsRecord ##apply when the item is dropped in the arena
    cooldown*: float
    energyCost*: float
    useSound*: PSoundRecord
    case kind*: TItemKind
    of Projectile:
      bullet*: PBulletRecord
    else:
      discard
  PBulletRecord* = ref TBulletRecord
  TBulletRecord* = object
    id*: int16
    name*: string
    anim*: PAnimationRecord
    physics*: TPhysicsRecord
    lifetime*, inheritVelocity*, baseVelocity*: float
    explosion*: TExplosionRecord
    trail*: TTrailRecord
  TTrailRecord* = object
    anim*: PAnimationRecord
    timer*: float ##how often it should be created
  TPhysicsRecord* = object
    mass*: float
    radius*: float
    moment*: float
  THandlingRecord = object
    thrust*, top_speed*: float
    reverse*, strafe*, rotation*: float
  TSoulRecord = object
    energy*: int
    health*: int
  TExplosionRecord* = object
    anim*: PAnimationRecord
    sound*: PSoundRecord
  PAnimationRecord* = ref TAnimationRecord
  TAnimationRecord* = object
    spriteSheet*: PSpriteSheet
    angle*: float
    delay*: float  ##animation delay
  PSoundRecord* = ref TSoundRecord
  TSoundRecord* = object
    file*: string
    when defined(NoSFML):
      contents*: TChecksumFile
    else:
      soundBuf*: PSoundBuffer
  PSpriteSheet* = ref TSpriteSheet
  TSpriteSheet* = object
    file*: string
    framew*,frameh*: int
    rows*, cols*: int
    when defined(NoSFML):
      contents*: TChecksumFile
    when not defined(NoSFML):
      sprite*: PSprite
      tex*: PTexture
  TGameState* = enum
    Lobby, Transitioning, Field
const
  MomentMult* = 0.62 ## global moment of inertia multiplier
var
  cfg: PZoneSettings
  SpriteSheets* = initTable[string, PSpriteSheet](64)
  SoundCache  * = initTable[string, PSoundRecord](64)
  nameToVehID*: Table[string, int]
  nameToItemID*: Table[string, int]
  nameToObjID*: Table[string, int]
  nameToBulletID*: Table[string, int]
  activeState = Lobby

proc newSprite*(filename: string; errors: var seq[string]): PSpriteSheet
proc load*(ss: PSpriteSheet): bool {.discardable.}
proc newSound*(filename: string; errors: var seq[string]): PSoundRecord
proc load*(s: PSoundRecord): bool {.discardable.}

proc validateSettings*(settings: JsonNode; errors: var seq[string]): bool
proc loadSettings*(rawJson: string, errors: var seq[string]): bool
proc loadSettingsFromFile*(filename: string, errors: var seq[string]): bool

proc fetchVeh*(name: string): PVehicleRecord
proc fetchItm*(itm: string): PItemRecord
proc fetchObj*(name: string): PObjectRecord
proc fetchBullet(name: string): PBulletRecord

proc importLevel(data: JsonNode; errors: var seq[string]): PLevelSettings
proc importVeh(data: JsonNode; errors: var seq[string]): PVehicleRecord
proc importObject(data: JsonNode; errors: var seq[string]): PObjectRecord
proc importItem(data: JsonNode; errors: var seq[string]): PItemRecord
proc importPhys(data: JsonNode): TPhysicsRecord
proc importAnim(data: JsonNode; errors: var seq[string]): PAnimationRecord
proc importHandling(data: JsonNode): THandlingRecord
proc importBullet(data: JsonNode; errors: var seq[string]): PBulletRecord
proc importSoul(data: JsonNode): TSoulRecord
proc importExplosion(data: JsonNode; errors: var seq[string]): TExplosionRecord
proc importSound*(data: JsonNode; errors: var seq[string]; fieldName: string = ""): PSoundRecord

## this is the only pipe between lobby and main.nim
proc getActiveState*(): TGameState =
  result = activeState
proc transition*() =
  assert activeState == Lobby, "Transition() called from a state other than lobby!"
  activeState = Transitioning
proc doneWithSaidTransition*() =
  assert activeState == Transitioning, "Finished() called from a state other than transitioning!"
  activeState = Field


proc checksumFile*(filename: string): TChecksumFile =
  let fullText = readFile(filename)
  result.unpackedSize = fullText.len
  result.sum = toMD5(fullText)
  result.compressed = compress(fullText)
proc checksumStr*(str: string): TChecksumFile =
  result.unpackedSize = str.len
  result.sum = toMD5(str)
  result.compressed = compress(str)


##at this point none of these should ever be freed
proc free*(obj: PZoneSettings) =
  echo "Free'd zone settings"
proc free*(obj: PSpriteSheet) =
  echo "Free'd ", obj.file
proc free*(obj: PSoundRecord) =
  echo "Free'd ", obj.file

proc loadAllAssets*() =
  var
    loaded = 0
    failed = 0
  for name, ss in SpriteSheets.pairs():
    if load(ss):
      inc loaded
    else:
      inc failed
  echo loaded," sprites loaded. ", failed, " sprites failed."
  loaded = 0
  failed = 0
  for name, s in SoundCache.pairs():
    if load(s):
      inc loaded
    else:
      inc failed
  echo loaded, " sounds loaded. ", failed, " sounds failed."
proc getLevelSettings*(): PLevelSettings =
  result = cfg.levelSettings

iterator playableVehicles*(): PVehicleRecord =
  for v in cfg.vehicles.items():
    if v.playable:
      yield v

template allAssets*(body: untyped) {.dirty.}=
  block:
    var assetType = FGraphics
    for file, asset in pairs(SpriteSheets):
      body
    assetType = FSound
    for file, asset in pairs(SoundCache):
      body

template cacheImpl(procName, cacheName, resultType, body: untyped) {.dirty.} =
  proc procName*(filename: string; errors: var seq[string]): resulttype =
    if hasKey(cacheName, filename):
      return cacheName[filename]
    new(result, free)
    body
    cacheName[filename] = result

template checkFile(path: untyped) {.dirty.} =
  if not existsFile(path):
    errors.add("File missing: " & path)

cacheImpl newSprite, SpriteSheets, PSpriteSheet:
  result.file = filename
  if filename =~ re"\S+_(\d+)x(\d+)\.\S\S\S":
    result.framew = strutils.parseInt(matches[0])
    result.frameh = strutils.parseInt(matches[1])
    checkFile("data/gfx"/result.file)
  else:
    errors.add "Bad file: " & filename & " must be in format name_WxH.png"
    return

cacheImpl newSound, SoundCache, PSoundRecord:
  result.file = filename
  checkFile("data/sfx"/result.file)

proc expandPath*(assetType: TAssetType; fileName: string): string =
  result = "data/"
  case assetType
  of FGraphics: result.add "gfx/"
  of FSound:    result.add "sfx/"
  else: discard
  result.add fileName
proc expandPath*(fc: ScFileChallenge): string {.inline.} =
  result = expandPath(fc.assetType, fc.file)

when defined(NoSFML):
  proc load*(ss: PSpriteSheet): bool =
    if not ss.contents.unpackedSize == 0: return
    ss.contents = checksumFile(expandPath(FGraphics, ss.file))
    result = true
  proc load*(s: PSoundRecord): bool =
    if not s.contents.unpackedSize == 0: return
    s.contents = checksumFile(expandPath(FSound, s.file))
    result = true
else:
  proc load*(ss: PSpriteSheet): bool =
    if not ss.sprite.isNil:
      return
    var image = sfml.newImage("data/gfx/"/ss.file)
    if image == nil:
      echo "Image could not be loaded"
      return
    let size = image.getSize()
    ss.rows = int(size.y / ss.frameh) #y is h
    ss.cols = int(size.x / ss.framew) #x is w
    ss.tex = newTexture(image)
    image.destroy()
    ss.sprite = newSprite()
    ss.sprite.setTexture(ss.tex, true)
    ss.sprite.setTextureRect(intrect(0, 0, ss.framew.cint, ss.frameh.cint))
    ss.sprite.setOrigin(vec2f(ss.framew / 2, ss.frameh / 2))
    result = true
  proc load*(s: PSoundRecord): bool =
    s.soundBuf = newSoundBuffer("data/sfx"/s.file)
    if not s.soundBuf.isNil:
      result = true

template addError(e: untyped) =
  errors.add(e)
  result = false
proc validateSettings*(settings: JsonNode, errors: var seq[string]): bool =
  result = true
  if settings.kind != JObject:
    addError("Settings root must be an object")
    return
  if not settings.hasKey("vehicles"):
    addError("Vehicles section missing")
  if not settings.hasKey("objects"):
    errors.add("Objects section is missing")
    result = false
  if not settings.hasKey("level"):
    errors.add("Level settings section is missing")
    result = false
  else:
    let lvl = settings["level"]
    if lvl.kind != JObject or not lvl.hasKey("size"):
      errors.add("Invalid level settings")
      result = false
    elif not lvl.hasKey("size") or lvl["size"].kind != JArray or lvl["size"].len != 2:
      errors.add("Invalid/missing level size")
      result = false
  if not settings.hasKey("items"):
    errors.add("Items section missing")
    result = false
  else:
    let items = settings["items"]
    if items.kind != JArray or items.len == 0:
      errors.add "Invalid or empty item list"
    else:
      var id = 0
      for i in items.items:
        if i.kind != JArray: errors.add("Item #$1 is not an array" % $id)
        elif i.len != 3: errors.add("($1) Item record should have 3 fields"%($id))
        elif i[0].kind != JString or i[1].kind != JString or i[2].kind != JObject:
          errors.add("($1) Item should be in form [name, type, {item: data}]" % $id)
          result = false
        inc id

proc loadSettingsFromFile*(filename: string, errors: var seq[string]): bool =
  if not existsFile(filename):
    errors.add("File does not exist: "&filename)
  else:
    result = loadSettings(readFile(filename), errors)

proc loadSettings*(rawJson: string, errors: var seq[string]): bool =
  var settings: JsonNode
  try:
    settings = parseJson(rawJson)
  except JsonParsingError:
    errors.add("JSON parsing error: " & getCurrentExceptionMsg())
    return
  except:
    errors.add("Unknown exception: " & getCurrentExceptionMsg())
    return
  if not validateSettings(settings, errors):
    return
  if cfg != nil: #TODO try this
    echo("Overwriting zone settings")
    free(cfg)
    cfg = nil
  new(cfg, free)
  cfg.levelSettings = importLevel(settings, errors)
  cfg.vehicles = @[]
  cfg.items = @[]
  cfg.objects = @[]
  cfg.bullets = @[]
  nameToVehID = initTable[string, int](32)
  nameToItemID = initTable[string, int](32)
  nameToObjID = initTable[string, int](32)
  nameToBulletID = initTable[string, int](32)
  var
    vID = 0'i16
    bID = 0'i16
  for vehicle in settings["vehicles"].items:
    var veh = importVeh(vehicle, errors)
    veh.id = vID
    cfg.vehicles.add veh
    nameToVehID[veh.name] = veh.id
    inc vID
  vID = 0
  if settings.hasKey("bullets"):
    for blt in settings["bullets"].items:
      var bullet = importBullet(blt, errors)
      bullet.id = bID
      cfg.bullets.add bullet
      nameToBulletID[bullet.name] = bullet.id
      inc bID
  for item in settings["items"].items:
    var itm = importItem(item, errors)
    itm.id = vID
    cfg.items.add itm
    nameToItemID[itm.name] = itm.id
    inc vID
    if itm.kind == Projectile:
      if itm.bullet.isNil:
        errors.add("Projectile #$1 has no bullet!" % $vID)
      elif itm.bullet.id == -1:
        ## this item has an anonymous bullet, fix the ID and name
        itm.bullet.id = bID
        itm.bullet.name = itm.name
        cfg.bullets.add itm.bullet
        nameToBulletID[itm.bullet.name] = itm.bullet.id
        inc bID
  vID = 0
  for obj in settings["objects"].items:
    var o = importObject(obj, errors)
    o.id = vID
    cfg.objects.add o
    nameToObjID[o.name] = o.id
    inc vID
  result = (errors.len == 0)

proc `$`*(obj: PSpriteSheet): string =
  return "<Sprite $1 ($2x$3) $4 rows $5 cols>" % [obj.file, $obj.framew, $obj.frameh, $obj.rows, $obj.cols]

proc fetchVeh*(name: string): PVehicleRecord =
  return cfg.vehicles[nameToVehID[name]]
proc fetchItm*(itm: string): PItemRecord =
  return cfg.items[nameToItemID[itm]]
proc fetchObj*(name: string): PObjectRecord =
  return cfg.objects[nameToObjID[name]]
proc fetchBullet(name: string): PBulletRecord =
  return cfg.bullets[nameToBulletID[name]]

proc getField(node: JsonNode, field: string, target: var float) =
  if not node.hasKey(field):
    return
  if node[field].kind == JFloat:
    target = node[field].fnum
  elif node[field].kind == JInt:
    target = node[field].num.float
proc getField(node: JsonNode, field: string, target: var int) =
  if not node.hasKey(field):
    return
  if node[field].kind == JInt:
    target = node[field].num.int
  elif node[field].kind == JFloat:
    target = node[field].fnum.int
proc getField(node: JsonNode; field: string; target: var bool) =
  if not node.hasKey(field):
    return
  case node[field].kind
  of JBool:
    target = node[field].bval
  of JInt:
    target = (node[field].num != 0)
  of JFloat:
    target = (node[field].fnum != 0.0)
  else: discard

template checkKey(node: untyped; key: string) =
  if not hasKey(node, key):
    return

proc importTrail(data: JsonNode; errors: var seq[string]): TTrailRecord =
  checkKey(data, "trail")
  result.anim = importAnim(data["trail"], errors)
  result.timer = 1000.0
  getField(data["trail"], "timer", result.timer)
  result.timer /= 1000.0
proc importLevel(data: JsonNode; errors: var seq[string]): PLevelSettings =
  new(result)
  result.size = vec2i(5000, 5000)
  result.starfield = @[]

  checkKey(data, "level")
  var level = data["level"]
  if level.hasKey("size") and level["size"].kind == JArray and level["size"].len == 2:
    result.size.x = level["size"][0].num.cint
    result.size.y = level["size"][1].num.cint
  if level.hasKey("starfield"):
    for star in level["starfield"].items:
      result.starfield.add(newSprite(star.str, errors))
proc importPhys(data: JsonNode): TPhysicsRecord =
  result.radius = 20.0
  result.mass = 10.0

  if data.hasKey("physics") and data["physics"].kind == JObject:
    let phys = data["physics"]
    phys.getField("radius", result.radius)
    phys.getField("mass", result.mass)
  when not defined(NoChipmunk):
    result.moment = momentForCircle(result.mass, 0.0, result.radius, VectorZero) * MomentMult
proc importHandling(data: JsonNode): THandlingRecord =
  result.thrust = 45.0
  result.topSpeed = 100.0 #unused
  result.reverse = 30.0
  result.strafe = 30.0
  result.rotation = 2200.0

  checkKey(data, "handling")
  if data["handling"].kind != JObject:
    return

  let hand = data["handling"]
  hand.getField("thrust", result.thrust)
  hand.getField("top_speed", result.topSpeed)
  hand.getField("reverse", result.reverse)
  hand.getField("strafe", result.strafe)
  hand.getField("rotation", result.rotation)
proc importAnim(data: JsonNode, errors: var seq[string]): PAnimationRecord =
  new(result)
  result.angle = 0.0
  result.delay = 1000.0
  result.spriteSheet = nil

  if data.hasKey("anim"):
    let anim = data["anim"]
    if anim.kind == JObject:
      if anim.hasKey("file"):
        result.spriteSheet = newSprite(anim["file"].str, errors)

      anim.getField "angle", result.angle
      anim.getField "delay", result.delay
    elif data["anim"].kind == JString:
      result.spriteSheet = newSprite(anim.str, errors)

  result.angle = radians(result.angle) ## comes in as degrees
  result.delay /= 1000 ## delay comes in as milliseconds
proc importSoul(data: JsonNode): TSoulRecord =
  result.energy = 10000
  result.health = 1
  checkKey(data, "soul")
  let soul = data["soul"]
  soul.getField("energy", result.energy)
  soul.getField("health", result.health)
proc importExplosion(data: JsonNode; errors: var seq[string]): TExplosionRecord =
  checkKey(data, "explode")
  let expl = data["explode"]
  result.anim = importAnim(expl, errors)
  result.sound = importSound(expl, errors, "sound")
proc importSound*(data: JsonNode; errors: var seq[string]; fieldName: string = ""): PSoundRecord =
  if data.kind == JObject:
    checkKey(data, fieldName)
    result = newSound(data[fieldName].str, errors)
  elif data.kind == JString:
    result = newSound(data.str, errors)

proc importVeh(data: JsonNode; errors: var seq[string]): PVehicleRecord =
  new(result)
  result.playable = false
  if data.kind != JArray or data.len != 2 or
    (data.kind == JArray and
      (data[0].kind != JString or data[1].kind != JObject)):
    result.name = "(broken)"
    errors.add "Vehicle record is malformed"
    return
  var vehData = data[1]
  result.name = data[0].str
  result.anim = importAnim(vehdata, errors)
  result.physics = importPhys(vehdata)
  result.handling = importHandling(vehdata)
  vehdata.getField("playable", result.playable)
  if result.anim.spriteSheet.isNil and result.playable:
    result.playable = false
proc importObject(data: JsonNode; errors: var seq[string]): PObjectRecord =
  new(result)
  if data.kind != JArray or data.len != 2:
    result.name = "(broken)"
    return
  result.name = data[0].str
  result.anim = importAnim(data[1], errors)
  result.physics = importPhys(data[1])
proc importItem(data: JsonNode; errors: var seq[string]): PItemRecord =
  new(result)
  if data.kind != JArray or data.len != 3:
    result.name = "(broken)"
    errors.add "Item record is malformed"
    return
  result.name = data[0].str
  result.anim = importAnim(data[2], errors)
  result.physics = importPhys(data[2])

  result.cooldown = 100.0
  data[2].getField("cooldown", result.cooldown)
  result.cooldown /= 1000.0  ##cooldown is stored in ms

  result.useSound = importSound(data[2], errors, "useSound")

  case data[1].str.toLowerAscii
  of "projectile":
    result.kind = Projectile
    if data[2]["bullet"].kind == JString:
      result.bullet = fetchBullet(data[2]["bullet"].str)
    elif data[2]["bullet"].kind == JInt:
      result.bullet = cfg.bullets[data[2]["bullet"].num.int]
    elif data[2]["bullet"].kind == JObject:
      result.bullet = importBullet(data[2]["bullet"], errors)
    else:
      errors.add "UNKNOWN BULLET TYPE for item " & result.name
  of "ammo":
    result.kind = Ammo
  of "utility":
    discard
  else:
    errors.add "Invalid item type \""&data[1].str&"\" for item "&result.name

proc importBullet(data: JsonNode; errors: var seq[string]): PBulletRecord =
  new(result)
  result.id = -1

  var bdata: JsonNode
  if data.kind == JArray:
    result.name = data[0].str
    bdata = data[1]
  elif data.kind == JObject:
    bdata = data
  else:
    errors.add "Malformed bullet record"
    return

  result.anim = importAnim(bdata, errors)
  result.physics = importPhys(bdata)

  result.lifetime = 2000.0
  result.inheritVelocity = 1000.0
  result.baseVelocity = 30.0
  getField(bdata, "lifetime", result.lifetime)
  getField(bdata, "inheritVelocity", result.inheritVelocity)
  getField(bdata, "baseVelocity", result.baseVelocity)
  result.lifetime /= 1000.0 ## lifetime is stored as milliseconds
  result.inheritVelocity /= 1000.0 ## inherit velocity 1000 = 1.0 (100%)
  result.explosion = importExplosion(bdata, errors)
  result.trail = importTrail(bdata, errors)
