import hashlib
from math import cos, sin, radians, atan2
import threading
import time


class BoardThread(threading.Thread):
    def __init__(self, tick, deltatime, sleep_time=None):
        threading.Thread.__init__(self)
        self._tick = tick
        self._deltatime = deltatime
        self._sleep_time = sleep_time if sleep_time is not None else deltatime

    def run(self):
        while 1:
            self._tick(self._deltatime)
            time.sleep(self._sleep_time)

    def get_sleep_time(self):
        return self._sleep_time


class Missile:
    def __init__(self, board, xy, degree, distance, speed, damage, owner=None):
        assert isinstance(board, Board)
        self._board = board
        self._owner = owner
        self._x, self._y = xy
        self._degree = degree
        self._distance = distance
        self._speed = speed
        self._damage = damage
        self._space = 0.0

    def tick(self, deltatime):
        dx = self._speed * cos(radians(self._degree)) * deltatime
        dy = self._speed * sin(radians(self._degree)) * deltatime
        self._distance -= self._speed * deltatime
        if self._distance < 1:
            self._board.spawn_explosion((self._x + dx + self._distance * cos(radians(self._degree)),
                                         self._y + dy + self._distance * sin(radians(self._degree))),
                                        self._damage,
                                        self._owner)
            self._board.remove_missile(self)
        self._x += dx
        self._y += dy

    def get_status(self):
        return dict(
            x=self._x,
            y=self._y,
            degree=self._degree,
            distance=self._distance,
            speed=self._speed,
            damage=self._damage
        )


class Explosion:
    def __init__(self, board, xy, damage, owner=None):
        assert isinstance(board, Board)
        self._owner = owner
        self._board = board
        self._x, self._y = xy
        self._damage = damage
        self._step = 0
        self._explosion_time = 1.0  # sec

    def tick(self, deltatime):
        self._step += 1
        if 1 == self._step:
            # do damage
            for robot in self._board.robots.values():
                if robot.is_dead():
                    continue
                d, a = robot.distance((self._x, self._y))
                total_damage = 0
                for distance, hp_damage in self._damage:
                    if d <= distance:
                        total_damage += hp_damage
                if total_damage:
                    robot.take_damage(total_damage)
                    if robot.is_dead():
                        if robot.get_name() not in self._board.kdr:
                            self._board.kdr[robot.get_name()] = {'kill': 0, 'death': 0}
                        self._board.kdr[robot.get_name()]['death'] += 1
                        if self._owner and self._owner.get_name() not in self._board.kdr:
                            self._board.kdr[self._owner.get_name()] = {'kill': 0, 'death': 0}
                        self._board.kdr[self._owner.get_name()]['kill'] += 1
        elif self._step > 1:
            self._explosion_time -= deltatime
            if self._explosion_time <= 0.0:
                self._board.remove_explosion(self)

    def get_status(self):
        return dict(
            x=self._x,
            y=self._y,
            step=self._step,
            damage=self._damage,
            time=self._explosion_time
        )


class RobotAlreadyExistsException(Exception):
    pass


class Board:
    def __init__(self, size=(1000, 1000)):
        self._size = size
        self.robots = {}
        self._missiles = {}
        self._explosions = {}
        self._radar = {}
        self._wall_hit_damage = 2
        self._join_status = None
        self.kdr = {}

    def add_robot(self, robot):
        if robot.get_name() not in self.robots:
            self.robots[robot.get_name()] = robot
            return True
        return False

    def remove_robot(self, robot):
        if robot.get_name() in self.robots:
            del self.robots[robot.get_name()]
            return True
        return False

    def reinit(self, size=(1000, 1000)):
        self.__init__(size)

    def get_status(self):
        ret = dict(
            size=self._size,
            robots=[v.get_status() for v in self.robots.values()],
            missiles=dict([(k, v.get_status()) for k, v in self._missiles.items()]),
            explosions=dict([(k, v.get_status()) for k, v in self._explosions.items()]),
            radar=dict(self._radar),
            kdr=self.kdr
        )
        self._radar = dict([(k, v) for k, v in self._radar.items() if v['spawntime'] + 1.0 > time.time()])
        return ret

    def radar(self, scanning_robot, xy, max_scan_distance, degree, resolution):
        key = hashlib.md5(repr(dict(time=time.time(), xy=xy, degree=degree, resolution=resolution,
                                    distance=max_scan_distance))).hexdigest()
        self._radar[key] = dict(xy=xy, degree=degree, resolution=resolution, distance=max_scan_distance,
                                spawntime=time.time())
        ret = []
        degree %= 360
        for robot in [x for x in self.robots.values() if x != scanning_robot]:
            if robot.is_dead():
                continue
            distance, angle = robot.distance(xy)
            angle = (180 + angle) % 360
            if self.angle_distance(angle, degree) > resolution:
                continue
            if distance > max_scan_distance:
                continue
            ret.append(distance)
        return min(ret) if ret else 0

    def detect_collision(self, robot, xy, dxy):
        x, y = xy
        dx, dy = dxy
        # collision with other robots
        step = max(abs(dx), abs(dy))
        if step:  # the robot is moving
            x0, y0 = x - dx, y - dy
            stepx = float(dx) / float(step)
            stepy = float(dy) / float(step)
            for other_robot in [j for j in self.robots.values() if j != robot]:
                xp, yp = x0, y0
                for i in xrange(int(step) + 1):
                    dist, angle = other_robot.distance((xp, yp))
                    if dist < 2:  # 1 per ogni robot
                        other_robot.take_damage(self._wall_hit_damage)
                        other_robot.block()
                        teta = atan2(dy, dx)
                        xp, yp = other_robot.get_xy()
                        xp -= 2 * cos(teta)
                        yp -= 2 * sin(teta)
                        return xp, yp, self._wall_hit_damage
                    xp += stepx
                    yp += stepy
        # COLLISION WITH WALLS
        if x < 0:
            y = y - dy * x / dx
            x = 0
            return x, y, self._wall_hit_damage
        if x > self._size[0]:
            y = y - dy * (x - self._size[0]) / dx
            x = self._size[0]
            return x, y, self._wall_hit_damage
        if y < 0:
            x = x - dx * y / dy
            y = 0
            return x, y, self._wall_hit_damage
        if y > self._size[1]:
            x = x - dx * (y - self._size[1]) / dy
            y = self._size[1]
            return x, y, self._wall_hit_damage
        return None

    def spawn_missile(self, xy, degree, distance, bullet_speed, bullet_damage, owner=None):
        key = hashlib.md5(repr(
            dict(time=time.time(), xy=xy, degree=degree, distance=distance, bullet_speed=bullet_speed,
                 bullet_damage=bullet_damage))).hexdigest()
        missile = Missile(self, xy, degree, distance, bullet_speed, bullet_damage, owner)
        self._missiles[key] = missile

    def remove_missile(self, missile):
        self._missiles = dict([(k, x) for k, x in self._missiles.items() if x != missile])
        # del self._missiles[self._missiles.index(missile)]

    def spawn_explosion(self, xy, damage, owner=None):
        key = hashlib.md5(repr(dict(time=time.time(), xy=xy, damage=damage))).hexdigest()
        self._explosions[key] = Explosion(self, xy, damage, owner)

    def remove_explosion(self, explosion):
        self._explosions = dict([(k, x) for k, x in self._explosions.items() if x != explosion])

    def tick(self, deltatime=0.125):
        # manage missiles
        for m in self._missiles.values():
            assert isinstance(m, Missile)
            m.tick(deltatime)
        # manage explosions
        for e in self._explosions.values():
            assert isinstance(e, Explosion)
            e.tick(deltatime)
        # manage robots
        for r in self.robots.values():
            r.tick(deltatime)

    def new_robot(self, clazz, name):
        return clazz(self, name, len(self.robots))

    @staticmethod
    def angle_distance(angle, degree):
        ret = (angle - degree) if angle > degree else (degree - angle)
        if ret > 180:
            ret = 360 - ret
        return ret
