# author: Ethosa
import
  thirdparty/opengl,
  thirdparty/opengl/glut,

  core/color,
  core/exceptions,
  core/input,

  nodes/node,
  nodes/scene,

  environment


var
  cmdLine {.importc: "cmdLine".}: array[0..255, cstring]
  cmdCount {.importc: "cmdCount".}: cint
loadExtensions()  # Load OpenGL extensions.
glutInit(addr cmdCount, addr cmdLine) # Initializ glut lib.
glutInitDisplayMode(GLUT_DOUBLE)


var
  env*: EnvironmentRef = newEnvironment()
  width, height: cint
  main_scene*: ScenePtr = nil
  current_scene*: ScenePtr = nil
  scenes*: seq[ScenePtr] = @[]
  paused*: bool = false


# --- Callbacks --- #
var
  mouse_on: NodePtr = nil
  window_created: bool = false

proc display {.cdecl.} =
  ## Displays window.
  let (r, g, b, a) = env.color.toFloatTuple()
  glClearColor(r*env.brightness, g*env.brightness, b*env.brightness, a*env.brightness)
  glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT)
  glEnable(GL_BLEND)
  glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)

  # Draw current scene.
  current_scene.drawScene(width.GLfloat, height.GLfloat, paused)
  press_state = -1
  mouse_on = nil

  # Update window.
  glFlush()
  glutSwapBuffers()


proc reshape(w, h: cint) {.cdecl.} =
  ## This called when window resized.
  if w > 0 and h > 0:
    glViewport(0, 0, w, h)
    glMatrixMode(GL_PROJECTION)
    glLoadIdentity()
    glOrtho(-w.GLdouble/2, w.GLdouble/2, -h.GLdouble/2, h.GLdouble/2, -1, 1)
    glMatrixMode(GL_MODELVIEW)
    glLoadIdentity()
    width = w
    height = h

    if current_scene != nil:
      current_scene.reAnchorScene(w.GLfloat, h.GLfloat, paused)

template check(event, condition, conditionelif: untyped): untyped =
  if last_event is `event` and `condition`:
    press_state = 2
  elif `conditionelif`:
    press_state = 1
  else:
    press_state = 0

proc mouse(button, state, x, y: cint) {.cdecl.} =
  ## Handle mouse input.
  check(InputEventMouseButton, last_event.pressed and state == GLUT_DOWN, state == GLUT_DOWN)
  last_event.button_index = button
  last_event.x = x.float
  last_event.y = y.float
  last_event.kind = MOUSE
  mouse_pressed = state == GLUT_DOWN
  last_event.pressed = state == GLUT_DOWN

  current_scene.handleScene(last_event, mouse_on, paused)

proc keyboardpress(c: int8, x, y: cint) {.cdecl.} =
  ## Called when press any key on keyboard.
  if c < 0:
    return
  let key = $c.char
  check(InputEventKeyboard, last_event.pressed, true)
  last_event.key = key
  last_event.key_int = c
  last_event.x = x.float
  last_event.y = y.float
  if key notin pressed_keys:
    pressed_keys.add(key)
    pressed_keys_ints.add(c)
  last_event.kind = KEYBOARD
  last_key_state = key_state
  key_state = true

  current_scene.handleScene(last_event, mouse_on, paused)

proc keyboardup(c: int8, x, y: cint) {.cdecl.} =
  ## Called when any key no more pressed.
  if c < 0:
    return
  let key = $c.char
  check(InputEventKeyboard, false, false)
  last_event.key = key
  last_event.key_int = c
  last_event.x = x.float
  last_event.y = y.float
  last_event.kind = KEYBOARD
  last_key_state = key_state
  key_state = false
  var i = 0
  for k in pressed_keys:
    if k == key:
      pressed_keys.delete(i)
      pressed_keys_ints.delete(i)
      break
    inc i

  current_scene.handleScene(last_event, mouse_on, paused)

proc keyboardspecialpress(c: cint, x, y: cint) {.cdecl.} =
  ## Called when press any key on keyboard.
  if c < 0:
    return
  check(InputEventKeyboard, last_event.pressed, true)
  last_event.key_cint = c
  last_event.x = x.float
  last_event.y = y.float
  if c notin pressed_keys_cints:
    pressed_keys_cints.add(c)
  last_event.kind = KEYBOARD
  last_key_state = key_state
  key_state = true

  current_scene.handleScene(last_event, mouse_on, paused)

proc keyboardspecialup(c: cint, x, y: cint) {.cdecl.} =
  ## Called when any key no more pressed.
  if c < 0:
    return
  check(InputEventKeyboard, false, false)
  last_event.key_cint = c
  last_event.x = x.float
  last_event.y = y.float
  last_event.kind = KEYBOARD
  last_key_state = key_state
  key_state = false
  var i = 0
  for k in pressed_keys_cints:
    if k == c:
      pressed_keys_cints.delete(i)
      break
    inc i

  current_scene.handleScene(last_event, mouse_on, paused)

proc motion(x, y: cint) {.cdecl.} =
  ## Called on any mouse motion.
  last_event.kind = MOTION
  last_event.xrel = last_event.x - x.float
  last_event.yrel = last_event.y - y.float
  last_event.x = x.float
  last_event.y = y.float

  current_scene.handleScene(last_event, mouse_on, paused)


# ---- Public ---- #
proc addScene*(scene: ScenePtr) =
  ## Adds a new scenes in app.
  ##
  ## Arguments:
  ## - `scene` - pointer to the Scene object.
  if scene notin scenes:
    scenes.add(scene)

proc changeScene*(name: string): bool {.discardable.} =
  ## Changes current scene.
  ##
  ## Arguments:
  ## - `name` - name of the added scene.
  result = false
  for scene in scenes:
    if scene.name == name:
      if current_scene != nil:
        current_scene.exit()
      current_scene = nil
      current_scene = scene
      current_scene.enter()
      current_scene.reAnchorScene(width.GLfloat, height.GLfloat, paused)
      result = true
      break

proc setMainScene*(name: string) =
  ## Set up main scene.
  ##
  ## Arguments:
  ## - `name` - name of the added scene.
  for scene in scenes:
    if scene.name == name:
      main_scene = scene
      break

proc setTitle*(title: cstring) =
  ## Changes window title.
  if not window_created:
    raise newException(WindowNotCreatedError, "Window not created!")
  glutSetWindowTitle(title)

proc Window*(title: cstring, w: cint = 640, h: cint = 360) {.cdecl.} =
  ## Creates a new window pointer
  ##
  ## Arguments:
  ## - `title` - window title.
  # Set up window.
  glutInitWindowSize(w, h)
  glutInitWindowPosition(100, 100)
  let success = glutCreateWindow(title)

  # Set up OpenGL
  let (r, g, b, a) = env.color.toFloatTuple()
  glClearColor(r, g, b, a)
  glShadeModel(GL_FLAT)
  glClear(GL_COLOR_BUFFER_BIT)

  reshape(w, h)
  window_created = true


proc windowLaunch* =
  ## Start main window loop.
  glutDisplayFunc(display)
  glutIdleFunc(display)
  glutReshapeFunc(reshape)
  glutMouseFunc(mouse)
  glutKeyboardFunc(keyboardpress)
  glutKeyboardUpFunc(keyboardup)
  glutSpecialFunc(keyboardspecialpress)
  glutSpecialUpFunc(keyboardspecialup)
  glutMotionFunc(motion)
  glutPassiveMotionFunc(motion)
  if main_scene == nil:
    raise newException(MainSceneNotLoadedError, "Main scene is not indicated!")
  changeScene(main_scene.name)
  glutMainLoop()
  current_scene.exit()
