require("util")
require("render")
require("vector")
require("mob")

glbAtlas = {}
glbMobInterstitial = {}


EFFECT_DURATION = 0.5

GameState = {}
GameState.__index = GameState

function GameState.new(level, pos)
    local v = {}
    v.level = level
    v.pos = pos
    v.mobs = {}
    v.spellslots = {}
    v.effects = {}
    setmetatable(v, GameState)
    return v
end

function GameState:addMob(pos, name, hp)
    self.mobs[#self.mobs+1] = { name=name, pos=pos, hp=hp }
end

function GameState:addSpell(slot, name)
    self.spellslots[slot] = name
end
    
function GameState.__eq(a, b)
    if a.level ~= b.level then return false end
    if a.pos ~= b.pos then return false end
    if #a.mobs ~= #b.mobs then return false end
    -- assume no duplicates so we just have to see if we
    -- are in both lists.
    for i = 1,#a.mobs do
	local found = false
	local mob = a.mobs[i]
	for j = 1,#b.mobs do
	    local other = b.mobs[j]
	    if other.name == mob.name and other.pos == mob.pos and other.hp == mob.hp then 
		found = true
		break
	    end
	end
	if not found then return false end
    end
    for slot = 1,5 do
	if a.spellslots[slot] ~= b.spellslots[slot] then return false end
    end
    -- Note effects are transient so NOT part of the
    -- state for comparisons!
    return true
end

GridData = {}
GridData.__index = GridData

function GridData.new(w, h)
    local v = {}
    v.width = w
    v.height = h
    v.data = {}
    setmetatable(v, GridData)
    return v
end

function GridData:address(x,y)
    return (x-1) + (y-1) * self.width + 1
end

function GridData:get(x, y)
    if getmetatable(x) == Coord then return self:get(x.x, x.y) end
    return self.data[self:address(x,y)]
end

function GridData:allXY()
    local x = 0
    local y = 1
    local w = self.width
    local h = self.height
    return function ()
	    x = x + 1
	    if x > w then
		x = 1
		y = y + 1
		if y > h then
		    return nil
		end
	    end
	    return Coord.new(x,y)
	end
end

-- Requires data to be comparable, nil isn't valid, finds direction
-- going to lower values.
-- Ties resolved randomly!!!
function GridData:randDownHillC(xy, rule)
    local bestangle = nil
    local bestval = nil
    local bestcount = nil

    for angle = 1,4 do
	local dxy = Coord.angleToDir(angle)
	local val = self:get(xy + dxy)
	if val ~= nil and (not rule or rule(xy+dxy)) then
	    if not bestval or bestval >= val then
		if bestval == val then
		    -- Random replace...
		    bestcount = bestcount + 1
		    if math.random(bestcount) == 1 then
			bestangle = angle
		    end
		else
		    bestval = val
		    bestangle = angle
		    bestcount = 1
		end
	    end
	end
    end
    
    if not bestangle then return Coord.new(0,0) end
    return Coord.angleToDir(bestangle)
end

function GridData:set(x, y, t)
    if getmetatable(x) == Coord then return self:set(x.x, x.y, y) end
    if x < 1 or y < 1 or x > self.width or y > self.height then
	return
    end
    self.data[self:address(x,y)] = t
end

function GridData:constant(t)
    for xy in self:allXY() do
	self:set(xy, t)
    end
end

function GridData:maximumWith(other)
    for xy in self:allXY() do
	v = other:get(xy)
	if v ~= nil then
	    self:set(xy, math.max(v, self:get(xy)))
	end
    end
end


function GridData:fillRect(rect, t)
    for y=rect.y,rect.y+rect.h-1 do
	for x=rect.x,rect.x+rect.w-1 do
	    self:set(x,y, t)
	end
    end
end

Tile = {}
Tile.__index = Tile

-- Global tile registry:
glbTiles = {}

function Tile.new(sprite, passable, transparent, levellook)
    local v = {}
    v.sprite = sprite
    v.passable = passable
    v.transparent = transparent
    v.levellook = levellook
    setmetatable(v, Tile)
    return v
end

function Tile.register(name, passable, transparent, levellook)
    local t = Tile.new(Sprite.lookup(name), passable, transparent, levellook)
    glbTiles[name] = t
    return t
end

function Tile.lookup(name)
    return glbTiles[name]
end

function Tile:numVariant()
    if self.levellook then
	return self.sprite:numVariant(2)
    end
    return self.sprite:numVariant()
end

function Tile:render(x, y, level, variant)
    if self.levellook then
	variant = { variant, level }
    end
    self.sprite:render(x, y, 50, 50, variant)
end

Effect = {}
Effect.__index = Effect

function Effect.new(name, variant, duration)
    local v = {}
    duration = duration or EFFECT_DURATION
    variant = variant or 1
    v.name = name
    v.sprite = Sprite.lookup(name)
    v.variant = variant
    v.duration = duration
    v.time = 0
    setmetatable(v, Effect)
    return v
end

Map = {}
Map.__index = Map

function Map.new(w, h)
    local v = { width = w, height = h }
    v.tile = GridData.new(w, h)
    v.moblist = {}
    v.tile_variant = GridData.new(w, h)
    v.flag_fov = GridData.new(w, h)
    v.flag_mapped = GridData.new(w, h)
    v.distance = GridData.new(w, h)
    v.quines = GridData.new(w, h)
    v.effect = GridData.new(w, h)
    v.level = 0
    v.isfinal = false
    v.turn = 0

    setmetatable(v, Map)
    v.tile_variant:constant(1)
    v.flag_fov:constant(0)
    v.flag_mapped:constant(0)
    return v
end

function Map:removeMob(mob)
    if mob then mob.map = nil end
    self.moblist[mob] = nil
end

function Map:addMob(mob)
    self.moblist[mob] = mob
    mob.map = self
end

function Map:allXY()
    local x = 0
    local y = 1
    local w = self.width
    local h = self.height
    return function ()
	    x = x + 1
	    if x > w then
		x = 1
		y = y + 1
		if y > h then
		    return nil
		end
	    end
	    return Coord.new(x,y)
	end
end

-- note this only returns live mobs
function Map:mob(x, y, allowdead)
    if getmetatable(x) == Coord then return self:mobC(x,y) end
    return self:mobC(Coord.new(x,y), allowdead)
end

-- note this only returns live mobs
function Map:mobC(xy, allowdead)
    for _,v in pairs(self.moblist) do
	if (allowdead or not v:isDead()) and v.pos == xy then
	    return v
	end
    end
    return nil
end

function Map:findMobs( rule )
    local results = {}
    for _,v in pairs(self.moblist) do
	if not rule or rule(v) then
	    results[#results+1] = v
	end
    end
    return results
end

function Map:countMobs( rule )
    return #self:findMobs(rule)
end

function Map:clear4Way(x, y)
    if getmetatable(x) == Coord then xy = x else xy = Coord.new(x, y) end

    for dxy in Coord.allDir() do
	local mob = self:mob(xy + dxy)
	if mob then
	    for pdxy in Coord.allDir() do
		if xy ~= xy+dxy+pdxy and mob:canMove(xy+dxy+pdxy) then
		    mob:moveTo(xy+dxy+pdxy)
		    break
		end
	    end
	end
    end
end


function Map:isPassable(x, y, allowmob)
    if getmetatable(x) == Coord then return self:isPassableC(x, y) end
    return self:isPassableC(Coord.new(x,y), allowmob)
end

function Map:isPassableC(xy, allowmob)
    local t = self.tile:get(xy)
    if t == nil then return false end
    if not t.passable then return false end

    if not allowmob and self:mob(xy) then return false end

    return true
end

function Map:isTransparent(x, y)
    if getmetatable(x) == Coord then return self:isTransparentC(x) end
    return self:isTransparentC(Coord.new(x,y))
end

function Map:isTransparentC(xy)
    local t = self.tile:get(xy)
    if t == nil then return false end
    if not t.transparent then return false end
    return true
end

function Map:findTile(t, allowmob, avoidfov)
    local	total = 1
    local	result
    for xy in self:allXY() do
	if self.tile:get(xy) == t 
	   and (not allowmob or not self:mob(xy)) 
	   and (not avoidfov or self.flag_fov:get(xy) == 0)
	   then
	    if math.random(total) == 1 then
		result = xy
	    end
	    total = total+1
	end
    end
    return result
end

function Map:update(dt)
    -- Handle any aniamtion effects
    for _,v in pairs(self.moblist) do
	v:update(dt)
    end
    -- Decay effects
    for xy in self:allXY() do
	e = self.effect:get(xy)
	if e ~= nil then
	    e.time = e.time + dt
	    if e.time > e.duration then
		self.effect:set(xy, nil)
	    end
	end
    end
end

function Map:heartbeat()
    local nextturn = self.turn + 1
    for _,v in pairs(self.moblist) do
	if v:heartbeat() then
	    v:doAI()
	end
	v.turn = nextturn
    end
    self.turn = nextturn
end

function Map:screenPixelOffset()
    local sz = self:tilePixelSize()

    local offx = (1920 - self.width * sz.x) / 2 - sz.x
    local offy = (1080 - self.height * sz.y) / 2 - sz.y
    offy = offy + 15
    offx = offx - 160
    return Coord.new(offx, offy)
end

function Map:tilePixelSize()
    return Coord.new(50, 50)
end

function Map:posToPixelCenter(xy)
    local off = self:screenPixelOffset()
    local sz = self:tilePixelSize()

    xy = xy * sz + off
    xy = xy + sz * 0.5

    return xy
end

function Map:buildState()
    local state = GameState.new(Mob.avatar().map.level, Mob.avatar().pos)

    for _,v in pairs(self.moblist) do
	if self.flag_fov:get(v.pos) == 1 then
	    state:addMob(v.pos, v.name, v.hp)
	end
    end
    
    for xy in self:allXY() do
	local effect = self.effect:get(xy)
	if effect then
	    state.effects[#state.effects+1] = { pos=xy, name=effect.name, variant=effect.variant }
	end
    end

    for slot = 1,5 do
	local spell = Mob.avatar().spellslots[slot]
	if spell then state:addSpell(slot, spell.name) end
    end
    return state
end

function Map:storeQuine(pos, state, turn)
    local	quine = self.quines:get(pos)
    if not quine then quine = {} end
    quine[#quine+1] = state
    self.quines:set(pos, quine)
end

function Map:findQuine(pos, state)
    local	quine = self.quines:get(pos)
    if not quine then return quine end
    for i = 1,#quine do
	if quine[i] == state then return true end
    end
    return false
end

function Map:renderTiles()
    -- render only the tiles of the map.
    local off = self:screenPixelOffset()
    local sz = self:tilePixelSize()

    for xy in self:allXY() do
	local t = self.tile:get(xy)
	if t ~= nil and self.flag_mapped:get(xy) == 1 then
	    if self.flag_fov:get(xy) == 1 then
		love.graphics.setColor(1, 1, 1)
	    else
		love.graphics.setColor(0.75, 0.75, 0.75)
	    end
	    local scrpos = xy * sz + off
	    t:render(scrpos.x, scrpos.y, self.level, self.tile_variant:get(xy))
	end
    end
end

function Map:render()
    -- offset by a tile as we count by one.
    local off = self:screenPixelOffset()
    local sz = self:tilePixelSize()

    self:renderTiles()

    love.graphics.setColor(1, 1, 1)
    -- render dead mobs first
    for _,v in pairs(self.moblist) do
	if v:isDead() and self.flag_fov:get(v.pos) == 1 then
	    local xy = v:animatePos()
	    v:render(xy.x*sz.x+off.x, xy.y*sz.y+off.y, sz.x, sz.y)
	end
    end

    -- render live mobs
    for _,v in pairs(self.moblist) do
	if not v:isDead() and self.flag_fov:get(v.pos) == 1 then
	    local xy = v:animatePos()
	    if not glbInterstitial and glbMobInterstitial[v.name] then
		glbInterstitial = glbMobInterstitial[v.name]
		glbMobInterstitial[v.name] = nil
	    end
	    v:render(xy.x*sz.x+off.x, xy.y*sz.y+off.y, sz.x, sz.y)
	end
    end

    -- Render effects
    for xy in self:allXY() do
	local effect = self.effect:get(xy)
	if effect ~= nil and self.flag_mapped:get(xy) == 1 then
	    if self.flag_fov:get(xy) == 1 then
		love.graphics.setColor(1, 1, 1)
	    else
		love.graphics.setColor(0.75, 0.75, 0.75)
	    end
	    local scrpos = xy * sz + off
	    local animoff = effect.time / effect.duration
	    effect.sprite:render(scrpos.x, scrpos.y, sz.x, sz.y, { effect.variant, effect.sprite:selectVariant01(animoff) } )
	end
    end
end

function Map:renderState(state, animoff)
    -- offset by a tile as we count by one.
    local off = self:screenPixelOffset()
    local sz = self:tilePixelSize()

    self:renderTiles()

    love.graphics.setColor(1, 1, 1)

    for i = 1,#state.mobs do
	local mobstate = state.mobs[i]
	local mob = Mob.create(mobstate.name)
	mob.hp = mobstate.hp
	mob.pos = mobstate.pos
	local xy = mob:animatePos()
	mob:render(xy.x*sz.x+off.x, xy.y*sz.y+off.y, sz.x, sz.y)
    end

    -- Render effects
    for i = 1,#state.effects do
	local effect = state.effects[i]
	local xy = effect.pos
	if self.flag_fov:get(xy) == 1 then
	    love.graphics.setColor(1, 1, 1)
	else
	    love.graphics.setColor(0.75, 0.75, 0.75)
	end
	local scrpos = xy * sz + off
	local sprite = Sprite.lookup(effect.name)
	sprite:render(scrpos.x, scrpos.y, sz.x, sz.y, { effect.variant, sprite:selectVariant01(animoff) } )
    end
end

function Map:buildDistanceC(xy)
    self.distance:constant(nil)
    self.distance:set(xy, 0)

    local wavefront = { xy }
    local distance = 0
    while #wavefront > 0 do
	local nextwave = {}

	distance = distance + 1

	for i = 1,#wavefront do
	    local p = wavefront[i]
	    for dxy in Coord.allDir() do
		local tp = p + dxy

		if self:isPassable(tp, true) and not self.distance:get(tp) then
		    self.distance:set(tp, distance)
		    nextwave[#nextwave+1] = tp
		end
	    end
	end

	wavefront = nextwave
	-- Coward:
	if distance > 100 then break end
    end
end

function Map:buildFov(x, y)
    if getmetatable(x) == Coord then return self:buildFovC(x) end
    return self:buildFovC(Coord.new(x,y), allowdead)
end

function Map:buildFovC(xy)
    self:buildDistanceC(xy)
    self.flag_fov:constant(0)

    self.flag_fov:set(xy, 1)
    for angle = 1,4 do
	dxy = Coord.angleToDir(angle)
	wavefront = { }
	wavefront[xy] = xy
	for radius = 1,5 do
	    nextwave = {}
	    for _,pos in pairs(wavefront) do
		p = pos + dxy + Coord.angleToDir(angle+1)
		self.flag_fov:set(p, 1)
		if self:isTransparentC(p) then nextwave[p] = p end
		p = pos + dxy
		self.flag_fov:set(p, 1)
		if self:isTransparentC(p) then nextwave[p] = p end
		p = pos + dxy + Coord.angleToDir(angle-1)
		self.flag_fov:set(p, 1)
		if self:isTransparentC(p) then nextwave[p] = p end
	    end
	    wavefront = nextwave
	end
    end

    -- Slightly expand fov to light any walls beside floor that is
    -- lit
    for xy in self:allXY() do
	if self.flag_fov:get(xy) == 1 and self.tile:get(xy) == Tile.lookup('floor') then
	    for dxy in Coord.all8Dir() do
		if self.tile:get(xy+dxy) == Tile.lookup('wall') then 
		    self.flag_fov:set(xy+dxy, 1)
		end
	    end
	end
    end

    -- Map anything in fov
    self.flag_mapped:maximumWith(self.flag_fov)
end

function Map:drawCorridor(a, b, t)
    -- Draw corridor between Rect a & b, returning length.
    -- If tile is nil, will not draw.

    local	dx = 0
    local	dy = 0
    -- Check for separating planes...
    if a.x >= b.x+b.w then dx = -1 end
    if b.x >= a.x+a.w then dx = 1 end
    if a.y >= b.y+b.h then dy = -1 end
    if b.y >= a.y+a.h then dy = 1 end

    if dx == 0 and dy == 0 then
	return 10000
    end

    local startx, starty
    local endx, endy

    -- must be one of dx/dy....
    if dx == 0 then
	-- Separate on y only.
	-- x ranges must overlap.
	local mx = math.min(a.x+a.w, b.x+b.w)-1
	local mn = math.max(a.x, b.x)
	-- [mn, mx] valid range
	startx = math.random(mn, mx)
	endx = startx
	if dy < 0 then
	    starty = a.y
	    endy = b.y+b.h-1
	else
	    starty = a.y+a.h-1
	    endy = b.y
	end
    elseif dy == 0 then
	-- Separate on x only.
	-- y ranges must overlap.
	local mx = math.min(a.y+a.h, b.y+b.h)-1
	local mn = math.max(a.y, b.y)
	-- [mn, mx] valid range
	starty = math.random(mn, mx)
	endy = starty
	if dx < 0 then
	    startx = a.x
	    endx = b.x+b.w-1
	else
	    startx = a.x+a.w-1
	    endx = b.x
	end
    else
	-- L shape
	-- WLOG, go from a x to b y
	-- a & b are randomly selected so this is unbiased.
	if dx < 0 then
	    startx = a.x
	else
	    startx = a.x+a.w-1
	end
	starty = math.random(a.y, a.y+a.h-1)
	if dy < 0 then
	    endy = b.y+b.h-1
	else
	    endy = b.y
	end
	endx = math.random(b.x, b.x+b.w-1)
    end

    -- Draw the corridor, always doing X then Y.
    local len = 0
    while startx ~= endx do
	if t then self.tile:set(startx, starty, t) end
	startx = startx + dx
	len = len + 1
	if len > 100 then print("Illegal", startx, starty, endx, endy, dx, dy, dump(a), dump(b)) self.fo() end
    end

    while starty ~= endy do
	if t then self.tile:set(startx, starty, t) end
	starty = starty + dy
	len = len + 1
	if len > 100 then print("Illegal", startx, starty, endx, endy, dx, dy, dump(a), dump(b)) self.fo() end
    end
    if t then self.tile:set(startx, starty, t) end

    return len
end

function Map:build(level, isfinal)
    self.tile:constant(Tile.lookup('wall'))
    self.flag_mapped:constant(0)
    self.flag_fov:constant(0)
    self.level = level
    self.isfinal = isfinal

    -- we have a 30 x 20 grid
    -- Rooms are {2-4} x {2-3}

    local rooms = {}

    while #rooms < 6 do
	local tries = 0
	local maxtry = 30

	while tries < maxtry do
	    local newroom = Rect.new(math.random(2, self.width-3), 
			      math.random(2, self.height-3),
			      math.random(4, 8),
			      math.random(4, 7) )

	    -- Validate on the screen.
	    local valid = true
	    -- I wish there was a continue!
	    if newroom.x + newroom.w >= self.width then valid = false end
	    if newroom.y + newroom.h >= self.height then valid = false end

	    if valid and not newroom:overlaps_list(rooms, 1) then
		rooms[#rooms+1] = newroom
		break
	    end
	    tries = tries + 1
	end
	if tries == maxtry then
	    rooms = {}
	end
    end

    -- Find min and max coords so we can center the rooms so it
    -- doesn't feel lopsided
    local bounds = rooms[1]:clone()
    for i = 1,#rooms do
	bounds:unionWith(rooms[i])
    end
    local offset = bounds:center()
    offset.x = math.floor(self.width/2 + 1- offset.x)
    offset.y = math.floor(self.height/2 +1- offset.y)
    for i = 1,#rooms do
	rooms[i]:translate(offset)
    end

    -- Draw corridors
    -- Idea: Draw a simple L | or -
    -- It is okay if this pierces another room as we just get
    -- more connectivity.  But we pick the other room that is closest.
    -- Given set of connected rooms, find the next room closest to
    -- any of our rooms, and draw that corridor.
    local connect = {}

    local total = #rooms

    shuffleInPlace(rooms)

    -- Copy our primary room...
    connect[#connect+1] = rooms[#rooms]
    rooms[#rooms] = nil

    -- This is optimal connectivity:

    while #connect < total do
	local bestlength = nil
	local bestroomkey = nil
	local bestconnectkey = nil
	for i = 1,#connect do
	    for k,v in pairs(rooms) do
		local len = self:drawCorridor(connect[i], v)
		if bestlength == nil or len < bestlength then
		    bestlength = len
		    bestroomkey = k
		    bestconnectkey = i
		end
	    end
	end
	
	self:drawCorridor(connect[bestconnectkey], rooms[bestroomkey], Tile.lookup('corridor'))
	connect[#connect+1] = rooms[bestroomkey]
	rooms[bestroomkey] = nil
    end

    -- Add random connections.
    -- 
    for i = 1, 3 do
	-- we have already connected pairs of connect[] so pick
	-- non-adjacent
	local first = math.random(#connect)
	local second = math.random(#connect-3)
	second = first + second + 1
	if second > #connect then second = second - #connect end
	self:drawCorridor(connect[first], connect[second], Tile.lookup('corridor'))
    end

    -- Draw these ontop of corridors!
    for i = 1, #connect do
	self.tile:fillRect(connect[i], Tile.lookup('floor'))
    end

    -- Setup tile variants.
    -- We will override this for things like lecterns.
    for xy in self:allXY() do
	self.tile_variant:set(xy, math.random(1,self.tile:get(xy):numVariant()))
    end

    -- Create special rooms: Spellbook, ladder down, ladder up
    shuffleInPlace(connect)

    self.tile:set(connect[1]:random(1), Tile.lookup('up'))
    if not isfinal then
	self.tile:set(connect[2]:random(1), Tile.lookup('down'))
    end
    local p = connect[3]:random(1)
    self.tile:set(p, Tile.lookup('spell'))
    self.tile_variant:set(p, self.spell.variant)

    -- Wipe mobs and spawn
    for _,v in pairs(self.moblist) do
	self:removeMob(v)
    end

    local nmob = 5
    nmob = 1 + level
    nmob = math.min(nmob, 5)
    -- Prebuild fov on level 1 and avoid it for initial mobs.
    if level == 1 then
	self:buildFov(self:findTile(Tile.lookup('up'), true, false))
    end
    for i = 1,nmob do
	Mob.createNPC(level, i == 1):moveMap(self, self:findTile(Tile.lookup('floor'), false, level == 1))
    end

end


