from siege import StateManager
from siege.component import Direction, Slope
from siege.log import Log
from siege.network import NetworkManager
from siege.util import Random, RangeInt, Rect, seconds, Sizes, TileVector, Timer, Vector
from siege.world import World
from siege.world.realm import TilePosition


class Sentient(object):
    '''Base class for sentient entities. Provides basic functionality for tracking and changing states.'''

    jumpTime = 300
    idleTimes = RangeInt(500, seconds(2))
    roamTimes = RangeInt(seconds(3), seconds(5))
    roamSpeed = Vector(1.0, -4.0)
    defaultDayInitializers = {}
    defaultDayHandlers = {}
    defaultNightInitializers = {}
    defaultNightHandlers = {}

    def __init__(self, entity, startingState, dayInitializers=None, dayHandlers=None, nightInitializers=None, nightHandlers=None):
        self.entity = entity
        self.combat = entity.combat
        self.targetTime = 0
        self.jumping = Timer()

        #Setup default handlers for the ones not specified
        def getStateHandlers(default, custom):
            handlers = {}
            for state, handler in default.iteritems():
                handlers[state] = custom[state] if custom and state in custom else handler
            return handlers

        self.dayInitializers = getStateHandlers(self.defaultDayInitializers, dayInitializers)
        self.dayHandlers = getStateHandlers(self.defaultDayHandlers, dayHandlers)
        self.nightInitializers = getStateHandlers(self.defaultNightInitializers, nightInitializers)
        self.nightHandlers = getStateHandlers(self.defaultNightHandlers, nightHandlers)
        # If no night initializers/handlers then set them to be same as day
        if not self.nightInitializers:
            self.nightInitializers = self.dayInitializers
        if not self.nightInitializers:
            self.nightInitializers = self.dayInitializers

        if World.get().time.isNight():
            self.handleNight()
        else:
            self.handleDay()

        self.state = startingState
        self.changeState(startingState)
        self.combat.setInvincible(True)
        self.direction = TileVector(0, 0)
        self.speed = Vector(0, 0)
        self.movementTile = TilePosition()
        self.movementTimer = Timer()

        if NetworkManager.isHost():
            self.entity.physics.onTimeStep.listen(self.handleTimeStep)
            self.combat.onHit.listen(self.handleHit)
            self.combat.isAlive.onChange.listen(self.handleAliveChange)
        self.isTouchingGround = True
        self.wasTouchingGround = True

    def changeState(self, state):
        # TODO: Consider aborting if state is self.state and providing an optional force=False
        if state is not self.state:
            self.previousState = self.state
            self.state = state
        self.targetTime = 0
        if state in self.initializers:
            self.initializers[state](self)
        self.stateTime = 0

    def handleDay(self):
        self.initializers = self.dayInitializers
        self.handlers = self.dayHandlers

    def handleNight(self):
        self.initializers = self.nightInitializers
        self.handlers = self.nightHandlers

    def update(self, frameTime):
        '''Returns True if the child object should immediately return after calling this.'''
        if not NetworkManager.isHost():
            return True
        self.stateTime += frameTime
        self.movementTimer.update(frameTime)
        self.wasTouchingGround = self.isTouchingGround
        self.isTouchingGround = self.entity.physics.isTouching(Direction.BOTTOM)
        #Call whatever handler is registered to the current state
        try:
            if self.handlers[self.state](self, frameTime):
                return True
        except KeyError:
            Log.info("KeyError with {} for {}".format(self.state, self.entity))
            raise

    def handleTimeStep(self, physics, time):
        if not self.jumping.expired():
            self.jumping.update(int(time))
            velocity = physics.getVelocity()
            velocity.y = self.speed.y
            physics.setVelocity(velocity)
            if self.jumping.expired():
                self.entity.animation.play("fall")

    def handleHit(self, attacker, defender, attackData, damage):
        pass

    def handleAliveChange(self, entity, previous, current):
        pass

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

    def surveyArea(self):
        pass

    def idle(self, animation):
        animation.play("idle")

    def move(self, animation):
        animation.play("move")

    def jump(self, animation):
        animation.play("jump")

    def fall(self, animation):
        animation.play("fall")


class MovementHelpers(object):
    @staticmethod
    def walkJump(sentient, speed):
        sentient.speed = speed
        if not sentient.jumping.expired():
            return
        realm = sentient.entity.realm
        physics = sentient.entity.physics
        position = sentient.entity.getPosition()
        velocity = physics.getVelocity()
        velocity.x = speed.x * sentient.direction.x
        physics.flipX(sentient.direction.x == -1)

        if sentient.isTouchingGround:
            animation = sentient.entity.animation
            if not sentient.wasTouchingGround:
                animation.play("land")
            if animation.isPlaying("land"):
                velocity.x = 0
            elif sentient.movementTimer.expired() or abs(sentient.movementPos.x - position.x) >= 8 or abs(sentient.movementPos.y - position.y) >= 8:
                #If blocks are directly in front of the sentient then check if we can jump over them. If not then reverse direction.
                #If no block if front of the sentient but there are some above it then randomly jump

                # Checks to see if an area is open to move into given the velocity.

                hasTileInFront = False
                hasHoleAbove = False
                body = physics.getWorldBody()
                change = Sizes.TILE * sentient.direction.x
                area = Rect(body.x, body.y + body.height, body.width, 1)
                collision = realm.physics.getCollisionY(sentient.entity.id, physics, area, Direction.TOP, body.width)
                # If moving up a slope then need to check at the top of the slope
                if collision.component and ((collision.slope & Slope.LEFT and sentient.direction.x == 1) or (collision.slope & Slope.RIGHT and sentient.direction.x == -1)):
                    slopeBody = collision.component.getWorldBody()
                    area.y = slopeBody.y - body.height
                else:
                    area.y = body.y
                area.height = body.height
                if sentient.direction.x == 1:
                    direction = Direction.LEFT
                    area.x += body.width
                else:
                    direction = Direction.RIGHT
                    area.x += change
                area.width = abs(change)

                collision = realm.physics.getCollisionX(sentient.entity.id, physics, area, direction, body.width)
                if collision.component and collision.slope == Slope.NONE:
                    hasTileInFront = True
                    jumpHeight = physics.calculateJumpHeight(speed.y, sentient.jumpTime / StateManager.TIMESTEP_TIME)
                    jumpTiles = max(0, int(jumpHeight / Sizes.TILE) - 1)
                    for i in xrange(jumpTiles):
                        area.y -= Sizes.TILE
                        collision = realm.physics.getCollisionX(sentient.entity.id, physics, area, direction, body.width)
                        if not collision.component:
                            area.x = body.x
                            area.width = body.width
                            area.height += Sizes.TILE * i
                            collision = realm.physics.getCollisionY(sentient.entity.id, physics, area, Direction.BOTTOM, body.width)
                            # There is something blocking the entity from jumping up to this area
                            hasHoleAbove = not bool(collision.component)
                            break

                if hasTileInFront and not hasHoleAbove:
                    sentient.direction.x = -sentient.direction.x
                elif hasTileInFront or (hasHoleAbove and Random.getBool()):
                    sentient.jump(animation)
                    velocity.y = speed.y
                    #TODO: Scale the jump time to only how high the entity needs to jump
                    sentient.jumping.reset(sentient.jumpTime)
                else:
                    sentient.move(animation)
                sentient.movementPos = position
                sentient.movementTimer.reset(seconds(1))
        physics.setVelocity(velocity)

    # @staticmethod
    # def hover(sentient, speed):
    #     sentient.speed = speed
    #     ground = sentient.entity.realm.layers[Layer.Ground]
    #     physics = sentient.entity.physics
    #     position = sentient.entity.getPosition()

    @staticmethod
    def flyTowardsTarget(sentient):
        #Move towards the target.
        #If something is blocking the path then randomly pick a direction and prefer that direction until hitting another wall
        realm = sentient.entity.realm
        target = None
        render = sentient.entity.getRender()
        physics = sentient.entity.physics
        target.y -= 20
        sentient.direction = sentient.entity.getPosition().getDirection(target.getPosition(), realm.size.width)
        velocity = physics.getVelocity()
        body = physics.getWorldBody()
        if abs(body.x - target.x) < 16:
            sentient.direction.x = -1 if render.getFlipX() else 1
        else:
            physics.flipX(sentient.direction.x == -1)
        if abs(body.y - target.y) < 8:
            sentient.direction.y = 0
        velocity.x = sentient.speed.x * sentient.direction.x
        velocity.y = sentient.speed.y * sentient.direction.y
        sentient.entity.animation.play("move")
        physics.setVelocity(velocity)
