var os = use('os');
var io = use('io');
var transform = use('transform');
var rasterize = use('rasterize');
var time = use('time')
var tilemap = use('tilemap')

// Frame timing variables
var frame_times = []
var frame_time_index = 0
var max_frame_samples = 60
var frame_start_time = 0
var average_frame_time = 0

var game = args[0]

var video

var cnf = use('accio/config')

$_.start(e => {
  if (e.type != 'greet') return
  video = e.actor
  graphics = use('graphics', video)
  send(video, {kind:"window", op:"makeRenderer"}, e => {
    $_.start(e => {
      if (gameactor) return
      gameactor = e.actor
      $_.couple(gameactor)
      start_pipeline()      
    }, args[0], $_)
  })
}, 'prosperon/sdl_video', cnf)

var geometry = use('geometry')

function updateCameraMatrix(camera, winW, winH) {
  // world→NDC
  def sx = 1 / camera.size[0];
  def sy = 1 / camera.size[1];
  def ox = camera.pos[0] - camera.size[0] * camera.anchor[0];
  def oy = camera.pos[1] - camera.size[1] * camera.anchor[1];

  // NDC→pixels
  def vx = camera.viewport.x * winW;
  def vy = camera.viewport.y * winH;
  def vw = camera.viewport.width  * winW;
  def vh = camera.viewport.height * winH;

  // final “mat” coefficients
  //   [ a   0   c ]
  //   [ 0   e   f ]
  //   [ 0   0   1 ]
  camera.a = sx * vw;
  camera.c = vx - camera.a * ox;
  camera.e = -sy * vh;
  camera.f = vy + vh + sy * vh * oy;

  // and store the inverses so we can go back cheaply
  camera.ia = 1 / camera.a;
  camera.ic = -camera.c * camera.ia;
  camera.ie = 1 / camera.e;
  camera.if = -camera.f * camera.ie;
}

//---- forward transform ----
function worldToScreenPoint(pos, camera) {
  return {
    x: camera.a * pos[0] + camera.c,
    y: camera.e * pos[1] + camera.f
  };
}

//---- inverse transform ----
function screenToWorldPoint(pos, camera) {
  return {
    x: camera.ia * pos[0] + camera.ic,
    y: camera.ie * pos[1] + camera.if
  };
}

//---- rectangle (two corner) ----
function worldToScreenRect(rect, camera) {
  // map bottom-left and top-right
  def x1 = camera.a * rect.x                  + camera.c;
  def y1 = camera.e * rect.y                  + camera.f;
  def x2 = camera.a * (rect.x + rect.width)   + camera.c;
  def y2 = camera.e * (rect.y + rect.height)  + camera.f;

  // pick mins and abs deltas
  def x0 = x1 < x2 ? x1 : x2;
  def y0 = y1 < y2 ? y1 : y2;
  return {
    x:      x0,
    y:      y0,
    width:  x2 > x1 ? x2 - x1 : x1 - x2,
    height: y2 > y1 ? y2 - y1 : y1 - y2
  };
}

var camera = {
  size: [640,480],//{width:500,height:500}, // pixel size the camera "sees", like its resolution
  pos: [250,250],//{x:0,y:0}, // where it is
  fov:50,
  near_z:0,
  far_z:1000,
  viewport: {x:0,y:0,width:1,height:1}, // viewport it appears on screen
  ortho:true,
  anchor:[0.5,0.5],//{x:0.5,y:0.5},
  rotation:[0,0,0,1],
  surface: null
}

var util = use('util')
var cammy = util.camera_globals(camera)

var graphics

var gameactor

var images = {}

var renderer_commands = []

// Convert high-level draw commands to low-level renderer commands
function translate_draw_commands(commands) {
  if (!graphics) return
  
  updateCameraMatrix(camera,500,500)
  
  renderer_commands.length = 0
  
  commands.forEach(function(cmd) {
    if (cmd.material && cmd.material.color) {
      renderer_commands.push({
        op: "set",
        prop: "drawColor",
        value: cmd.material.color
      })
    }
    
    switch(cmd.cmd) {
      case "draw_rect":
        cmd.rect = worldToScreenRect(cmd.rect, camera)
        // Handle rectangles with optional rounding and thickness
        if (cmd.opt && cmd.opt.radius && cmd.opt.radius > 0) {
          // Rounded rectangle
          var thickness = (cmd.opt.thickness == 0) ? 0 : (cmd.opt.thickness || 1)
          var raster_result = rasterize.round_rect(cmd.rect, cmd.opt.radius, thickness)
          
          if (raster_result.type == 'rect') {
            renderer_commands.push({
              op: "fillRect",
              data: {rect: raster_result.data}
            })
          } else if (raster_result.type == 'rects') {
            raster_result.data.forEach(function(rect) {
              renderer_commands.push({
                op: "fillRect",
                data: {rect: rect}
              })
            })
          }
        } else if (cmd.opt && cmd.opt.thickness && cmd.opt.thickness > 0) {
          // Outlined rectangle
          var raster_result = rasterize.outline_rect(cmd.rect, cmd.opt.thickness)
          
          if (raster_result.type == 'rect') {
            renderer_commands.push({
              op: "fillRect",
              data: {rect: raster_result.data}
            })
          } else if (raster_result.type == 'rects') {
            renderer_commands.push({
              op: "rects",
              data: {rects: raster_result.data}
            })
          }
        } else {
          renderer_commands.push({
            op: "fillRect",
            data: {rect: cmd.rect}
          })
        }
        break
        
      case "draw_circle":
      case "draw_ellipse":
        cmd.pos = worldToScreenPoint(cmd.pos, camera)
        // Rasterize ellipse to points or rects
        var radii = cmd.radii || [cmd.radius, cmd.radius]
        var raster_result = rasterize.ellipse(cmd.pos, radii, cmd.opt || {})
        
        if (raster_result.type == 'points') {
          renderer_commands.push({
            op: "point",
            data: {points: raster_result.data}
          })
        } else if (raster_result.type == 'rects') {
          // Use 'rects' operation for multiple rectangles
          renderer_commands.push({
            op: "rects",
            data: {rects: raster_result.data}
          })
        }
        break
        
      case "draw_line":
        renderer_commands.push({
          op: "line",
          data: {points: cmd.points.map(p => worldToScreenPoint(p, camera))}
        })
        break
        
      case "draw_point":
        cmd.pos = worldToScreenPoint(cmd.pos, camera)
        renderer_commands.push({
          op: "point",
          data: {points: [cmd.pos]}
        })
        break
        
      case "draw_image":
        var img = graphics.texture(cmd.image)
        var gpu = img.gpu
        if (!gpu) break
        
        cmd.rect.width ??= img.width
        cmd.rect.height ??= img.height
        cmd.rect = worldToScreenRect(cmd.rect, camera)

        renderer_commands.push({
          op: "texture",
          data: {
            texture_id: gpu.id,
            dst: cmd.rect,
            src: img.rect
          }
        })
        break
        
      case "draw_text":
        if (!cmd.text) break
        if (!cmd.pos) break
        var rect = worldToScreenRect({x:cmd.pos.x, y:cmd.pos.y, width:8, height:8}, camera, 500,500)
        var pos = {x: rect.x, y: rect.y}
        renderer_commands.push({
          op: "debugText",
          data: {
            pos,
            text: cmd.text
          }
        })
        break
        
      case "tilemap":
        var texid
        tilemap.for(cmd.tilemap, (tile,{x,y}) => {
          if (!texid) texid = graphics.texture(tile)
          return graphics.texture(tile)
        })
        var geom = geometry.tilemap_to_data(cmd.tilemap)
        if (!texid) break
        if (texid.gpu)
          geom.texture_id = texid.gpu.id

        renderer_commands.push({
          op: "geometry_raw",
          data: geom
        })
        break
    }
  })
  
  return renderer_commands
}

var parseq = use('parseq', $_.delay)

// Wrap `send(actor,msg,cb)` into a parseq “requestor”
//  • on success:   cb(data)        → value=data, reason=null
//  • on failure:   cb(null,err)
function rpc_req(actor, msg) {
  return (cb, _) => {
    send(actor, msg, data => {
      if (data.error)
        cb(null, data)
      else
        cb(data)
    })
  }
}

var game_rec = parseq.sequence([
  rpc_req(gameactor, {kind:'update', dt:1/60}),
  rpc_req(gameactor, {kind:'draw'})
])

var pending_draw = null
var pending_next = null
var last_time = time.number()
var frames = []
var frame_avg = 0

var input = use('input')

var input_state = {
  poll: 1/60
}

// 1) input runs completely independently
function poll_input() {
  send(video, {kind:'input', op:'get'}, (evs=[]) => {
    for (var ev of evs) {
      if (ev.type == 'quit')
        $_.stop()
        
      if (ev.type.includes('mouse')) {
        if (ev.pos)
          ev.pos = screenToWorldPoint(ev.pos, camera, 500,500)
          
        if (ev.d_pos)
          ev.d_pos.y *= -1
      }
      
      if (ev.type.includes('key')) {
        if (ev.key)
          ev.key = input.keyname(ev.key)
      }
    }
        
    send(gameactor, evs)
  })
  $_.delay(poll_input, input_state.poll)
}

// 2) helper to build & send a batch, then call done()
function create_batch(draw_cmds, done) {
  def batch = [
    {op:'set',    prop:'drawColor', value:[0.1,0.1,0.15,1]},
    {op:'clear'}
  ]
  if (draw_cmds && draw_cmds.length)
    batch.push(...translate_draw_commands(draw_cmds))

  batch.push(
    {op:'set', prop:'drawColor', value:[1,1,1,1]},
    {op:'debugText', data:{pos:{x:10,y:10}, text:`Fps: ${(1/frame_avg).toFixed(2)}`}},
    {op:'present'}
  )

  send(video, {kind:'renderer', op:'batch', data:batch}, () => {
    def now = time.number()
    def dt = now - last_time
    last_time = now

    frames.push(dt)
    if (frames.length > 60) frames.shift()
    let sum = 0
    for (let f of frames) sum += f
    frame_avg = sum / frames.length

    done(dt)
  })
}

// 3) kick off the very first update→draw
function start_pipeline() {
  poll_input()
  send(gameactor, {kind:'update', dt:1/60}, () => {
    send(gameactor, {kind:'draw'}, cmds => {
      pending_draw = cmds
      render_step()
    })
  })
}

function render_step() {
  // a) fire off the next update→draw immediately
  def dt = time.number() - last_time
  send(gameactor, {kind:'update', dt:1/60}, () =>
    send(gameactor, {kind:'draw'}, cmds => pending_next = cmds)
  )

  // c) render the current frame
  create_batch(pending_draw, ttr => { // time to render
    // only swap in when there's a new set of commands
    if (pending_next) {
      pending_draw = pending_next
      pending_next = null
    }

    // d) schedule the next render step
    def render_dur = time.number() - last_time
    def wait = Math.max(0, 1/60 - ttr)
    $_.delay(render_step, 0)
  })
}



$_.receiver(e => {
  switch(e.op) {
    case 'resolution':
      log.console(json.encode(e))
      send(video, {
        kind:'renderer',
        op:'set',
        prop:'logicalPresentation',
        value: {...e}
      })
      break
  }
})
