local component = require("component")
local event = require("event")
local keyboard = require("keyboard")
local shell = require("shell")
local term = require("term")
local unicode = require("unicode")
local raytracer = require("raytracer")

local args = shell.parse(...)
if #args < 1 then
  io.write("Usage: print3d-view FILE [fov]\n")
  os.exit(0)
end

-- model loading

local file, reason = io.open(args[1], "r")
if not file then
  io.stderr:write("Failed opening file: " .. reason .. "\n")
  os.exit(1)
end

local rawdata = file:read("*all")
file:close()
local data, reason = load("return " .. rawdata)
if not data then
  io.stderr:write("Failed loading model: " .. reason .. "\n")
  os.exit(2)
end
data = data()

-- set up raytracer

local rt = raytracer.new()
rt.camera.position={-22+8,20+8,-22+8}
rt.camera.target={8,8,8}
rt.camera.fov=tonumber(args[2]) or 90

local state
local function setState(value)
  if state ~= value then
    state = value
    rt.model = {}
    for _, shape in ipairs(data.shapes or {}) do
      if not not shape.state == state then
        table.insert(rt.model, shape)
      end
    end
    if state and #rt.model < 1 then -- no shapes for active state
      setState(false)
    end
  end
end
setState(false)

-- set up gpu

local gpu = component.gpu
local cfg, cbg
local function setForeground(color)
  if cfg ~= color then
    gpu.setForeground(color)
    cfg = color
  end
end
local function setBackground(color)
  if cbg ~= color then
    gpu.setBackground(color)
    cbg = color
  end
end

-- helper functions

local function vrotate(v, origin, angle)
  local x, y = v[1]-origin[1], v[3]-origin[3]
  local s = math.sin(angle)
  local c = math.cos(angle)

  local rotx = x * c + y * s
  local roty = -x * s + y * c
  return {rotx+origin[1], v[2], roty+origin[3]}
end

local function ambient(normal)
  if math.abs(normal[1]) > 0.5 then
    return 0.6
  elseif math.abs(normal[3]) > 0.5 then
    return 0.8
  elseif normal[2] > 0 then
    return 1.0
  else
    return 0.4
  end
end

local function hash(str)
  local result = 7
  for i=1,#str do
    result = (result*31 + string.byte(str, i))%0xFFFFFFFF
  end
  return result
end

local function multiply(color, brightness)
  local r,b,g=(color/2^16)%256,(color/2^8)%256,color%256
  r = r*brightness
  g = g*brightness
  b = b*brightness
  return r*2^16+g*2^8+b
end

local palette = {0x0000FF, 0x00FF00, 0x00FFFF, 0xFF0000, 0xFF00FF, 0xFFFF00, 0xFFFFFF}

-- render model
while true do
  setForeground(0x000000)
  setBackground(0x000000)
  local rx, ry = gpu.getResolution()
  gpu.fill(1, 1, rx, ry, unicode.char(0x2580))

  rt:render(rx, ry*2, function(x, y, shape, normal)
    local sx, sy = x, math.ceil(y / 2)
    local ch, fg, bg = gpu.get(sx, sy)
    local brightness = ambient(normal)
    local color = multiply(data.palette and data.palette[shape.texture] or palette[hash(shape.texture or "") % #palette + 1], brightness)
    if color == 0x000000 then return end
    if y % 2 == 1 then
      setBackground(bg)
      setForeground(color)
    else
      setBackground(color)
      setForeground(fg)
    end
    gpu.set(sx, sy, ch)
  end)

  gpu.setForeground(0xFFFFFF)
  gpu.setBackground(0x000000)

  gpu.set(1, ry, "[q] Quit    [left/right] Rotate    [space] Toggle state")
  os.sleep(0.1) -- consume events that arrived in the meantime
  while true do
    local _,_,_,code=event.pull("key_down")
    if code == keyboard.keys.q then
      term.clear()
      os.exit(0)
    elseif code == keyboard.keys.space then
      setState(not state)
      break
    elseif code == keyboard.keys.left then
      local step = 10
      if keyboard.isShiftDown() then step = 90 end
      rt.camera.position = vrotate(rt.camera.position, rt.camera.target, -step/180*math.pi)
      break
    elseif code == keyboard.keys.right then
      local step = 10
      if keyboard.isShiftDown() then step = 90 end
      rt.camera.position = vrotate(rt.camera.position, rt.camera.target, step/180*math.pi)
      break
    end
  end
end