require("render")
require("vector")

Spell = {}
Spell.__index = Spell

SPELL_ANIMATE_NONE = 0
SPELL_ANIMATE_COLLAPSE = 1
SPELL_ANIMATE_FLYFROM = 2
SPELL_ANIMATE_SELECTED = 3
SPELL_ANIMATE_DURATION = 0.3

glbSpells = {}
glbSpellList = {}

function Spell.new(name, sprite, needsdir, variant)
    local v = {}
    v.name = name
    v.sprite = sprite
    v.variant = variant
    v.needsdir = needsdir
    v.animate_state = SPELL_ANIMATE_NONE
    v.animate_time = 0
    v.animate_pos = Coord.new(0,0)

    setmetatable(v, Spell)
    return v
end

function Spell.register(name, needsdir, variant)
    glbSpells[name] = Spell.new(name, Sprite.lookup("spell_" .. name), needsdir, variant)
    glbSpellList[#glbSpellList+1] = glbSpells[name]
    return glbSpells[name]
end

function Spell.random()
    return glbSpellList[math.random(#glbSpellList)]:clone()
end

function Spell.randomOtherThan(list)
    local spell = nil
    while not spell do
	spell = Spell.random()
	for i = 1,#list do
	    if spell.name == list[i] then
		spell = nil
		break
	    end
	end
    end
    return spell
end

function Spell.create(name)
    return glbSpells[name]:clone()
end

function Spell:clone()
    return Spell.new(self.name, self.sprite, self.needsdir, self.variant)
end

function Spell.lookup(name)
    return glbSpells[name]
end

function Spell:update(dt)
    if self.animate_state ~= SPELL_ANIMATE_NONE then
	self.animate_time = self.animate_time + dt
	if self.animate_state ~= SPELL_ANIMATE_SELECTED and self.animate_time > SPELL_ANIMATE_DURATION then
	    self.animate_time = 0
	    if self.animate_state == SPELL_ANIMATE_COLLAPSE then
		return false
	    end
	    self.animate_state = SPELL_ANIMATE_NONE
	end
    end
    return true
end

function Spell:animateNone()
    self.animate_state = SPELL_ANIMATE_NONE
    self.animate_time = 0
end

function Spell:animateDead()
    self.animate_state = SPELL_ANIMATE_COLLAPSE
    self.animate_time = 0
end

function Spell:animateFlyFrom(xy)
    self.animate_state = SPELL_ANIMATE_FLYFROM
    self.animate_pos = xy
    self.animate_time = 0
end

function Spell:animateSelected()
    self.animate_state = SPELL_ANIMATE_SELECTED
    self.animate_time = 0
end

function Spell.spellSlotRect(slot)
    local yoffs = { 241, 429, 617, 805, 993 }
    
    local y = yoffs[slot]
    y = y-130
    y = y+2

    return Rect.new(1615, y, 240, 130)
end

function Spell:render(slot)
    local r = self.spellSlotRect(slot)
    local wh = r:size()

    local nt = self.animate_time / SPELL_ANIMATE_DURATION
    nt = math.max(0, math.min(1, nt))
    if self.animate_state == SPELL_ANIMATE_COLLAPSE then
	wh = wh * (1-nt)
	local goal = r:xy()
	goal = goal + ((r:size() - wh) / 2)
	self.sprite:render(goal.x, goal.y, wh.x, wh.y)
    elseif self.animate_state == SPELL_ANIMATE_SELECTED then
	local nt = self.animate_time / SPELL_ANIMATE_DURATION
	local pulse = math.sin(math.pi * 2 * nt)
	pulse = 1 + pulse * 0.05
	wh = wh * pulse
	local goal = r:xy()
	goal = goal + (r:size() - wh) / 2
	self.sprite:render(goal.x, goal.y, wh.x, wh.y)
    elseif self.animate_state == SPELL_ANIMATE_FLYFROM then
	wh = wh * nt
	local xy = self.animate_pos
	local goal = r:xy()
	xy = Coord.mix(xy, goal, nt)
	self.sprite:render(xy.x, xy.y, wh.x, wh.y)
    else
	self.sprite:render(r.x, r.y)
    end
end

function Spell:isSelected()
    return self.animate_state == SPELL_ANIMATE_SELECTED
end

function Spell:isDead()
    return self.animate_state == SPELL_ANIMATE_COLLAPSE
end

Mob = {}
Mob.__index = Mob

-- GLobal mob prototypes
glbMobs = {}
glbAvatar = nil

MOB_ANIMATE_NONE = 0
MOB_ANIMATE_MOVEFROM = 1
MOB_ANIMATE_BUMPTO = 2
MOB_ANIMATE_DYING = 3
ANIMATE_DURATION = 0.1

AI_NONE = -1
AI_FRIENDLY = 0
AI_CHARGE = 1
AI_GUARD = 2

MOB_TURNS_DEAD = 20

function Mob.new(name, sprite, maxhp, attack, ai_type)
    local v = {}
    v.pos = Coord.new(0,0)
    v.map = nil
    v.name = name
    v.sprite = sprite
    v.attack_damage = attack
    v.hp = maxhp
    v.spellslots = {}
    v.death_clock = 0
    v.turn = 0
    v.maxhp = maxhp
    v.animate_state = MOB_ANIMATE_NONE
    v.animate_time = 0
    v.animate_pos = nil
    v.animate_look_hp = nil
    v.animate_look = MOB_ANIMATE_NONE
    v.animate_look_time = 0
    v.ai_type = ai_type
    v.ai_charging = false
    v.ai_rage = false
    setmetatable(v, Mob)
    return v
end

function Mob.register(name, maxhp, attack, ai_type)
    glbMobs[name] = Mob.new(name, Sprite.lookup('mob_' .. name), maxhp, attack, ai_type)
    return glbMobs[name]
end

function Mob.lookup(name)
    return glbMobs[name]
end

function Mob.create(name)
    local proto = Mob.lookup(name)
    if not proto then return proto end

    return proto:clone()
end

function Mob.createNPC(level, forcelevel)
    
    local list = { 'easy', 'guard', 'brick', 'friendly', 'glass' }

    local choice = level
    if not forcelevel then
	choice = math.random(math.min(#list, level))
    end
    choice = math.min(#list, choice)

    return Mob.create(list[choice])
end

function Mob:clone()
    return Mob.new(self.name, self.sprite, self.maxhp, self.attack_damage, self.ai_type)
end

function Mob:animateMoveFrom(xy)
    self.animate_state = MOB_ANIMATE_MOVEFROM
    self.animate_time = 0
    self.animate_pos = xy
end

function Mob:animateBumpTo(xy)
    self.animate_state = MOB_ANIMATE_BUMPTO
    self.animate_time = 0
    self.animate_pos = xy
end

function Mob:animateDying(oldhp)
    self.animate_look = MOB_ANIMATE_DYING
    self.animate_look_time = 0
    self.animate_look_hp = oldhp
end

function Mob:animateNone()
    self.animate_state = MOB_ANIMATE_NONE
    self.animate_look = MOB_ANIMATE_NONE
end

function Mob:animatePos()
    local xy = self.pos
    local nt = self.animate_time / ANIMATE_DURATION
    nt = math.max(0, math.min(nt, 1))

    if self.animate_state == MOB_ANIMATE_MOVEFROM then
	xy = Coord.mix(self.animate_pos, xy, nt)
    elseif self.animate_state == MOB_ANIMATE_BUMPTO then
	-- Scale to a 20% overlap, triangle function...
	if nt < 0.5 then
	    nt = nt * 2 * 0.2
	else
	    nt = (1 - nt) * 2 * 0.2
	end
	xy = Coord.mix(xy, self.animate_pos, nt)
    else
	-- None
    end
    return xy
end

function Mob:isAvatar()
    return self == glbAvatar
end

function Mob.avatar()
    return glbAvatar
end

function Mob.setAvatar(avatar)
    glbAvatar = avatar
end

function Mob:isDead()
    return self.hp <= 0
end

function Mob:selectedSpellSlot()
    for slot = 1, 5 do
	if self.spellslots[slot] and self.spellslots[slot]:isSelected() then
	    return slot
	end
    end
    return nil
end

-- Return true if still 
function Mob:takeDamage(damage)
    self:animateDying(self.hp)
    self.hp = self.hp - damage
    self.hp = math.max(self.hp, 0)
    -- Engage charging...  this makes friendlies mean
    self.ai_charging = true
    self.ai_rage = true
    if self:isDead() then
	self.death_clock = MOB_TURNS_DEAD
    end
    if self:isAvatar() then
	self.map.effect:set(self.pos, Effect.new('effect_damageB', 1, 0.25))
    else
	self.map.effect:set(self.pos, Effect.new('effect_damageA', 1, 0.25))
    end
    return not self:isDead()
end

-- Acquires this spell into next free slot, returning true
-- if success and false if no slots
function Mob:learnSpell(spell, npos)
    -- Determine replacement slot, first free, and if no
    -- free pick anyone on cool down
    npos = npos or self.pos
    slot = nil
    for i = 1,5 do
	if not self.spellslots[i] then
	    slot = i
	    break
	end
    end
    if not slot then
	for i = 1,5 do
	    if self.spellslots[i]:isDead() == true then
		slot = i
		break
	    end
	end
    end
    if slot then
	-- Acquire a slot!
	spell:animateFlyFrom(self.map:posToPixelCenter(npos))
	self.spellslots[slot] = spell
	return true
    end
    return false
end

function Mob:knowsSpellName(spellname)
    for i = 1,5 do
	local spell = self.spellslots[i]
	if spell and spell.name == spellname and not spell:isDead() then
	    return true
	end
    end
    return false
end

function Mob:doAI()
    if self.ai_type == AI_NONE then return true end

    -- disable charging if different map
    if self.ai_charging then
	if self.map ~= Mob.avatar().map then
	    self.ai_charging = false
	end
	-- Randomly lose if not in fov.
	-- Helps make shadow step more useful.
	if self.map.flag_fov:get(self.pos) == 0 then
	    if math.random(10) == 1 then
		self.ai_charging = false
	    end
	end
    end

    -- acquire charging..
    if self.ai_type == AI_CHARGE then
	if self.map.flag_fov:get(self.pos) == 1 then
	    self.ai_charging = true
	end
    elseif self.ai_type == AI_FRIENDLY and self.ai_rage then
	if self.map.flag_fov:get(self.pos) == 1 then
	    self.ai_charging = true
	end
    elseif self.ai_type == AI_GUARD then
	if self.map.flag_fov:get(self.pos) == 1 then
	    -- Also see if close enough...
	    if (self.map.distance:get(self.pos) or 100) <= 2 then
		self.ai_charging = true
	    end
	end
    end

    if self.ai_charging then
	if self:aiCharge() == true then return true end
    end

    if self.ai_type ~= AI_GUARD then
	return self:aiWander()
    end

    return self:actionWait()
end

function Mob:actionWait()
    return true
end

function Mob:heartbeat()
    if self:isDead() then
	-- Update death clock...
	self.death_clock = self.death_clock - 1
	if self.death_clock < 0 then
	    self.death_clock = 0
	    self.hp = self.maxhp
	    self.ai_charging = false
	    -- Note we do not clear rage here!
	    -- This is rather debatable, but has the effect that
	    -- if you kill a sheep it will come back angry, which is
	    -- likely good. (Contrast with polymorph which resets)
	    self:animateNone()
	    self:moveTo(self.map:findTile(Tile.lookup('floor'), false, true))
	end
	-- Note we don't move on resurrection.
	return false
    end
    return true
end

function Mob:update(dt)
    for k, v in pairs(self.spellslots) do
	if v:update(dt) == false then
	    self.spellslots[k] = nil
	end
    end
    if self.animate_state ~= MOB_ANIMATE_NONE then
	self.animate_time = self.animate_time + dt
	if self.animate_time > ANIMATE_DURATION then
	    self.animate_state = MOB_ANIMATE_NONE
	    self.animate_time = 0
	end
    end
    if self.animate_look ~= MOB_ANIMATE_NONE then
	self.animate_look_time = self.animate_look_time + dt
	if self.animate_look_time > ANIMATE_DURATION then
	    self.animate_look = MOB_ANIMATE_NONE
	    self.animate_look_time = 0
	end
    end
end

function Mob:actionCast(slot, dxy)
    local spell = self.spellslots[slot]
    if not spell then return false end
    if spell:isDead() then return false end
    spell:animateDead()

    -- Now figure out what the spell does...
    if spell.name == 'escape' then
	-- This is where we go!
	local target = self.map:findTile(Tile.lookup('floor'), false, true)
	self:animateMoveFrom(self.pos)
	self:moveTo(target)
    elseif spell.name == 'fullheal' then
	self.hp = self.maxhp
	self.map.effect:set(self.pos+Coord.up(), Effect.new('effect_healA', 1, 0.25))
	self.map.effect:set(self.pos, Effect.new('effect_healB', 1, 0.25))
    elseif spell.name == 'teleport' then
	for dxy in Coord.all8Dir() do
	    local mob = self.map:mob(self.pos+dxy, true)
	    if mob then
		local np = self.map:findTile(Tile.lookup('floor'), false, true)
		if np then 
		    mob:moveTo(np) 
		    -- Lose sight of the avatar so stop charge
		    mob.ai_charging = false
		end
	    end
	end
	local step = 1
	for dx = -1,1 do
	    for dy = -1,1 do
		self.map.effect:set(self.pos + Coord.new(dx,dy), 
			Effect.new('effect_teleport', step))
		step = step+1
	    end
	end
    elseif spell.name == 'firering' then
	for dxy in Coord.all8Dir() do
	    local mob = self.map:mob(self.pos+dxy, true)
	    if mob then
		mob:takeDamage(mob.hp)
	    end
	    local step = 1
	    for dx = -1,1 do
		for dy = -1,1 do
		    self.map.effect:set(self.pos + Coord.new(dx,dy), 
			    Effect.new('effect_firering', step))
		    step = step+1
		end
	    end
	end
    elseif spell.name == 'polymorph' then
	for dxy in Coord.all8Dir() do
	    local p = self.pos
	    p = p + dxy
	    local mob = self.map:mob(p, true)
	    if mob then
		self.map.effect:set(p, Effect.new('effect_polymorph'))
		local map = self.map
		map:removeMob(mob)
		local newmob = mob
		while newmob.name == mob.name do
		    newmob = Mob.createNPC(100)
		end
		newmob:moveMap(map, p)
		-- Fully healed stays same, otherwise maintain lower
		if mob.hp ~= mob.maxhp then
		    newmob.hp = math.min(newmob.hp, mob.hp)
		end
		-- Do not copy, which does make injured mobs "friendly"
		-- if they convert, but otherwise you can't fix an undamged
		-- angry mob. And polymorph is weak.
		-- Likewise don't copy rage.
		-- newmob.ai_charging = mob.ai_charging
		newmob.death_clock = mob.death_clock
	    end
	    local tile = self.map.tile:get(p)
	    if tile == Tile.lookup('spell') and self.map.spell.name ~= 'escape' then
		self.map.spell = Spell.randomOtherThan({ 'escape', self.map.spell.name })
		self.map.effect:set(p, Effect.new('effect_polymorph'))
		self.map.tile_variant:set(p, self.map.spell.variant)
	    end
	end
	local step = 1
	for dx = -1,1 do
	    for dy = -1,1 do
		self.map.effect:set(self.pos + Coord.new(dx,dy), 
			Effect.new('effect_polymorph', step))
		step = step+1
	    end
	end
    elseif spell.name == 'lightning' then
	if not dxy then return true end
	local p = self.pos
	p = p + dxy
	local effectname
	if dxy.x ~= 0 then effectname = 'effect_lightning_hor'
	else effectname = 'effect_lightning_vert' end
	local step = 1
	while self.map:isTransparent(p) do
	    local mob = self.map:mob(p)
	    if mob then
		mob:takeDamage(mob.hp)
	    end
	    self.map.effect:set(p, 
		    Effect.new(effectname, step))
	    p = p + dxy
	    step = step + 1
	    if step > 6 then step = step - 6 end
	end
    elseif spell.name == 'shadowstep' then
	if not dxy then return true end
	local target = self.pos + dxy
	while self.map:isTransparent(target) do
	    target = target + dxy
	end
	-- back one to be passable again.
	target = target - dxy
	-- now while we have a mob go back, but not past ourself..
	while target ~= self.pos do
	    if self.map:isPassable(target) then break end
	    target = target - dxy
	end
	-- This is where we go!
	self:animateMoveFrom(self.pos)
	self:moveTo(target)
    end
    return true
end

function Mob:actionBump(dxy)
    local npos = self.pos + dxy
    -- Check for hitting mobs...
    local target = self.map:mob(npos)
    if target then
	-- Only attack if avatar states don't match.
	if self:isAvatar() ~= target:isAvatar() then
	    return self:actionAttack(dxy)
	end
	return false
    end
    local tile = self.map.tile:get(npos)
    if tile == Tile.lookup('up') then
	if not self:isAvatar() then return false end
	if self.map.level == 1 then
	    if self:knowsSpellName('escape') then
		self:animateMoveFrom(self.pos)
		self:moveTo(npos)
		glbVictory = true
	    end
	end
	local nextmap = glbAtlas[self.map.level-1]
	if nextmap then
	    self:moveMap(nextmap, nextmap:findTile(Tile.lookup('down'), true, false))
	    if self:knowsSpellName('escape') and nextmap.up_overlay then
		glbInterstitial = nextmap.up_overlay
		nextmap.up_overlay = nil
	    end
	    nextmap:clear4Way(self.pos)
	    return true
	else
	    -- Uselessly bump.
	    self:animateBumpTo(npos)
	    return false
	end
    end
    if tile == Tile.lookup('down') then
	if not self:isAvatar() then return false end
	local nextmap = glbAtlas[self.map.level+1]
	if nextmap then
	    self:moveMap(nextmap, nextmap:findTile(Tile.lookup('up'), true, false))
	    nextmap:clear4Way(self.pos)
	    if nextmap.down_overlay then
		glbInterstitial = nextmap.down_overlay
		nextmap.down_overlay = nil
	    end
	    return true
	else
	    -- Uselessly bump.
	    self:animateBumpTo(npos)
	    return false
	end
    end
    if tile == Tile.lookup('spell') then
	if not self:isAvatar() then return false end
	local spell = self.map.spell:clone()

	local success = self:learnSpell(spell, npos)
	self:animateBumpTo(npos)
	return success
    end

    if self:canMove(npos) then
	self:animateMoveFrom(self.pos)
	self:moveTo(npos)
	return true
    end
    return false
end

function Mob:actionAttack(dxy)
    local npos = self.pos + dxy
    local target = self.map:mob(npos)
    if not target then return false end
    if target == self then return false end

    self:animateBumpTo(npos)
    target:takeDamage(self.attack_damage)

    return true
end

function Mob:aiCharge()
    local dist = self.map.distance:get(self.pos)
    -- Wait for the avatar to move first...
    if self.hp <= 1 and dist == 2 then
	-- Check for exceptions.
	-- a) if someone is beside that is charging, we can join
	--    as they can only hit one of us.
	if self.map:countMobs( 
		    function (mob) 
			if mob:isDead() then return false end
			return mob.map.distance:get(mob.pos) == 1 and mob.ai_charging
		    end) > 0 then
	    --- allow a move
	else
	    -- I really wish LUA had a continue.
	    -- This one is tricky.
	    -- We have two mobs that are aggressors exactly two
	    -- distance
	    -- Neither have moved yet.
	    -- And they both have unique spaces to go to beside the
	    -- avatar
	    -- We start by getting all the mobs that are 2 away
	    -- that haven't moved and are aggressive.
	    local gang = self.map:findMobs(
		    function (mob)
			if mob:isDead() then return false end
			if mob.map.distance:get(mob.pos) ~= 2 then return false end
			if mob.turn ~= mob.map.turn then return false end
			if not mob.ai_charging then return false end
			return true
		    end)
	    -- If there are enough mobs, we can see if there
	    -- is a different place to go for each.
	    -- Note after we go, the other mob will see a close mob
	    -- so should be willing to follow!
	    if #gang < 2 then
		return self:actionWait()
	    end
	    local gtargets = {}
	    local nonsingle = 0
	    for i = 1,#gang do
		gtargets[i] = {}
		for xy in Mob.avatar().pos:allAdj() do
		    if self.map:isPassable(xy) and xy:manDist(gang[i].pos) == 1 then
			gtargets[i][#gtargets[i]+1] = xy
		    end
		end
		if #gtargets[i] > 1 then
		    nonsingle = nonsingle + 1
		end
	    end
	    -- If at least two gang members have two choices
	    -- we can't prevent each other.
	    -- Otherwise, there must be at least one of the
	    -- gangmembers with a single target whose target is NOT
	    -- in another gang member's list.
	    -- This is still too conservative, as
	    -- @.A
	    -- .B.
	    -- has one choice for A, but two for B.  So we'll refuse
	    -- to act in case B will stomp on us.
	    -- But it is currently either A or B's turn.  So we can
	    -- actually pick a direction here...
	    local mygidx
	    for i = 1,#gang do
		if gang[i] == self then mygidx = i end
	    end
	    shuffleInPlace(gtargets[mygidx])
	    for i = 1,#gtargets[mygidx] do
		local gxy = gtargets[mygidx][i]
		-- If we move to gxy, will there still be another
		-- mob available?
		local othervalid = false
		for j = 1,#gtargets do
		    if j ~= mygidx then
			-- More than one, we'll have one left
			if #gtargets[j] > 1 then
			    othervalid = true
			    break
			end
			-- Exactly one, must not be the one we picked.
			if #gtargets[j] == 1 then
			    if gtargets[j][1] ~= gxy then
				othervalid = true
				break
			    end
			end
		    end
		end
		-- If we have other valid, we can move to gxy!
		if othervalid then
		    return self:actionBump(gxy - self.pos)
		end
	    end
	    --  All of our moves would block another person
	    return self:actionWait()
	end
    end
    local dxy = self.map.distance:randDownHillC(self.pos, 
			function (xy) 
			    local mob = self.map:mob(xy)
			    return not mob or mob:isAvatar() 
			    end)
    return self:actionBump(dxy)
end

function Mob:aiWander()
    local dxy = Coord.rand4dir()
    return self:actionBump(dxy)
end

function Mob:canMove(xy)
    if self.map:isPassable(xy) then return true end
    return false
end

function Mob:moveTo(xy)
    self.pos = xy
end

function Mob:moveMap(map, xy)
    if map == self.map then
	self:moveTo(xy)
	return
    end
    if self.map ~= nil then
	self.map:removeMob(self)
    end
    self.map = map
    self.pos = xy
    if self.map ~= nil then
	self.map:addMob(self)
    end
end

function Mob:render(x, y, w, h)
    -- select sprite list from hp
    local curhp = self.hp
    if self.animate_look == MOB_ANIMATE_DYING then
	local nt = self.animate_look_time / ANIMATE_DURATION
	nt = math.max(0, math.min(nt, 1))
	if nt < 0.5 then
	    curhp = self.animate_look_hp
	end
    end

    local idx 
    local maxidx = self.sprite:numVariant()
    if self.maxhp == curhp then
	idx = 1
    elseif curhp <= 0 then
	idx = maxidx
    else
	idx = self.maxhp - curhp + 1
    end
    self.sprite:render(x, y, w, h, idx)
end
