from functools import partial

from core.combat import AttackType, AttackData, damageEnemies, DamageType, getHitEnemies, hitStun, handleMonsterDeath
from core.helper import enum, getRandomPath, getRelativeArea
from core.sentient import MovementHelpers, Sentient

from siege import game
from siege.graphic import Color
from siege.network import NetworkManager
from siege.util import minutes, RangeInt, Random, Rect, seconds, TileVector, Timer, Vector
from siege.world import getNearbyEnemies


MonsterState = enum(
    "Spawn",
    "Idle",
    "Roam",
    "Chase",
    "Flee",
    "Hostile",
    "Death",
    "Action",
    "Custom1",
    "Custom2"
)


class BasicMonsterStates(object):
    """ Basic callbacks for monster states.

    Initializers are called when monsters first enter a state.

    Handlers are called on all subsequent updates while the monster is in that
    state.

    """
    @staticmethod
    def spawn(monster, frameTime):
        color = monster.entity.getRender().getColor()
        color.a = int(min(float(monster.stateTime) / float(monster.spawnTime), 1.0) * 255.0)
        monster.entity.getRender().setColor(color)
        if monster.stateTime >= monster.spawnTime:
            monster.changeState(MonsterState.Idle)
        else:
            return True

    @staticmethod
    def initializeIdle(monster):
        monster.idle(monster.entity.animation)
        monster.targetTime = monster.idleTimes.getRandom()
        monster.entity.physics.setVelocity(Vector(0, 0))

    @staticmethod
    def idle(monster, frameTime):
        if monster.stateTime >= monster.targetTime:
            monster.changeState(MonsterState.Roam)

    @staticmethod
    def death(monster, frameTime):
        if monster.entity.animation.wasPlaying("death"):
            if monster.stateTime >= monster.deathTime:
                handleMonsterDeath(monster.entity, monster.loot)
            else:
                scale = 1 - (monster.stateTime / monster.deathTime)
                monster.entity.getRender().sprite.setScale(scale, scale)
        return True

    @staticmethod
    def initializeRoam(monster):
        """ Pick some nearby place to move towards. Gravitate towards the spawner. """

        spawner = monster.entity.monster.getSpawner()
        if spawner:
            monster.direction = monster.entity.getPosition().getDirection(spawner.getPosition(), monster.entity.loopWidth)
            chance = Random.getFloat()
            if chance > 0.7:
                # Flip direction so we move away from the spawner
                monster.direction.x = -monster.direction.x
            if chance > 0.55 and chance < 0.85:
                monster.direction.y = -monster.direction.y
        else:
            monster.direction.x = 1 if Random.getBool() else -1
        monster.targetTime = monster.roamTimes.getRandom()

    @staticmethod
    def roam(idleChance, movement, monster, frameTime):
        if monster.stateTime >= monster.targetTime and monster.isTouchingGround and not monster.entity.animation.isPlaying("fall"):
            # State time has expired. Either start roaming again or possibly idle for a moment
            if Random.getFloat() <= idleChance:
                monster.changeState(MonsterState.Idle)
            else:
                monster.changeState(MonsterState.Roam)
        else:
            movement(monster, monster.roamSpeed)

    @staticmethod
    def idleAggressive(monster, frameTime):
        BasicMonsterStates.idle(monster, frameTime)
        monster.surveyArea()
        monster.attemptAction()

    @staticmethod
    def roamAggressive(idleChance, movement, monster, frameTime):
        BasicMonsterStates.roam(idleChance, movement, monster, frameTime)
        monster.surveyArea()
        monster.attemptAction()

    @staticmethod
    def initializeHostile(monster):
        if monster.target:
            monster.pivot = monster.entity.getPosition().x
            monster.direction = monster.entity.getPosition().getDirection(monster.target.getPosition(), monster.entity.loopWidth)
            monster.targetTime = monster.hostileTimes.getRandom()
        else:
            monster.changeState(MonsterState.Idle)

    @staticmethod
    def initializeHostilePassive(monster):
        """ Changes a passive monster to become aggressive. """
        monster.handlers[MonsterState.Idle] = BasicMonsterStates.idleAggressive
        monster.handlers[MonsterState.Roam] = partial(BasicMonsterStates.roamAggressive, 0.3, MovementHelpers.walkJump)
        monster.initializers[MonsterState.Hostile] = BasicMonsterStates.initializeHostile
        BasicMonsterStates.initializeHostile(monster)

    @staticmethod
    def makeFriendly(monster):
        """ Changes an aggressive monster to become friendly. """
        monster.handlers[MonsterState.Idle] = BasicMonsterStates.idle
        monster.handlers[MonsterState.Roam] = partial(BasicMonsterStates.roam, 0.3, MovementHelpers.walkJump)

    @staticmethod
    def hostile(movement, monster, frameTime):
        if monster.stateTime >= monster.targetTime and monster.isTouchingGround and not monster.entity.animation.isPlaying("attack") or not monster.target:
            # State time has expired idle for a moment
            monster.changeState(MonsterState.Idle)
        else:
            render = monster.entity.render
            position = monster.entity.getPosition()
            if not render.hasMoved() or abs(position.x - monster.pivot) >= 10:
                monster.pivot = position.x
                monster.direction = position.getDirection(monster.target.physics.getCenter(), monster.entity.loopWidth)
            movement(monster, monster.hostileSpeed)
            monster.attemptAction()


class BaseMonster(Sentient):
    """ Base class for monsters. Provides the basic functionality.
    Tracks the state the monster is in and provides some helper functions for the basic state changes.
    """
    spawnTime = 500.0
    hostileTimes = RangeInt(seconds(12), seconds(20))
    deathTime = 400.0
    targetingTime = seconds(1)
    loot = []
    hostileSpeed = Vector(1.35, -4.0)
    dayDetectionArea = Rect()
    nightDetectionArea = Rect()

    defaultDayInitializers = {
        MonsterState.Idle: BasicMonsterStates.initializeIdle,
        MonsterState.Roam: BasicMonsterStates.initializeRoam,
        MonsterState.Hostile: BasicMonsterStates.initializeHostile,
    }

    defaultDayHandlers = {
        MonsterState.Spawn: BasicMonsterStates.spawn,
        MonsterState.Idle: BasicMonsterStates.idleAggressive,
        MonsterState.Death: BasicMonsterStates.death,
        MonsterState.Roam: partial(BasicMonsterStates.roamAggressive, 0.3, MovementHelpers.walkJump),
        MonsterState.Hostile: partial(BasicMonsterStates.hostile, MovementHelpers.walkJump),
    }

    defaultNightInitializers = {
        MonsterState.Idle: BasicMonsterStates.initializeIdle,
        MonsterState.Roam: BasicMonsterStates.initializeRoam,
        MonsterState.Hostile: BasicMonsterStates.initializeHostile,
    }

    defaultNightHandlers = {
        MonsterState.Spawn: BasicMonsterStates.spawn,
        MonsterState.Idle: BasicMonsterStates.idleAggressive,
        MonsterState.Death: BasicMonsterStates.death,
        MonsterState.Roam: partial(BasicMonsterStates.roamAggressive, 0.3, MovementHelpers.walkJump),
        MonsterState.Hostile: partial(BasicMonsterStates.hostile, MovementHelpers.walkJump),
    }

    growlSound = ""
    growlTime = RangeInt(minutes(1), minutes(3))

    def __init__(self, entity, dayInitializers=None, dayHandlers=None, nightInitializers=None, nightHandlers=None):
        super(BaseMonster, self).__init__(entity, MonsterState.Spawn, dayInitializers, dayHandlers, nightInitializers, nightHandlers)
        self.actions = []
        self.currentAction = None
        self.combat.setInvincible(True)
        self.entity.render.setColor(Color(255, 255, 255, 0))
        self.direction = TileVector(0, 0)
        self.speed = Vector(0, 0)
        self.target = None
        self.growlTimer = Timer(self.growlTime.getRandom())
        spawner = self.entity.monster.getSpawner()
        if spawner:
            spawner.combat.onHit.listen(self.handleSpawnerHit)

    def changeState(self, state):
        # Once the monster is dying, do not allow states changes
        if self.state is MonsterState.Death:
            return
        elif self.state is MonsterState.Spawn:
            self.combat.setInvincible(False)
        super(BaseMonster, self).changeState(state)

    def update(self, frameTime):
        """ Returns True if the child object should immediately return after calling this. """

        if not NetworkManager.isHost() or self.entity.effects.has('Stun'):
            return True
        if self.target and not self.target.combat.isAlive.get():
            self.target = None
        self.growlTimer.update(frameTime)
        if self.growlSound and self.growlTimer.expired() and (self.state == MonsterState.Idle or self.state == MonsterState.Roam or self.state == MonsterState.Hostile):
            self.growl()
        for action in self.actions:
            action.update(frameTime)
        super(BaseMonster, self).update(frameTime)

    def handleTimeStep(self, physics, time):
        if self.state is not MonsterState.Death:
            super(BaseMonster, self).handleTimeStep(physics, time)

    def handleHit(self, attacker, defender, attackData, damage):
        if (self.state is not MonsterState.Hostile or self.stateTime > seconds(2)) and self.state is not MonsterState.Action:
            self.target = attacker
            self.changeState(MonsterState.Hostile)

    def handleSpawnerHit(self, attacker, defender, attackData, damage):
        if (self.state is not MonsterState.Hostile or self.stateTime > seconds(2)) and self.state is not MonsterState.Action:
            self.target = attacker
            self.changeState(MonsterState.Hostile)

    def handleAliveChange(self, entity, previous, current):
        if not current:
            self.changeState(MonsterState.Death)
            entity.animation.onFinished.listen(self.finishedDeathAnimation)

    def finishedDeathAnimation(self, name, group):
        # Reset the state time here so that the basic death handles stateTime correctly
        self.stateTime = 0

    def performAction(self, action):
        action.perform(self)
        self.currentAction = action
        self.dayHandlers[MonsterState.Action] = action
        self.nightHandlers[MonsterState.Action] = action
        self.changeState(MonsterState.Action)

    def attemptAction(self):
        for action in self.actions:
            if action.shouldPerform(self):
                self.performAction(action)
                break

    def handleDay(self):
        super(BaseMonster, self).handleDay()
        self.detectionArea = self.dayDetectionArea

    def handleNight(self):
        super(BaseMonster, self).handleNight()
        self.detectionArea = self.nightDetectionArea

    def surveyArea(self):
        if self.detectionArea.width == 0:
            return
        # Area offset is always relative to the front of the monster
        body = self.entity.physics.getWorldBody()
        area = getRelativeArea(self.entity, body, self.detectionArea)
        game.debugRender.add(area, Color(255, 100, 100))
        realm = self.entity.realm
        enemies = getNearbyEnemies(self.entity.combat.team, realm.layers, area, onlyAlive=True, onlyTargetable=True)
        for enemy in enemies:
            body = enemy.physics.getWorldBody()
            if body.intersects(area, realm.size.width):
                self.target = enemy
                self.changeState(MonsterState.Hostile)
                break

    def growl(self):
        position = self.entity.getPosition()
        realm = self.entity.realm
        player, distance = realm.getClosestPlayer(position)
        if distance < 1000:
            game.audio.playAt(getRandomPath(self.growlSound), position, broadcast=True)
        self.growlTimer.reset(self.growlTime.getRandom())


class ActionHelpers(object):
    @staticmethod
    def checkMeleeRange(monster, area):
        body = monster.entity.physics.getWorldBody()
        finalArea = getRelativeArea(monster.entity, body, area)
        game.debugRender.add(finalArea, Color(185, 38, 101))
        realm = monster.entity.realm
        enemies = getNearbyEnemies(monster.entity.combat.team, realm.layers, body, onlyAlive=True, onlyTargetable=True)
        for enemy in enemies:
            enemyBody = enemy.physics.getWorldBody()
            if enemyBody.intersects(finalArea, realm.size.width) or enemyBody.intersects(body, realm.size.width):
                return True
        return False

    @staticmethod
    def checkBodyCollision(monster):
        rect = monster.entity.getRender().getRect()
        realm = monster.entity.realm
        enemies = getNearbyEnemies(monster.entity.combat.team, realm.layers, rect, onlyAlive=True, onlyTargetable=True)
        for enemy in enemies:
            if enemy.has('physics') and rect.intersects(enemy.physics.getWorldBody(), realm.size.width):
                return True
        return False

    @staticmethod
    def checkRangedRange(monster, entity, distance):
        # TODO: Consider different trajectories (arch vs straight)
        #TODO: Check to see if path is clear
        return False


class MonsterMeleeAttack(object):
    hitFrames = []
    power = 0
    detectionArea = Rect()
    cooldownTime = 0
    recoverTime = 0
    sound = ""
    soundDelay = 0

    def __init__(self):
        self.attackId = 0
        self.cooldown = Timer()
        self.hitStunTime = 800
        self.attackData = AttackData(self.power, AttackType.Melee, DamageType.Physical, knockback=2)
        self.recoverTimer = Timer()
        self.animation = ""
        self.attackTime = 0
        self.playedSound = False

    def update(self, frameTime):
        self.cooldown.update(frameTime)

    def shouldPerform(self, monster):
        return self.inRange(monster)

    def inRange(self, monster):
        return self.cooldown.expired() and ActionHelpers.checkMeleeRange(monster, self.detectionArea)

    def perform(self, monster, animation="attack"):
        self.animation = animation
        monster.entity.animation.play(self.animation)
        combat = monster.entity.combat
        combat.finishAttack(self.attackId)
        self.attackId = combat.startAttack()
        physics = monster.entity.physics
        physics.setVelocity(Vector(0, 0))
        self.monster = monster
        physics.onTimeStep.listen(self.handleTimeStep)
        self.attackTime = 0
        self.playedSound = False

    def updateAttackTime(self, physics, frameTime):
        self.attackTime += frameTime
        if not self.playedSound and self.sound and self.attackTime >= self.soundDelay:
            game.audio.playAt(getRandomPath(self.sound), physics.getCenter(), broadcast=True)
            self.playedSound = True

    def handleTimeStep(self, physics, frameTime):
        self.updateAttackTime(physics, frameTime)
        if not self.monster.entity.combat.isAlive.get():
            return True
        if not self.recoverTimer.expired():
            self.recoverTimer.update(frameTime)
            hasRecovered = self.recoverTimer.expired()
            if hasRecovered:
                self.monster.changeState(self.monster.previousState)
            return hasRecovered
        else:
            isFinished = not self.monster.entity.animation.isPlaying(self.animation)
            if not isFinished:
                # Check to see if the monster is hitting anyone
                if self.monster.entity.render.getFrame().index in self.hitFrames:
                    hitEnemies = damageEnemies(self.attackId, self.monster.entity, getHitEnemies(self.monster.entity), self.attackData)
                    if self.hitStunTime > 0:
                        for enemy in hitEnemies:
                            hitStun(enemy, self.hitStunTime)
            else:
                self.cooldown.reset(self.cooldownTime)
                if self.recoverTime > 0:
                    self.monster.entity.animation.play("idle")
                    physics.setVelocity(Vector(0.0, 0.0))
                    self.recoverTimer.reset(self.recoverTime)
                    return False
                else:
                    self.monster.changeState(self.monster.previousState)
            return isFinished

    def __call__(self, monster, frameTime):
        return True
