local redstone = {}

--  =================================
--  ====    Redstone.lua v1.3.0  ====
--  ====      By  SetaYoshi      ====
--  =================================


function string.startswith(str, start)
  return str:sub(1, #start) == start
end

function string.endswith(str, ending)
	return ending == "" or str:sub(-#ending) == ending
end

--[[

  ██████╗░███████╗██████╗░░██████╗████████╗░█████╗░███╗░░██╗███████╗  ██╗░░░██╗░░███╗░░░░░██████╗░
  ██╔══██╗██╔════╝██╔══██╗██╔════╝╚══██╔══╝██╔══██╗████╗░██║██╔════╝  ██║░░░██║░████║░░░░░╚════██╗
  ██████╔╝█████╗░░██║░░██║╚█████╗░░░░██║░░░██║░░██║██╔██╗██║█████╗░░  ╚██╗░██╔╝██╔██║░░░░░░█████╔╝
  ██╔══██╗██╔══╝░░██║░░██║░╚═══██╗░░░██║░░░██║░░██║██║╚████║██╔══╝░░  ░╚████╔╝░╚═╝██║░░░░░░╚═══██╗
  ██║░░██║███████╗██████╔╝██████╔╝░░░██║░░░╚█████╔╝██║░╚███║███████╗  ░░╚██╔╝░░███████╗██╗██████╔╝
  ╚═╝░░╚═╝╚══════╝╚═════╝░╚═════╝░░░░╚═╝░░░░╚════╝░╚═╝░░╚══╝╚══════╝  ░░░╚═╝░░░╚══════╝╚═╝╚═════╝░
  
  ██████╗░██╗░░░██╗  ░██████╗███████╗████████╗░█████╗░██╗░░░██╗░█████╗░░██████╗██╗░░██╗██╗
  ██╔══██╗╚██╗░██╔╝  ██╔════╝██╔════╝╚══██╔══╝██╔══██╗╚██╗░██╔╝██╔══██╗██╔════╝██║░░██║██║
  ██████╦╝░╚████╔╝░  ╚█████╗░█████╗░░░░░██║░░░███████║░╚████╔╝░██║░░██║╚█████╗░███████║██║
  ██╔══██╗░░╚██╔╝░░  ░╚═══██╗██╔══╝░░░░░██║░░░██╔══██║░░╚██╔╝░░██║░░██║░╚═══██╗██╔══██║██║
  ██████╦╝░░░██║░░░  ██████╔╝███████╗░░░██║░░░██║░░██║░░░██║░░░╚█████╔╝██████╔╝██║░░██║██║
  ╚═════╝░░░░╚═╝░░░  ╚═════╝░╚══════╝░░░╚═╝░░░╚═╝░░╚═╝░░░╚═╝░░░░╚════╝░╚═════╝░╚═╝░░╚═╝╚═╝
  
]]
  

local expandedDefines = require("expandedDefines")
local npcutils = require("npcs/npcutils")
local textplus = require("textplus")
local repl = require("base/game/repl")

local insert, map, unmap, append, remove = table.insert, table.map, table.unmap, table.append, table.remove
local min, max, abs, clamp = math.min, math.max, math.abs, math.clamp
local split, gmatch, find, sub, startswith, endswith = string.split, string.gmatch, string.find, string.sub


--[[
  TODO
  THE PISTON UPDATE!!!
]]

-- Set this to false and the script will no longer stop NPCs from despawning. This will reduce lag in your level! I reccomend you set this to false and install spawnzones into your level
redstone.disabledespawn = false

redstone.componentList = {}
redstone.component = {}

-- Function to register a component
function redstone.register(module)
  module.name = module.name or "noname_"..RNG.randomInt(1000, 9999)
  module.order = module.order or 0.5
  module.config = module.config or NPC.config[module.id]
  module.test = function(x) return (x == module.id or x == module.name)  end
  
  redstone.component[module.name] = module
  table.insert(redstone.componentList, module)
end


redstone.is = {}
setmetatable(redstone.is, {
  __index = function(t, k)
    local com = redstone.component[k]
    if com then
      return com.test
    else
      return function() return false end
    end
  end,
  
  __call = function(t, x, ...)
    local inp = {...}
  
    for k, v in ipairs(inp) do
      local is = redstone.is[v](x)
      if is then
        return true
      end
    end
  
    return false
  end,
})


redstone.id = {}
setmetatable(redstone.id, {
  __index = function(t, k)
    local com = redstone.component[k]
    if com then
      return com.id
    end
  end,
  
  __call = function(t, ...)
    local inp = {...}
    local out = {}
  
    for k, v in ipairs(inp) do
      local id = redstone.id[v]
      if id then
        table.insert(out, id)
      end
    end
  
    return out
  end,
})



-- Helper functions
-- All the following functions can be used by other NPCs and inside of control chips and command blocks
--[[
  Terminology:

  n: NPC being powered
  c: compenent providing power
  p: The amount of power being provided
  d: The direction being provided
  hitbox: The hitbox of the power provided
--]]

--[[
  @setEnergy(npc, power, dir)
  Adds energy to an NPC
    npc:   The NPC that energy will be applied to
    power: The energy level that will be applied. If the NPC already has a higher energy level, nothing will happen
    dir:   Optional, the direction the energy is applied [0: left, 1:up, 2:right, 3:down]
]]

function redstone.setEnergy(n, p, d)
  if not n.data.power then return end
  if p > n.data.power then
    n.data.power = p
    if d then
      n.data.dir = (d + 2)%4
    else
      n.data.dir = -1
    end
  end
end


--[[
  @energyFilter(n, c, power, dir, hitbox)
  Adds energy to an NPC following standard filter procedures. This function checks if an NPC has criteria for being powered
    n:       The NPC that energy will be applied to
    c:       The NPC that is supplying the energy
    power:   The energy level being applied
    dir:     The direction the energy is applied [0: left, 1:up, 2:right, 3:down]
    hitbox:  The hitbox that was used to apply energy
]]
function redstone.energyFilter(n, c, p, d, hitbox)
  if n == c then return end
  n.data.power = n.data.power or 0

  local component = redstone.comList[n.id]
  if component and component.onRedPower then
    return component.onRedPower(n, c, p, d, hitbox)
  else
    redstone.setEnergy(n, p)
  end
end

-- A function that always returns true. Can be used in filters in getColliding
function redstone.nofilter() return true end

-- A function that returns true when an NPC is not hidden. Can be used in filters in getColliding
function redstone.nothidden(v) return not v.isHidden end


--[[
  @passEnergy(args)
  Passes energy to the sorroundings of the NPC in all directions
    source: The NPC being the source of power
    power: The amount of power being provided
    area: Collider box representing area from where to search NPCs.
    npcList: List of NPCs the power should affect
    hitbox: The list collision box of the power as a box collider {x, y, w, h, direction}
      direction of power (0:left, 1:up, 2:right, 3:down). If left empty then direction is universal
]]
function redstone.passEnergy(args)
  args.npcList = args.npcList or redstone.comID
  args.filter = args.filter or redstone.nothidden

  local list = Colliders.getColliding{a = args.area, b = args.npcList, btype = Colliders.NPC, filter = args.filter}
  local found = false

  for _, v in ipairs(args.hitbox) do
    for i = #list, 1, -1 do
      local n = list[i]
      if Colliders.collide(v, n) then
        local power = args.power

        if args.powerAI then power = args.powerAI(n, args.source, args.power, v.direction, v) end
        local cancelled = redstone.energyFilter(n, args.source, power, v.direction, v)

        if not cancelled then
          found = true
          remove(list, i)
        end
      end
    end
  end

  return found
end


--[[
  @passDirectionEnergy(args)
  Passes energy in a single direction
    source: The NPC being the source of power
    power: The amount of power being provided
    npcList: List of NPCs the power should affect
    hitbox: The collision box of the power as a box collider {x, y, w, h, direction}
      direction of power (0:left, 1:up, 2:right, 3:down). If left empty then direction is universal
]]
function redstone.passDirectionEnergy(args)
  local c = args.hitbox  
  args.npcList = args.npcList or redstone.comID
  args.filter = args.filter or redstone.nothidden
  local list = Colliders.getColliding{a = c, b = args.npcList, btype = Colliders.NPC, filter = args.filter}
  for _, n in ipairs(list) do
    if Colliders.collide(c, n) then
      local power = args.power
      if args.powerAI then power = args.powerAI(n, args.source, args.power, c.direction, c)  end
      redstone.energyFilter(n, args.source, power, c.direction, c)
    end
  end
end

--[[
  @passInventory
  Passes inventory items. Returns true if the pass is successful
    source: The NPC being the source of the inventory
    inventory: The inventory ID being passed
    npcList: List of NPCs the power should affect
    hitbox: The collision box of the power as a box collider {x, y, w, h}
]]
function redstone.passInventory(args)
  local c = args.hitbox
  args.npcList = args.npcList or redstone.comID
  local list = Colliders.getColliding{a = c, b = args.npcList, btype = Colliders.NPC, filter = redstone.nofilter}
  for _, n in ipairs(list) do
    if Colliders.collide(c, n) and n.data.invspace then
      local com = redstone.comList[n.id]
      if com.onRedInventory then
        return not com.onRedInventory(n, args.source, args.inventory, c.direction, c)
      else
        n.data.inv = args.inventory
        return true
      end
    end
  end
end


--[[
  @basicRedArea(npc)
  Creates a hitbox to be used for internal redstone functions. Used to filter down NPCs when passing power.
  Hitbox can be updated using @updateRedArea
    npc: NPC whose area will be applied to
]]
function redstone.basicRedArea(n)
  return Colliders.Box(0, 0, 1.5*n.width, 1.5*n.height)
end

--[[
  @basicRedHitBox(npc)
  Creates a list of hitboxes to be used for internal redstone functions. Used to pass power in every direction
  Hitbox can be updated using @updateRedHitBox
    npc: NPC whose area will be applied to
]]
function redstone.basicRedHitBox(n)
  local list = {
    Colliders.Box(0, 0, 0.25*n.width, 0.9*n.height),
    Colliders.Box(0, 0, 0.9*n.width, 0.25*n.height),
    Colliders.Box(0, 0, 0.25*n.width, 0.9*n.height),
    Colliders.Box(0, 0, 0.9*n.width, 0.25*n.height)
  }

  for i = 1, 4 do
    list[i].direction = i - 1
  end

  return list
end

--[[
  @basicDirectionalRedHitBox(npc, dir)
  Creates a hitbox to be used for internal redstone functions. Used to pass power in a specific direction
  Hitbox can be updated using @updateDirectionalRedHitBox
    npc: NPC whose area will be applied to
    dir: The direction the hitbox will be created [0: left, 1:up, 2:right, 3:down]
]]
function redstone.basicDirectionalRedHitBox(n, dir)
  local coll

  if dir == 0 then
    coll = Colliders.Box(0, 0, 0.25*n.width, 0.9*n.height)
  elseif dir == 1 then
    coll = Colliders.Box(0, 0, 0.9*n.width, 0.25*n.height)
  elseif dir == 2 then
    coll = Colliders.Box(0, 0, 0.25*n.width, 0.9*n.height)
  elseif dir == 3 then
    coll = Colliders.Box(0, 0, 0.9*n.width, 0.25*n.height)
  end

  coll.direction = dir

  return coll
end

--[[
  @updateRedArea(npc)
  Updates a red area created using @redarea(). The red are must be stored in NPCObj.data.redarea
    npc: NPC whose redarea will be updated
]]
function redstone.updateRedArea(n)
  n.data.redarea.x = n.x - 0.25*n.width
  n.data.redarea.y = n.y - 0.25*n.height
end

--[[
  @updateRedHitBox(npc)
  Updates a red area created using @redhitbox(). The red are must be stored in NPCObj.data.redhitbox
    npc: NPC whose redhitbox will be updated
]]
function redstone.updateRedHitBox(n)
  local list = n.data.redhitbox
  list[1].x, list[1].y = n.x - 0.25*n.width, n.y + 0.05*n.height
  list[2].x, list[2].y = n.x + 0.05*n.width, n.y - 0.25*n.height
  list[3].x, list[3].y = n.x + n.width, n.y + 0.05*n.height
  list[4].x, list[4].y = n.x + 0.05*n.width, n.y + n.height
end

--[[
  @updateDirectionalRedHitBox(npc)
  Updates a red area created using @basicDirectionalRedHitBox(). The red are must be stored in NPCObj.data.redhitbox
    npc: NPC whose redhitbox will be updated
    dir: The direction the redhitbox is facing [0: left, 1:up, 2:right, 3:down]
]]
function redstone.updateDirectionalRedHitBox(n, dir)
  local coll = n.data.redhitbox
  if dir == 0 then
    coll.x, coll.y = n.x - 0.5*n.width, n.y + 0.05*n.height
  elseif dir == 1 then
    coll.x, coll.y = n.x + 0.05*n.width, n.y - 0.5*n.height
  elseif dir == 2 then
    coll.x, coll.y = n.x + n.width, n.y + 0.05*n.height
  elseif dir == 3 then
    coll.x, coll.y = n.x + 0.05*n.width, n.y + n.height
  end
end



--[[
  @updateDraw(npc)
  Updates the animFrame and animTimer data to somewhat replicate SMBX animation system.
  The values must be stored in NPCObj.data.animTimer and NPCObj.data.animFrame
    npc: NPC whose timers will be updated
]]
function redstone.updateDraw(n)
  local data = n.data
  local config = NPC.config[n.id]

  data.animTimer = data.animTimer + 1
  if data.animTimer >= config.frameSpeed then
    data.animTimer = 0
    data.animFrame = data.animFrame + 1
    if data.animFrame >= config.frames then
      data.animFrame = 0
    end
  end
end

--[[
  @resetPower(npc)
  Updates the power and powerPrev values. meant to be called at the end of onRedTick
    npc: NPC that is affected
]]
function redstone.resetPower(n)
  n.data.powerPrev = n.data.power
  n.data.power = 0
end

--[[
  @resetPower(npc)
  Applies friction to NPCs that are touching the floor. To be used to fix NPCs that are thrown but will slide in the floor
    npc: NPC that is affected
]]
function redstone.applyFriction(n)
  if n.collidesBlockBottom then
    n.speedX = n.speedX*0.5
  end
end

--[[
  @spawnEffect(effectID, obj)
  Spawns an effect centered to the object
    obj: OBJ that is affected
]]
function redstone.spawnEffect(id, obj)
  if type(obj) == "NPC" and NPC.config[id].invisible then return end
  local e = Effect.spawn(id, obj.x + 0.5*obj.width, obj.y + 0.5*obj.height)
  e.x, e.y = e.x - e.width*0.5, e.y - e.height*0.5
end

--[[
  @spawnEffect(obj1, obj2)
  Returns a vector representing the displacemenent between their positions
]]
function redstone.displacement(a, b)
  return vector((a.x + 0.5*a.width) - (b.x + 0.5*b.width), (a.y + 0.5*a.height) - (b.y + 0.5*b.height))
end

--[[
  @printNPC(text, npc, xoffset, yoffset)
  prints debug text to an npc. the offsets are optional
]]
function redstone.printNPC(text, n, xo, yo)
  local x, y = n.x, n.y

  if xo then x = x + xo end
  if yo then y = y + yo end

  textplus.print{text = tostring(text), x = x, y = y, sceneCoords = true, priority = 0}
end

--[[
  @parseListMAP(str)
  Turns "1, 2, 3"  ->  {[1] = true, [2] = true, [3] = true}
]]
function redstone.parseListMAP(str)
  if str == "" then return {} end

  local t = {}
  for k, v in ipairs(split(str, ",")) do
    t[tonumber(v)] = true
  end

  return t
end

--[[
  @parseListMAP(str)
  Turns "1, 2, 3"  ->  {1, 2 ,3}
]]
function redstone.parseList(str)
  if str == "" then return {} end

  local t = {}
  for k, v in ipairs(split(str, ",")) do
    local n = tonumber(v)
    if n then insert(t, n) end
  end

  return t
end

--[[
  @setLayerLineguideSpeed(npc)
  Applies movement from lineguides and layer speeds and simply deals with it
]]
function redstone.setLayerLineguideSpeed(n)
  if not (n.data._basegame.lineguide and n.data._basegame.lineguide.state == 1) then
    n.speedX, n.speedY = npcutils.getLayerSpeed(n)
  end
end

--[[
  @onScreen(npc)
  Returns true if the npc is in either camera
]]
local camlist = {camera, camera2}
function redstone.onScreen(n)
  for _, c in ipairs(camlist) do
    if Colliders.collide(n, Colliders.Box(c.x, c.y, c.width, c.height)) then
      return true
    end
  end
  return false
end

redstone.isMuted = function(n)
  return NPC.config[n.id].mute
end

redstone.onScreenSound = function(n)
  if redstone.isMuted(n) then return false end
  for _, c in ipairs(camlist) do
    if Colliders.collide(n, Colliders.Box(c.x - 100, c.y - 100, c.width + 200, c.height + 200)) then
      return true
    end
  end
  return false
end


redstone.getByTag = function(tag)
  for _, v in NPC.iterate() do
    if v.data._settings._global.redTag == tag then
      return v
    end
  end
end

redstone.getListByTag = function(tag)
  local t = {}
  for _, v in NPC.iterate() do
    if v.data._settings._global.redTag == tag then
      insert(t, v)
    end
  end
  return t
end

redstone.powerTag = function(tag, power)
  local list = redstone.getListByTag(tag)
  power = power or 15
  for k, v in ipairs(list) do
    redstone.setEnergy(v, power)
  end
end

-- Helper function
-- Draws the custom npc that most components use
redstone.drawNPC = function(n)
  local config = NPC.config[n.id]
  n.animationFrame = -1

  if not redstone.onScreen(n) or config.invisible then return end

  local z = n.data.priority or -45
  if config.foreground then
    z = -15
  elseif n:mem(0x12C, FIELD_WORD) > 0 then
    z = -30
  elseif n:mem(0x138, FIELD_WORD) > 0 then
    z = -75
  end
  Graphics.draw{
    type = RTYPE_IMAGE,
    isSceneCoordinates = true,
    image = Graphics.sprites.npc[n.id].img,
    x = n.x + (n.width - config.gfxwidth)*0.5 + config.gfxoffsetx,
    y = n.y + n.height- config.gfxheight + config.gfxoffsety,
    sourceX = n.data.frameX*config.gfxwidth,
    sourceY = (n.data.frameY*config.frames + n.data.animFrame)*config.gfxheight,
    sourceWidth = config.gfxwidth,
    sourceHeight = config.gfxheight,
    priority = z,
    opacity = n.opacity
  }
end

redstone.showLayer = function(layername, hideSmoke)
  local layer = Layer.get(layername)
  if layer then layer:show(hideSmoke or false) end
end

redstone.hideLayer = function(layername, hideSmoke)
  local layer = Layer.get(layername)
  if layer then layer:hide(hideSmoke or false) end
end

redstone.toggleLayer = function(layername, hideSmoke)
  local layer = Layer.get(layername)
  if layer then layer:toggle(hideSmoke or false) end
end

redstone.reddata = {}

local proxytbl = {}

local proxymt = {
	__index = function(t, k) return redstone[k] or lunatime[k] or RNG[k] or math[k] or Routine[k] or _G[k] end,
	__newindex = function() end
}
setmetatable(proxytbl, proxymt)

local funcCache = {}
redstone.luaParse = nil -- Local outside for recursion
redstone.luaParse = function(name, n, msg, recurse)
	if funcCache[msg] then return funcCache[msg] end

	local str = msg
	local chunk, err = load(str, str, "t", proxytbl)

	if chunk then
		local func = chunk()
		funcCache[msg] = func
		return func
	elseif not recurse then
		return redstone.luaParse(name, n, msg:gsub("\r?\n", ";\n"), true)
	else
    insert(repl.log, "ERROR ["..name.."] x:"..n.x..", y:"..n.y..", section:"..n.section)
    insert(repl.log, err)
    Misc.dialog("["..name.."] x:"..n.x..", y:"..n.y..", section:"..n.section.."\n\n"..err)
    return redstone.luaParse(name, n, "return function() return {} end")
	end
end

redstone.luaCall = function(func, params)
  return func(params)
end

-- Helper function
-- Checks if the NPC is valid
local function sectionList()
  local t = {}
  for k, p in ipairs(Player.get()) do
    t[p.section] = true
  end
  return unmap(t)
end

local seccah = {}
local function sectionCache(id)
  local coll = seccah[id]
  local s = Section(id).boundary
  if not coll then
    seccah[id] = Colliders.Box(0,0,0,0)
    coll = seccah[id]
  end
  coll.x, coll.y, coll.width, coll.height = s.left, s.top, s.right - s.left, s.bottom - s.top
  return coll
end

local function validCheck(v)
  return not (v.isHidden or v:mem(0x12A, FIELD_WORD) <= 0 or v:mem(0x124, FIELD_WORD) == 0)
end

-- List of important per-NPC variables
local function primechecker(n, com)
  local data = n.data
  data.prime = true

  -- Power of the NPC, ranges from 0 to 15
  data.power = data.power or 0

  -- Power of the NPC, in the previous frame
  data.powerPrev = data.powerPrev or 0

  -- When true, an observer facing this NPC gets powered
  data.observ = data.observ or false

  -- The power level the observer will output when data.observ is true
  data.observpower = data.observpower or 15

  -- The current inventory slot of the NPC, 0 is empty
  data.inv = data.inv or 0

  -- If true, the inventory slot can be filled
  data.invspace = data.invspace or false

  if com.prime then
    com.prime(n)
  end
end



local function forceStart()
  local sort = {}

  for _, n in ipairs(NPC.get(redstone.comID, -1)) do
    local order = redstone.comOrder[n.id]
    sort[order] = sort[order] or {}
    n.animationFrame = -1
    insert(sort[order], n)
  end

  for i = 1, #redstone.comID do
    if sort[i] then
      for _, n in ipairs(sort[i]) do
        if not n.data.prime then
          primechecker(n, redstone.comList[n.id])
        end
      end
    end
  end
end

local function tickLogic(com, n)
  if not n.data.prime then
    primechecker(n, com)
  end
  -- Copied from spawnzones.lua by Enjl
  if (redstone.disabledespawn or com.config.disabledespawn or n.data.disabledespawn) and not n.isHidden then
    if n:mem(0x124,FIELD_BOOL) then
      n:mem(0x12A, FIELD_WORD, 180)
    elseif n:mem(0x12A, FIELD_WORD) == -1 then
      if not redstone.onScreen(n) then
        n:mem(0x124,FIELD_BOOL, true)
        n:mem(0x12A, FIELD_WORD, 180)
      end
    end
    n:mem(0x74, FIELD_BOOL, true)
  end

  if com.config.grabfix then n:mem(0x134, FIELD_WORD, 0) end -- Custom grabfix (thx mrdoublea)
  if com.config.nogravity then redstone.setLayerLineguideSpeed(n) end

  if com.onRedTick then com.onRedTick(n) end
end

local function tickendLogic(n)
  local com = redstone.comList[n.id]
  if not n.data.prime then
    primechecker(n, com)
  end

  if com.onRedTickEnd then
    com.onRedTickEnd(n)
  end

  if n.data.animTimer then
    redstone.updateDraw(n)
  end
end

local function drawLogic(n)
  local com = redstone.comList[n.id]

  if validCheck(n) then
    if not n.data.prime then
      primechecker(n, com)
    end

    if com.onRedDraw then
      com.onRedDraw(n)
    end
  else
    n.animationFrame = -1
  end
end



-- Helper function
-- Passes a function to all NPCs of all component types
--[[
  I know the profiler took you here, but I can explain
  In order for the system to work, there has to be some type of order in which the NPCs are called
  so for example, chest are the first to execute its AI, then hoppers, and at the end, dust and observers

  This order ensures that energy is being passed correctly and that the NPCs interact with each other properly
  so because of this, I cannot use onTickNPC, Im sure if the system was built differently at its foundation it might be possible...
  but this wasnt made that way and Im not to sure how to change the approach at this point.

  So, all NPC AI goes through this function, this is why the profiler takes you here. This is in charge of passing the AI in order
  Can there be improvememnts to this function? Maybe, but this is the best I got

  Also, dust is laggy, if you have a lot of it, try using the basicdust flag and see if that doesnt break your stuff (it probably wont break anything and it will save you from a LOT of lag)
]]

local redstoneLogic = function(func)
  local sort = {}
  local f = function(n)
    local order = redstone.comOrder[n.id]
    sort[order] = sort[order] or {}
    if validCheck(n) then
      insert(sort[order], n)
    else
      n.animationFrame = -1
    end
  end

  for _, v in ipairs(sectionList()) do
    Colliders.getColliding{a = sectionCache(v), b = redstone.comID, btype = Colliders.NPC, filter = f}
  end

  for i = 1, #redstone.comID do
    if sort[i] then
      local com = redstone.comList[redstone.comID[i]]
      for _, n in ipairs(sort[i]) do
        func(com, n)
      end
    end
  end
end

local redstoneLogic_UNSORT = function(func)
  for _, v in ipairs(sectionList()) do
    Colliders.getColliding{a = sectionCache(v), b = redstone.comID, btype = Colliders.NPC, filter = func}
  end
end

function redstone.onStart()

  redstone.loadAI()

  -- Adds a check for each component. e.g. redstone.isDust()
  redstone.comID = {}
  redstone.comOrder = {}
  redstone.comList = {}
  for k, com in ipairs(redstone.componentList) do
    insert(redstone.comID, com.id)
    redstone.comList[com.id] = com
    redstone.comOrder[com.id] = k

    if com.onRedLoad then com.onRedLoad() end
  end

  forceStart()
end

function redstone.onTick()
  -- component onTick
   redstoneLogic(tickLogic)
end


function redstone.onTickEnd()
  -- component onTickEnd
  redstoneLogic_UNSORT(tickendLogic)

  -- Observers need this special case to work properly!
  if redstone.id.observer then
    local onRedTickObserver = redstone.component.observer.onRedTickObserver
    for k, v in ipairs(sectionList()) do
      local l = Colliders.getColliding{a = sectionCache(v), b = redstone.component.observer.id, btype = Colliders.NPC, filter = validCheck}
      for _, n in ipairs(l) do
        onRedTickObserver(n)
      end
    end
  end
end

function redstone.onDraw()
  -- component onDraw
  redstoneLogic_UNSORT(drawLogic)
end



local function split(str, delim)
	local ret = {}
	if not str then
		return ret
	end
	if not delim or delim == '' then
		for c in gmatch(str, '.') do
			insert(ret, c)
		end
		return ret
	end
	local n = 1
	while true do
		local i, j = find(str, delim, n)
		if not i then break end
		insert(ret, sub(str, n, i - 1))
		n = j + 1
	end
	insert(ret, sub(str, n))
	return ret
end

local function getNameID(name)
  local id = ''
  for k, v in ipairs(split(name)) do
    if k > 7 and k < #name - 3 then
      if tonumber(v) then
        id = id..v
      else
        break
      end
    end
  end
  return tonumber(id)
end

function redstone.loadAI()
local filepaths = {"", "../", "/RedstoneAI", "../RedstoneAI"}
local requirepaths = {"", "", "RedstoneAI/", "RedstoneAI/"}

  local redfiles = {}

  for k, p in ipairs(filepaths) do
    for _, v in ipairs(Misc.listLocalFiles(p)) do
      if string.startswith(v, "rednpc-") and string.endswith(v, ".lua") then
        insert(redfiles, {p, requirepaths[k], v})
      end
    end
  end

  local pNPC_ID = NPC_ID
  for k, v in ipairs(redfiles) do
    local path, name = v[2], v[3]
    local id = getNameID(name)
    if id then
      _G.NPC_ID = id
      local t = require(string.sub(path..name, 1, -5))
      t.id = id
      redstone.register(t)
    else
      require(sub(path..name, 1, -5))
    end
  end
  _G.NPC_ID = nil
end





function redstone.onInitAPI()
	registerEvent(redstone, "onStart", "onStart")
	registerEvent(redstone, "onTick", "onTick")
  registerEvent(redstone, "onTickEnd", "onTickEnd")
  registerEvent(redstone, "onDraw", "onDraw")
end

return redstone
