-- v1.3.0

local transmitter = {}

local redstone = require("redstone")
local npcManager = require("npcManager")

local floor, ceil, abs = math.floor, math.ceil, math.abs
local insert = table.insert

transmitter.name = "transmitter"
transmitter.id = NPC_ID
transmitter.order = 0.40

transmitter.onRedPower = function(n, c, power, dir, hitbox)
  if redstone.is.reciever(c.id) and d == (n.data.frameX + 2)%4 then
    return true
  end
  redstone.setEnergy(n, power)
end

transmitter.config = npcManager.setNpcSettings({
	id = transmitter.id,

  width = 32,
  height = 32,

	gfxwidth = 32,
	gfxheight = 32,
	gfxoffsetx = 0,
	gfxoffsety = 0,

	frames = 4,
	framespeed = 8,
	framestyle = 0,
  invisible = false,

  nogravity = true,
  notcointransformable = true,
	jumphurt = true,
  noblockcollision = true,
	nohurt = true,
	noyoshi = true,
  blocknpc = true,
  playerblock = true,
  playerblocktop = true,
  npcblock = true,
  disabledespawn = false,

  debug = false,
  effectid = 802,
})

local interactions = {}
local interactionsList = {}
transmitter.interactions = interactions

function transmitter.registerInteraction(id, t)
  if not id then return end
  t.filter = t.filter or function() return true end

  interactions[id] = t
  table.insert(interactionsList, id)
end
local registerInteraction = transmitter.registerInteraction

function transmitter.prime(n)
  local data = n.data

  data.animFrame = data.animFrame or 0
  data.animTimer = data.animTimer or 0

  data.frameX = data._settings.dir or 0
  data.frameY = data.frameY or 0
end

local dirToAngle = {
  [0] = 180,
  [1] = -90,
  [2] = 0,
  [3] = 90
}

local function getOutputPosition(n, dir)
  if dir == 0 then
    return vector(n.x, n.y + n.height*0.5)
  elseif dir == 1 then
    return vector(n.x + 0.5*n.width, n.y)
  elseif dir == 2 then
    return vector(n.x + n.width, n.y + n.height*0.5)
  elseif dir == 3 then
    return vector(n.x + 0.5*n.width, n.y + n.height)
  end
end

local function getInversePosition(n, point)
  if abs(n.x - point.x) < 4 then
    return 0
  elseif abs(n.y - point.y) < 4 then
    return 1
  elseif abs(n.x + n.width - point.x) < 4 then
    return 2
  elseif abs(n.y + n.height - point.y) < 4 then
    return 3
  end
end

local function debugdraw(start, stop, power)
  local n = 2*((start - stop):normalize()):rotate(90)
  local z1, z2, z3, z4 = start + n, start - n, stop + n, stop - n
  Graphics.glDraw{vertexCoords = {z1.x, z1.y, z2.x, z2.y, z4.x, z4.y, z1.x, z1.y, z3.x, z3.y, z4.x, z4.y}, priority = 0, sceneCoords = true, color = Color.green}
end


local function fixCollider(c)
  if c.width < 0 then
    c.x = c.x + c.width
    c.width = -c.width
  elseif c.width == 0 then
    c.x = c.x - 2
    c.width = c.width + 4
  end
  if c.height < 0 then
    c.y = c.y + c.height
    c.height = -c.height
  elseif c.height == 0 then
    c.y = c.y - 2
    c.height = c.height + 4
  end
  return c
end


local function getBlockDiff(start, stop)
  return (start - stop).length/32
end

local findInRaycast
findInRaycast = function(start, ray, power, blacklist, found, i)
  i = i + 1
  if i == 50 then return end -- just in case of infinite loop

  local maxRay = ray*power*32
  local trueRay = maxRay
  local boundBox = fixCollider(Colliders.Box(start.x, start.y, maxRay.x, maxRay.y))  -- sometimes max ray may contain negative values

  local interactionList = Colliders.getColliding{a = boundBox, b = interactionsList, btype = Colliders.NPC, filter = function(v) return interactions[v.id].filter(v) and v ~= blacklist and not v.isHidden end} or {}
  local recieverList = Colliders.getColliding{a = boundBox, b = redstone.id.reciever, btype = Colliders.NPC, filter = function(v) return not v.isHidden end}

  local interactionColls = {}
  local foundInteractable = false

  for k, v in ipairs(interactionList) do
    local collFunc = interactions[v.id].collision
    if collFunc then
      insert(interactionColls, collFunc(v))
    else
      insert(interactionColls, v)
    end
  end

  for k, v in ipairs(interactionColls) do
    local iscollision, point, normal, crashcollider = Colliders.raycast(start, maxRay, v)
    if iscollision then
      local d = (start - point).length

      local id
      if crashcollider.reflector then id = redstone.id.reflector else id = crashcollider.id end

      local isInteractable = interactions[id].canInteract(iscollision, point, normal, crashcollider)

      if isInteractable and (not foundInteractable or d < foundInteractable.dist) then
        foundInteractable = {npc = crashcollider, dist = d, stop = point, normal = normal}
      end
    end
  end

  if foundInteractable then
    trueRay = foundInteractable.dist*ray
  end

  if transmitter.config.debug then
    debugdraw(start, start + trueRay, power)
  end

  for k, v in ipairs(recieverList) do
    local iscollision, point, normal, crashcollider = Colliders.raycast(start, trueRay, v)
    if iscollision then
      local diff = getBlockDiff(start, point)
      insert(found, {npc = crashcollider, power = ceil(power - diff)})
    end
  end


  if foundInteractable then
    local npc = foundInteractable.npc
    local stop = foundInteractable.stop
    local normal = foundInteractable.normal
    local newPower = power - trueRay.length/32

    local id
    if npc.reflector then id = redstone.id.reflector else id = npc.id end

    interactions[id].action(npc, trueRay, start, stop, normal, newPower, found, i)
  end
end

function transmitter.onRedLoad()
  registerInteraction(redstone.id.repeater, {
    ["canInteract"] = function(iscollision, point, normal, crashcollider)
      local dir = getInversePosition(crashcollider, point)
      return dir and dir == (crashcollider.data.frameX + 2)%4
    end,

    ["action"] = function(npc, ray, start, stop, normal, power, found, i)
      if lunatime.tick()%Effect.config[transmitter.config.effectid][1].lifetime == 0 then
        local e = Animation.spawn(transmitter.config.effectid, npc.x + 0.5*npc.width, npc.y)
        e.x, e.y = e.x - e.width*0.5, e.y - e.height - 2
      end

      local out = getOutputPosition(npc, npc.data.frameX)
      findInRaycast(out, vector(1, 0):rotate(dirToAngle[npc.data.frameX]), 15, npc, found, i)
    end,
  })

  registerInteraction(redstone.id.capacitor, {
    ["canInteract"] = function(iscollision, point, normal, crashcollider)
      local dir = getInversePosition(crashcollider, point)
      return dir and dir == (crashcollider.data.frameX + 2)%4
    end,

    ["action"] = function(npc, ray, start, stop, normal, power, found, i)
      if lunatime.tick()%Effect.config[transmitter.config.effectid][1].lifetime == 0 then
        local e = Animation.spawn(transmitter.config.effectid, npc.x + 0.5*npc.width, npc.y)
        e.x, e.y = e.x - e.width*0.5, e.y - e.height - 2
      end
      local data = npc.data
      local out = getOutputPosition(npc, npc.data.frameX)
      findInRaycast(out, vector(1, 0):rotate(dirToAngle[npc.data.frameX]), data.capacitance/data.maxcapacitance*power, npc, found, i)
    end,
  })

  registerInteraction(redstone.id.alternator, {
    ["canInteract"] = function(iscollision, point, normal, crashcollider)
      local dir = getInversePosition(crashcollider, point)
      if crashcollider.data.type == 0 then
        return dir and 1 - dir%2 == crashcollider.data.frameX
      else
        return dir and dir%2 == crashcollider.data.frameX
      end
    end,

    ["action"] = function(npc, ray, start, stop, normal, power, found, i)

      local dir = getInversePosition(npc, stop)
      if npc.data.type == 0 then
        if lunatime.tick()%Effect.config[transmitter.config.effectid][1].lifetime == 0 then
          local e = Animation.spawn(transmitter.config.effectid, npc.x + 0.5*npc.width, npc.y)
          e.x, e.y = e.x - e.width*0.5, e.y - e.height - 2
        end

        local dir = npc.data.frameX + npc.data.facing + 1
        local out = getOutputPosition(npc, dir)
        findInRaycast(out, vector(1, 0):rotate(dirToAngle[dir]), power, npc, found, i)
      elseif  dir == npc.data.frameX + 1 + npc.data.facing then
        if lunatime.tick()%Effect.config[transmitter.config.effectid][1].lifetime == 0 then
          local e = Animation.spawn(transmitter.config.effectid, npc.x + 0.5*npc.width, npc.y)
          e.x, e.y = e.x - e.width*0.5, e.y - e.height - 2
        end

        local dir = 2 - npc.data.frameX
        local out1 = getOutputPosition(npc, dir + 1)
        local out2 = getOutputPosition(npc, dir - 1)
        findInRaycast(out1, vector(1, 0):rotate(dirToAngle[dir + 1]), power, npc, found, i)
        findInRaycast(out2, vector(1, 0):rotate(dirToAngle[dir - 1]), power, npc, found, i)
      end
    end,
  })

  registerInteraction(redstone.id.fuse, {
    ["canInteract"] = function(iscollision, point, normal, crashcollider)
      local dir = getInversePosition(crashcollider, point)
      return dir and (dir == (crashcollider.data.frameX)%4 or dir == (crashcollider.data.frameX + 2)%4)
    end,

    ["action"] = function(npc, ray, start, stop, normal, power, found, i)
      if not npc.data.broken then
        if lunatime.tick()%Effect.config[transmitter.config.effectid][1].lifetime == 0 then
          local e = Animation.spawn(transmitter.config.effectid, npc.x + 0.5*npc.width, npc.y)
          e.x, e.y = e.x - e.width*0.5, e.y - e.height - 2
        end

        local out = getOutputPosition(npc, npc.data.frameX)
        findInRaycast(out, vector(1, 0):rotate(dirToAngle[npc.data.frameX]), power, npc, found, i)
      end
    end,
  })

  registerInteraction(redstone.id.absorber, {
    ["canInteract"] = function()
      return true
    end,

    ["action"] = function()
    end,
  })

  registerInteraction(redstone.id.reflector, {
    ["filter"] = function(n)
      return n.data.isOn
    end,

    ["collision"] = function(n)
      return n.data.collision
    end,

    ["canInteract"] = function()
      return true
    end,

    ["action"] = function(npc, ray, start, stop, normal, power, found, i)
      local dir = ray:normalize()
      local newpath = -2*(dir .. normal)*normal + dir

      findInRaycast(stop, newpath, power, npc.reflector, found, i)
    end,
  })
end


function transmitter.onRedTick(n)
  local data = n.data
  data.observ = false

  if data.power ~= 0 then
    local out = getOutputPosition(n, data.frameX)
    local foundRecievers = {}
    findInRaycast(out, vector(1, 0):rotate(dirToAngle[data.frameX]), data.power, n, foundRecievers, 0)
    if foundRecievers then
      for k, v in ipairs(foundRecievers) do
        redstone.energyFilter(v.npc, n, v.power, data.frameX, n)
      end
    end
  end

  if data.power == 0 then
    data.frameY = 0
  else
    data.frameY = 1
  end

  if data.power ~= data.powerPrev then
    data.observ = true
  end

  redstone.resetPower(n)
end

transmitter.onRedDraw = redstone.drawNPC

redstone.register(transmitter)

return transmitter
