-------------------------------------------- DEPENDENCIES --------------------------------------------

local Movable = require("movable")
local Graphics = require("graphics")
local Collectible = require("collectible")
local GameObject = require("gameobject")


-------------------------------------------- DEFINITIONS ---------------------------------------------


local DEFAULT_SIZE = 10
local DEFAULT_SKIN = {colors.gray,colors.lightGray}

local Snake
local SnakeMT
local Bot
local BotMT

local math_sqrt = math.sqrt
local math_ceil = math.ceil
local math_floor = math.floor
local math_abs = math.abs
local math_random = math.random
local math_pi = math.pi
local math_cos = math.cos
local math_sin = math.sin
local table_insert = table.insert
local string_rep = string.rep


----------------------------------------------- SNAKE ------------------------------------------------

Snake = {class="Snake"}
setmetatable(Snake, {__index=Movable})

SnakeMT = {__index=Snake}

Snake.new = function(player, world, direction, x, y)
	if not (x and y) then
		x, y = math_random(1, world.width), math_random(1, world.height)
		-- refine later (check if safe to spawn)
	end
	local snake = GameObject.new(world, x, y, {
		direction=0,
		speed=10,
		desiredDirection=0,
		size=DEFAULT_SIZE,
		segments = {},
		skin = player and player.skin or DEFAULT_SKIN,
		regions={},
	})
	
	if player then
		setmetatable(snake, SnakeMT)
	else
		setmetatable(snake, BotMT)
	end
	
	world.mobileObjects[snake] = true
	
	return snake

end

Snake.getWidth = function(self)
	return 2+math_ceil(self.size/40)
end

Snake.getRotationSpeed = function(self)

	return 180-self:getWidth()*8

end

Snake.isInside = function(self, objX, objY, objSize)

	if not objSize then
		objSize = 1
	end
	
	local segWidth = math_floor(self:getWidth() + 0.5)
	local stepSize = math_ceil(segWidth/2)
	local segments = self.segments
	
	for s=1,#segments,stepSize do
		local seg = segments[s]
		if math_sqrt((seg.x-objX)^2+(seg.y-objY)^2) < segWidth/2+objSize/2 then
			return true
		end
	end
	
	return false
	
end

Snake.collide = function(self, obj)
	if obj:isInside(self.x, self.y, math_floor(self:getWidth() + 0.5)) then
		-- headbutt
		if self.size >= obj.size then
			-- self died
			return self:kill()
		else
			-- other died
			return obj:collide(self)
		end
	else
		return obj:kill()
	end
end

Snake.kill = function(self)

	local segWidth = math_floor(self:getWidth() + 0.5)
	local stepSize = math_ceil(segWidth)
	local segments = self.segments
	local world = self.world
	local pallets = math.floor((self:getWidth()-1)/2)
	local skin = self.skin
	
	for s=1,#segments,stepSize do
		local seg = segments[s]
		
		for p=1,pallets do
			local x,y = math_random((seg.x-segWidth/2)*10,(seg.x+segWidth/2)*10)/10, math_random((seg.y-segWidth/2)*10,(seg.y+segWidth/2)*10)/10
			x = math.min(math.max(1,x),world.width)
			y = math.min(math.max(1,y),world.height)
			Collectible.new(world, x, y, 1.5, 2, skin[math.random(1,#skin)])
		end
	end
	
	self.dying = 300
	
end

Snake.update = function(self, dt)
	
	if self.dying then
		self.dying = self.dying-dt*1000
		if self.dying <= -4700 then
			local segWidth = math_floor(self:getWidth() + 0.5)
			local stepSize = math_ceil(segWidth/2)
			local segments = self.segments
			local world = self.world
			--[[for s=1,#segments,stepSize do
				local seg = segments[s]
				local region = self.world:getRegion(seg.x, seg.y)
				region.objects[self] = nil
			end]]
			for _,region in pairs(world.regions) do
				region.objects[self] = nil
			end
			world.mobileObjects[self] = nil
			self.dying = nil
			self.dead = true
		end
		return
	end
	
	self.desiredDirection = self.desiredDirection%360
	local rotSpeed = self:getRotationSpeed()
	local inv = false
	if math_abs((self.desiredDirection-360) - self.direction) < math_abs(self.desiredDirection - self.direction) or 
		math_abs((self.desiredDirection+360) - self.direction) < math_abs(self.desiredDirection - self.direction)
	then
		inv = true
	end
	--[[self.direction = self.direction%360
	if math.abs(self.direction+360 - self.desiredDirection) < math.abs(self.direction]] --if self.direction is 5 but self.desiredDirection 355 then
		
	if math_abs(self.direction%360-self.desiredDirection%360) < math_abs(self:getRotationSpeed()*dt) then
		self.direction = self.desiredDirection%360
	elseif (not inv and self.direction < self.desiredDirection) or (inv and self.direction > self.desiredDirection) then
		self.direction = self.direction+self:getRotationSpeed()*dt
	elseif (not inv and self.direction > self.desiredDirection) or (inv and self.direction < self.desiredDirection) then
		self.direction = self.direction-self:getRotationSpeed()*dt
	end
	
	local velX,velY = self:getVelocity()
	
	if self.boosting then
		self.size = math.max(5,self.size-dt*2)
		if self.size == 5 then
			self.boosting = false
		else
			velX,velY = velX*2,velY*2
		end
	end
	
	local nX,nY = self.x+velX*dt, self.y+velY*dt
	
	if nX < 1 or nX > self.world.width or nY < 1 or nY > self.world.height then
		self.direction = (self.direction+180)%360
		self.desiredDirection = self.direction
		velX,velY = self:getVelocity()
		if self.boosting then
			velX,velY = velX*2,velY*2
		end
		nX,nY = self.x+velX*dt, self.y+velY*dt
	end
	
	self.x,self.y = nX,nY
	
	local segments = self.segments
	local sX,sY = math_floor(self.x+0.5), math_floor(self.y+0.5)
	
	if not segments[1] then
		segments[1] = {x=sX,y=sY}
	end
	
	if sX ~= segments[1].x or sY ~= segments[1].y then
		--table.insert(segments, 1, {x=sX, y=sY})
		local x1,y1,x2,y2 = segments[1].x,segments[1].y,sX,sY
		local i = 1
		local xDiff = x2-x1
		local yDiff = y2-y1
		local xOff,yOff = sX%1, sY%1
		
		if math_abs(xDiff) > math_abs(yDiff) then
			if x2 < x1 then
				i = -1
			end
			local dy = yDiff / xDiff
			for x=x1,x2-i,i do
				local y = y1+(x-x1)*dy+yOff
				if not (x == segments[1].x and y == segments[1].y) then
					table_insert(segments,1,{x=x,y=y})
				end
			end
		else
			if y2 < y1 then
				i = -1
			end
			local dx = xDiff / yDiff
			for y=y1,y2-i,i do
				local x = math_floor(x1+(y-y1)*dx)
				if not (x == segments[1].x and y == segments[1].y) then
					table_insert(segments,1,{x=x,y=y})
				end
			end
		end
	end
	
	while #segments > self.size do
		local seg = segments[#segments]
		local region = self.world:getRegion(seg.x, seg.y)
		region.objects[self] = nil
		segments[#segments] = nil
	end
	
	local segWidth = math_floor(self:getWidth() + 0.5)
	
	local stepSize = math_ceil(segWidth/2)
	
	for s=1,#segments,stepSize do
		-- wait what will i use this for OH yeah wait im being stupid
		local seg = segments[s]
		local region = self.world:getRegion(seg.x, seg.y)
		if region then
			region.objects[self] = true
		end
	end
	
	-- collisions
	
	local region, rX, rY = self.world:getRegion(nX, nY)
	local function scanRegion(reg)
		local colliding = {}
		if not reg then return end
		for obj,_ in pairs(reg.objects) do
			if obj ~= self and not obj.dying and obj:isInside(nX, nY, segWidth) then
				if obj.collide then
					table.insert(colliding,obj)
				end
			end
		end
		for c=1,#colliding do
			colliding[c]:collide(self)
			if self.dying then
				break
			end
		end
	end
	for regX=rX-1, rX+1 do
		for regY=rY-1, rY+1 do
			local reg = self.world:indexRegion(regX,regY)
			if reg and not self.dying then
				scanRegion(region)
			end
		end
	end
end

Snake.render = function(self, baseX, baseY)
	
	local zoom = self.world.zoom or 1
	
	local segWidth = math_floor(self:getWidth() + 0.5)
	
	local stepSize = math_ceil(segWidth/2)
	
	local segments = self.segments
	local skin = self.skin
	
	local max = (math_ceil(#segments/stepSize)-1)*stepSize+1
	
	local i = ((max-1)/stepSize)%#skin
	
	local xOff,yOff = self.x-segments[1].x, self.y-segments[1].y
	
	local world = self.world
	local col
	--[[for obj,_ in pairs(world.mobileObjects) do
		if obj ~= self and obj:isInside(self.x, self.y, segWidth) then
			col = true
		end
	end]]
	if self.dying then
		if self.dying <= 0 then
			return
		end
		col = self.dying > 200 and colors.white or self.dying > 100 and colors.lightGray or self.dying > 0 and colors.gray
	end
	if self.boosting then
		term.setBackgroundColor(colors.lightBlue)
		for s=1,max,stepSize do
			local seg = segments[s]
			local rX,rY = seg.x, seg.y
			Graphics.drawCircle(math_ceil(segWidth*zoom+3),math_floor(baseX+rX*zoom),math_floor(baseY+rY*zoom))
		end
	end
	
	for s=max,1,-stepSize do
		local seg = segments[s]
		local rX,rY = seg.x, seg.y -- convert to relative position on screen
		if col then
			term.setBackgroundColor(col)
		else
			term.setBackgroundColor(skin[i+1])
		end
		Graphics.drawCircle(math.max(segWidth*zoom,1),math_floor(baseX+rX*zoom),math_floor(baseY+rY*zoom))
		i = (i-1)%#skin
	end
	
	local seg = segments[1]
	
	--term.setCursorPos()
	term.setBackgroundColor(colors.white)
	for t=1,2 do
		local off = 30
		if t == 1 then
			off = -off
		end
		local angle = (self.direction+off) * math_pi/180
		local xDist, yDist = math_cos(angle)*(segWidth*0.3*zoom), math_sin(angle)*(segWidth*0.3*zoom)
		Graphics.drawCircle(math_ceil(segWidth*zoom/10),math_floor(baseX+seg.x*zoom+xDist),math_floor(baseY+seg.y*zoom+yDist))
	end
end

------------------------------------------------ BOT -------------------------------------------------

Bot = {}
setmetatable(Bot,{__index=Snake})

BotMT = {__index=Bot}

Bot.update = function(self, dt)
	
	local world = self.world
	local region,regX,regY = world:getRegion(self.x,self.y)
	
	local closestDis = 20
	local closest
	for rX=math.max(regX-1,1),math.min(regX+1,world.size) do
		for rY=math.max(regY-1,1),math.min(regY+1,world.size) do
			local reg = world:indexRegion(rX,rY)
			for obj,_ in pairs(reg.objects) do
				local dis = self:getDistanceTo(obj.x, obj.y)
				if closest and closest.class == "Snake" then
					break
				elseif obj ~= self and obj.class == "Snake" and not obj.dying and obj:isInside(self.x, self.y, 30) and (true or self.fleeing) then
					closest = obj
					if not self.fleeing then
						self.desiredDirection = self.direction-180
						-- instead of this, get angle to closest collision point and reverse that
					end
					self.fleeing = true
					break
				elseif obj.class == "Collectible" and ((dis < closestDis and not (closest and obj.score < closest.score)) or (closest and obj.score > closest.score)) then
					self.fleeing = false
					closest = obj
					closestDis = dis
				end
			end
			if closest and closest.class == "Snake" then
				break
			end
		end
		if closest and closest.class == "Snake" then
			break
		end
	end
	
	if closest and closest.class == "Collectible" then
		self.desiredDirection = self:getAngleTo(closest.x,closest.y)
	end
	
	if closest and closest.class == "Collectible" and closest.score > 1 and self:getDistanceTo(closest.x, closest.y) > 5 then
		self.boosting = true
	else
		self.boosting = false
	end
	
	if not closest and self.direction == self.desiredDirection then
		self.desiredDirection = math.random(0,359)
	end
	
	Snake.update(self, dt)
	
end



return Snake