local debugging = false
local space = dspacehash.new()
local enemyspace = dspacehash.new(space)
--enemyspace:collidetest(false)

local time = 0

local function adif (x,y)
	local dif = (x - y + math.pi)%(math.pi*2) - math.pi
	return dif
end

local function kdf (which) -- returns a function which 
	-- returns the state of the given key
	return function () return Keyboard.isKeyDown(which) end 
end

local up,down,left,right,shoot =  -- keyboard setup
	kdf "UP",kdf "DOWN", kdf "LEFT", kdf "RIGHT", kdf "LCTRL"

local thinklist = {}
function thinklist:add(obj) thinklist[#thinklist+1] = obj end
function thinklist:rem(obj) 
	for i,v in ipairs(thinklist) do 
		if v == obj then table.remove(thinklist,i) return end 
	end
end



local function createbody(name,l3d,geom)
	local body = dbody.new()
	body.actor = actornode.new(name or "bd")
	if l3d then 
		body.l3d = l3d
		body.l3d:linkinterface(body.actor)
	end
	if geom then
		body.geom = geom
		body.geom:body(body)
	end
	body.actor:link(body)
	return body
end


----- PROJECTILES

function spawnexplosion (x,y,z, size)
	-- TODO: Use particles here
	local actor = actornode.new ("p",x,y,z)
	actor.l3d = l3dprimitive.newquadcentered("p",size,size)
	actor.l3d:linkinterface(actor)
	actor.l3d:rfBlend(true)
	actor.l3d:rsBlendmode(blendmode.add())
	actor.l3d:rfNodepthmask(true)
	
	actor.age = 0
	actor.maxage = 20
	function actor:think()
		actor.age = actor.age + 1
		if actor.age > actor.maxage then 
			actor:delete()
			actor.l3d:delete()
			return true
		end
		local c = 1 - actor.age / actor.maxage
		actor.l3d:color(c,c^8,c^15,1)
		actor.l3d:renderscale(c*size,c*size,c*size)
	end
	thinklist:add(actor)
end

projectile = {}
function projectile:spawnhit1 (x,y,z)
	spawnexplosion(x,y,z,30)
end

function projectile:spawn1 (bit,x,y,z,vx,vy,vz)
	local body = createbody("proj",
		l3dtrail.new("projtrail",5),dgeomsphere.new(2,space))
	body.l3d:matsurface(tex.trail1)
	body.l3d:color(-1,0,0,0,0)
	
	body.l3d:rfBlend(true)
	body.l3d:rsBlendmode(blendmode.add())
	body.l3d:facecamera(true)
	body.l3d:size(-1,5)
	body.l3d:rfNodepthmask(true)
	body.geom:category(0,bit,false)
	body.geom:category(1,bit,false)
	body:pos(x,y,z)
	body:velocity(vx,vy,vz)
	body.spawn = time
	body:masssphere(.01,5,true)
	body.maxage = 1000
	function body:think()
		if not self.geom then 
			self.spawn = self.spawn - 100 -- make it die faster 
			 -- if it has already hit something
		end
		local x,y,z = self:pos()
		body.l3d:spawn(x,y,z,0,0,1)
		local vx,vy,vz = self:velocity()
		local vel = vx*vx+vy*vy+vz*vz
		for i=0,body:jointcount()-1 do
			local j = body:joint(i)
			local a,b = j:body1(),j:body2()
			if a == body then a = b end
			if a and a.hit then 
				a:hit(vel*.1) 
				if self.geom then 
					self.geom:delete()
					projectile:spawnhit1(x,y,z)
					self.geom = nil
				end
			end
		end
		local col = math.min(1, 1 - (time - self.spawn) / self.maxage)
		body.l3d:color(0,col,col,col,0)
		body.l3d:color(1,col,col,0,0)	
		self:pos(x,y,z*.9)
		if self.spawn+self.maxage<time then
			self:delete()
			if self.geom then self.geom:delete() end
			self.actor:delete()
			self.l3d:delete()
			return true
		end
	end
	thinklist:add(body)
end

----- PLAYER
local w,h,d = 100,80,40
player = createbody("player",l3dmodel.new("player",mdl.player),dgeombox.new(w,h,d,space))
for i=1,31 do 
	player.geom:category(0,i,false)
	player.geom:category(1,i,false)
end
player:massbox(1,w,h,d,true)
player:veldrag(.9,.9,.9)
player.trails = {
	main = l3dtrail.new("maintrail",8),
	left = l3dtrail.new("maintrail",8),
	right = l3dtrail.new("maintrail",8),
}
player.trails.main.x,player.trails.main.y,player.trails.main.z 
	= 0,-55,-8
player.trails.right.x,player.trails.right.y,player.trails.right.z 
	= 32.5,-65,-5
player.trails.left.x,player.trails.left.y,player.trails.left.z 
	= -32.5,-65,-5
player.trails.main:size(-1,15)
player.trails.left:size(-1,5)
player.trails.right:size(-1,5)
player.trails.main:usevelocity(true)
player.trails.left:usevelocity(true)
player.trails.right:usevelocity(true)
for i,t in pairs(player.trails) do
	t:linkinterface(player.actor)
	t:uselocaltime(true)
	--t:rfWireframe(true)
	t:matsurface(tex.trail1)
	t:rfNodepthmask(true)
	t:rfBlend(true)
	t:rsBlendmode(blendmode.add())
	t:color(0,1,1,1,1)
	t:color(1,0,0,1,1)
	t:color(2,0,0,0,1)
end

if debugging then
	player.geom.l3d = l3dprimitive.newbox("geom",w,h,d)
	player.geom.l3d:linkinterface(player.actor)
	player.geom.l3d:rfWireframe(true)
end

function player:shoot()
	if self.lastshot and time - self.lastshot < 200 then return end
	self.lastshot = time
	local x,y,z = self:local2world(30,30,0)
	projectile:spawn1 (0,x,y,z,0,50,0)
	local x,y,z = self:local2world(-30,30,0)
	projectile:spawn1 (0,x,y,z,0,50,0)
end

function player:think()
	local function map (fn) return fn() and 1 or 0 end
	local fx,fy = map(right) - map(left), map(up) - map(down)
	fy = (fy+1.55)*.25
	local fscale = 2
	fx,fy = fx*fscale,fy*fscale
	local x,y,z = self:pos()
	if x>levelw then fx = -(x-levelw)*.1 end
	if x<-levelw then fx = -(x+levelw)*.1 end
	
	self:pos(x,y,z*.9)
	self:addforce(fx,fy,0)
	self.smfx = (self.smfx or 0)*.9 + fx*.1
	self:rotrad(0,self.smfx*.4,0)
	local vx,vy,vz = self:velocity()
	for i,t in pairs(self.trails) do 
		local x,y,z = self:local2world(t.x,t.y,t.z)
		local ux,uy,uz = select(7,self:rotaxis())
		t:localtime(time)
		t:spawn(x,y,z,ux,uy,uz,vx*50,-600,0+vz*50)
		
	end
	
	if shoot () then 
		self:shoot()
	end
end

thinklist:add(player)

----- CAMERA
cam = actornode.new("camera",0,-15,15)
cam.camera = l3dcamera.default()
cam.camera:linkinterface(cam)
cam.camera:backplane(2^16)
cam.camera:frontplane(2^8)
function cam:think()
	local x,y,z = player:pos()
	local tx,ty,tz = player:local2world(0,500,0)
	local u,v,w = cam:pos()
	local a,b = .8,.2
	--cam:pos(u*a+x*b, v*a+(y-500)*b, 2500)
	--cam:lookat(tx,ty,tz, 0,0,1)
	cam:pos(0,y-500,z+2500)
	cam:lookat(0,ty,tz, 0,0,1)
end

thinklist:add(cam)

----- LEVEL ENEMIES

enemy = {}

function enemy:spawn (x,y,z,w,h,d,mdl,mass,speed)
	local body = createbody("enemy1",l3dmodel.new("enemy1",mdl),dgeombox.new(w,h,d,enemyspace))
	body:pos(x,y,z)
	if debugging then
		body.geom.l3d = l3dprimitive.newbox("dbbox",w,h,d)
		body.geom.l3d:linkinterface(body.actor)
		body.geom.l3d:rfWireframe(true)
	end
	body:massbox(mass,w,h,d,true)
	body:veldrag(.9,.9,.9)
	body:rotdrag(.9,.9,.9)
	body.wantedpos = {x,y,z}
	body.engine = djointamotor.new()
	body.engine:attach(body)
	body.engine:numaxes(3)
	body.engine:usermode(false)
	body.engine:axis(0,1,0,1,0)
	body.engine:axis(1,1,1,0,0)
	body.engine:axis(2,1,0,0,1)
	body.engine:fmax(10*mass)
	body.engine:fmax3(mass*4)
	body.engine:fmax2(10*mass)
	
	body.hitpoints = mass*200
	
	speed = speed or 2
	body.lookangle = math.pi
	-- rathaus neuköln 
	
	local function mima(min,max,val) return math.min(max,math.max(min,val)) end
	function body:hit (dmg)
		body.hitpoints = body.hitpoints - dmg
		if body.hitpoints < 0 then 
			local x,y,z = body:pos()
			spawnexplosion (x,y,z,120)
			self:destroy() 
		end
	end
	function body:think()
		if self.trail and time % 2==0 then 
			local tx,ty,tz = body:local2world(self.trail:localpos())
			self.trail:spawn(tx,ty,tz,0,0,1)
		end
		
	
		local x,y,z = self:pos()
		local tx,ty,tz = unpack(self.wantedpos)
		local dx,dy = tx-x,ty-y
		
		local dist = math.sqrt(dx*dx+dy*dy)
		if dist>85 then
			local collides = self:jointcount()>2
			local right = {self:rotaxis()}
			local turn = ExtMath.v3dot({dx/dist,dy/dist,0},right)
			
			
			body.engine:velocity3(-turn)
			local up = {select(7,self:rotaxis())}
			local dot = ExtMath.v3dot({0,0,1},right)
			body.engine:velocity(dot+turn)
			body.engine:velocity2(-body.engine:angle(1))
			local force = 1
			for i=0,self:jointcount()-1 do
				local j = self:joint(i)
				if j.pos then
					local x,y,z = self:world2local(j:pos()) 
					if y>0 then force = -.2 end
				end
			end
			body:addrelforce(0,mima(-.3*speed,.3*speed,dist*.01 * math.max(0,.7-math.abs(turn)))*mass*force,0)
		else
			self.engine:velocity(-body.engine:angle(0)*.1)
			self.engine:velocity2(-body.engine:angle(1))
			self.engine:velocity3(adif(body.lookangle,body.engine:angle(2)))
			local mf = 0.01
			self:addforce(mima(-mf,mf,dx*.01),mima(-mf,mf,dy*.01),0)
		end
		local px,py,pz = player:pos()
		local dx,dy,dz = x-px,y-py,z-pz
		if dx*dx+dy*dy+dz*dz>5000*5000 then
			self:destroy() -- unimportant for player
		end
		body:pos(x,y,z*.9)
		if body.destroyed then
			body:delete()
			body.actor:delete()
			body.geom:delete()
		end
		return body.destroyed
	end
	
	thinklist:add(body)
	
	function body : destroy ()
		self.destroyed = true
	end
	
	return body
end

function enemy:spawn1 (x,y,z)
	local body = self:spawn(x,y,z,50,60,50,mdl.enemy1,.2, 
		8 -- speed
	)
	body.trail = l3dtrail.new("trail",18)
	body.trail:size(-1,15)
	body.trail:linkinterface(body.actor)
	body.trail:matsurface(tex.trail1)
	body.trail:uselocal(true)
	body.trail:localpos(0,-20,-5)
	body.trail:rfBlend(true)
	body.trail:rsBlendmode(blendmode.add())
	body.trail:rfNodepthmask(true)
	body.trail:color(0,1,1,1,1)
	body.trail:color(1,.5,1,0,1)
	body.trail:color(2,0,0,0,1)
	return body
end


function enemy:spawn2 (x,y,z)
	local body = self:spawn(x,y,z,220,140,70,mdl.enemy2,4)
	body.trail = l3dtrail.new("trail",8)
	body.trail:size(-1,5)
	body.trail:linkinterface(body.actor)
	return body
end

function enemy:spawn3 (x,y,z)
	local body = self:spawn(x,y,z,220,100,70,mdl.enemy3,2)
	body.trail = l3dtrail.new("trail",8)
	body.trail:size(-1,5)
	body.trail:linkinterface(body.actor)
	return body
end

function enemy:spawn4 (x,y,z)
	local body = self:spawn(x,y,z,220,100,70,mdl.enemy4,1.8)
	body.trail = l3dtrail.new("trail",8)
	body.trail:size(-1,5)
	body.trail:linkinterface(body.actor)
	return body
end

enemy.activegroups = 0

function enemy:spawngroup1(groupsize,x,y,z)
	self.activegroups = self.activegroups + 1
	local group = {}
	group.pos = {x,y,z}
	group.angle = phase
	local phase = math.random()*1000
	group.angle = math.pi*.5
	
	function group:getPosFor (i)
		local a,b,c = unpack(self.pos)
		local x,y,z = math.floor((i)*.5)*150 * (i%2*2-1),math.floor(i/2)*150,0
		local s,c = math.cos(group.angle),math.sin(group.angle)
		x,y = c*x + s*y + a, c*y - s*x + b
		return x,y,z
	end
	
	for i=1,groupsize do 
		group[i] = enemy:spawn1(group:getPosFor(i))
	end
	
	function group:think()
		local rem = {}
		for i=1,#self do
			self[i].wantedpos = {group:getPosFor(i)}
			self[i].lookangle = self.angle + math.pi*.5
			if self[i].destroyed then rem[#rem+1] = i end
		end
		for i=#rem,1,-1 do 
			table.remove(self,rem[i])
		end
		--self.angle = math.sin(time/1500)
		group.pos[2] = group.pos[2] + 6--math.sin(self.angle+math.pi*.5)*2000 + y
		group.pos[1] = math.sin(time/1500)*400 + x
		if #self==0 then
			enemy.activegroups = enemy.activegroups - 1
			return true
		end
	end
	
	thinklist:add(group)
end
----

map = {}
function map:think()
	local px,py,pz = player:pos()
	if enemy.activegroups<5 then
		enemy:spawngroup1(math.random(8),px + (math.random()-.5)*400,py+2500+math.random()*2400,pz)
	end
end
thinklist:add(map)

----- WORLD SIMULATION
local realtime = os.clock()

while true do
	while time < os.clock()*1000 do
		time = time + 20
		
		dworld.collidetest(space)
		dworld.makecontacts()
		dworld.quickstep(1)
		
		local remlist = {}
		for i,o in ipairs(thinklist) do 
			if o:think() then	
				remlist[#remlist+1] = i
			end
		end
		for i=#remlist,1,-1 do
			table.remove(thinklist,remlist[i])
		end
	end
	coroutine.yield()
	
end
