#!/usr/bin/env python3

import errno
import itertools
import logging
import logging.handlers
import math
import os
import random
import signal
import socket
import sys
import time

import bottisota
import bottisota.net
import bottisota.math
import bottisota.protocol

logger = logging.getLogger("bottisota")
logger.setLevel(logging.INFO)

logger.addHandler(logging.StreamHandler(sys.stderr))
logger.handlers[0].setFormatter(logging.Formatter("%(levelname)s|%(name)s|%(asctime)s|%(message)s"))

battle_logger = logging.getLogger("bottisota.battle")
battle_logger.addHandler(logging.StreamHandler(sys.stdout))

class Clock:

    def __init__(self):
        self.__ticks = 0

    def tick(self):
        self.__ticks += 1
        battle_logger.info("tick %d", self.__ticks)
        time.sleep(bottisota.TICK_MS / 1000)

    def now(self):
        return self.__ticks

CLOCK = Clock()

class World:

    def __init__(self):
        self.__things = {}

    def __iter_things(self):
        for thing_class in self.__things:
            for thing in self.__things[thing_class]:
                yield thing

    def __log_status(self):
        for thing in self.__iter_things():
            thing.log_status()

    def __move_things(self):
        for thing in self.__iter_things():
            thing.move()

    def __inflict_missile_damages(self):
        for missile in self.__things.get(Missile, []):
            missile.inflict_damage(iter(self.__things.get(Bot, [])))

    def __scan_bots(self):
        bot_iter = iter(self.__things.get(Bot, []))
        for scanning_bot, scanned_bot in itertools.permutations(bot_iter, 2):
            scanning_bot.scan(scanned_bot)

    def new_thing(self, thing_class, *args, **kwargs):
        thing = thing_class(*args, **kwargs)
        thing.log_status()
        self.__things.setdefault(thing_class, []).append(thing)

        return thing

    def update(self):
        self.__move_things()
        self.__inflict_missile_damages()
        self.__scan_bots()

        self.__log_status()

    def remove_destroyed_things(self):
        destroyed_things = []

        for thing in self.__iter_things():
            if thing.is_destroyed():
                destroyed_things.append(thing)

        for destroyed_thing in destroyed_things:
            self.__things[destroyed_thing.__class__].remove(destroyed_thing)

WORLD = World()

class Thing:

    _next_idnum = 1

    def __init__(self, x, y, health):
        self.idnum = self.__class__._get_next_idnum()
        self.x = x
        self.y = y
        self.__health = health

    @property
    def health(self):
        return self.__health

    @property
    def loc(self):
        return self.x, self.y

    @classmethod
    def _get_next_idnum(cls):
        next_idnum = cls._next_idnum
        cls._next_idnum += 1
        return next_idnum

    def destroy(self):
        self.add_damage(self.health)

    def is_destroyed(self):
        return self.health == 0

    def add_damage(self, damage):
        self.__health = max(0, self.health - damage)

    def _status_line(self):
        return "x %d, y %d, health %d" % (self.x, self.y, self.health)

    def log_status(self):
        battle_logger.info("%s %d: %s", self.__class__.__name__, self.idnum, self._status_line())

class MovableThing(Thing):

    def __init__(self, x, y, health, direction, initial_speed, target_speed):
        super().__init__(x, y, health)
        self.__speed = initial_speed
        self.__target_speed = target_speed
        self.__direction = direction % 360

    def drive(self, speed, direction):
        self.__target_speed = speed
        self.__direction = direction % 360

    @property
    def direction(self):
        return self.__direction

    @property
    def speed(self):
        return self.__speed

    def add_damage(self, damage):
        super().add_damage(damage)
        if self.is_destroyed():
            self.__target_speed = 0

    def _move_by(self, distance):
        direction_rad = math.radians(self.direction)
        dy = round(math.sin(direction_rad) * distance)
        dx = round(math.cos(direction_rad) * distance)
        if dx or dy:
            new_x, new_y = (self.x + dx, self.y + dy)
            if new_x < 0 or new_x >= bottisota.ARENA_WIDTH or new_y < 0 or new_y >= bottisota.ARENA_HEIGHT:
                new_x = min(max(new_x, 0), bottisota.ARENA_WIDTH - 1)
                new_y = min(max(new_y, 0), bottisota.ARENA_HEIGHT - 1)
                self.__target_speed = bottisota.DRV_SPEED_MIN
            self.x = new_x
            self.y = new_y

        self.__speed = self.__target_speed

    def move(self):
        self._move_by(self.speed * 1) # Must be called once per tick.

    def _status_line(self):
        return super()._status_line() + ", direction %d, speed %d" % (self.direction, self.speed)

class Missile(MovableThing):

    def __init__(self, x, y, speed, direction, distance):
        super().__init__(x, y, distance, direction, speed, bottisota.MSL_SPEED)
        self.__has_exploded = False

    def _move_by(self, distance):
        distance = min(self.health, distance)
        super()._move_by(distance)
        self.add_damage(distance)
        if self.speed == 0:
            self.destroy()

    def inflict_damage(self, target_things):
        if self.__has_exploded:
            return False

        if not self.is_destroyed():
            # Still flying.
            return False

        for target_thing in target_things:
            explosion_distance = bottisota.math.distance(self.loc, target_thing.loc)

            if explosion_distance == 0:
                damage = 40
            elif explosion_distance > 40:
                damage = 0
            else:
                damage = min(25, math.floor(1 / (explosion_distance / 150)))

            target_thing.add_damage(damage)

        self.__has_exploded = True

        return True

class Bot(MovableThing):

    def __init__(self, x, y, direction):
        super().__init__(x, y, bottisota.BOT_HEALTH_MAX, direction, bottisota.DRV_SPEED_MIN, bottisota.DRV_SPEED_MIN)

        self.__scn_direction = 0
        self.__scn_resolution = 0
        self.__scn_distance = 0
        self.__scn_idnum = 0

    def scan(self, target_bot):
        if self.__scn_resolution == 0:
            return

        if target_bot.is_destroyed():
            return

        target_distance = bottisota.math.distance(self.loc, target_bot.loc)
        if target_distance > bottisota.SCN_DISTANCE:
            return

        target_direction = bottisota.math.direction(self.loc, target_bot.loc)
        left_bound = (self.__scn_direction - self.__scn_resolution) % 360
        right_bound = (self.__scn_direction + self.__scn_resolution) % 360
        if left_bound <= target_direction <= right_bound:
            distance_min = max(target_distance - bottisota.SCN_ERROR_PER_RESOLUTION * self.__scn_resolution, 0)
            distance_max = min(target_distance + bottisota.SCN_ERROR_PER_RESOLUTION * self.__scn_resolution, bottisota.SCN_DISTANCE)
            self.__scn_distance = random.randint(round(distance_min), round(distance_max))
            self.__scn_idnum = target_bot.idnum

    def _status_line(self):
        return super()._status_line() + ", scan %d±%d" % (self.__scn_direction, self.__scn_resolution)

    def launch_missile(self, direction, distance):
        WORLD.new_thing(Missile, self.x, self.y, self.speed, direction, distance)

    def start_scan(self, direction, resolution):
        self.__scn_direction = direction % 360
        self.__scn_resolution = resolution
        self.__scn_distance = 0
        self.__scn_idnum = 0

    def stop_scan(self):
        scn_distance = self.__scn_distance
        scn_idnum = self.__scn_idnum
        self.__scn_distance = 0
        self.__scn_idnum = 0
        self.__scn_resolution = 0
        self.__scn_direction = 0

        return scn_distance, scn_idnum

class Controller:

    def __init__(self, sock):
        self.__link = bottisota.net.Link(sock, bottisota.protocol.ServerStack())
        self.__syscall = None
        x, y, = bottisota.math.randloc()
        direction = bottisota.math.randdirection()

        bot = WORLD.new_thing(Bot, x, y, direction)

        self.__syscalls = {
            bottisota.protocol.MSG_POS: SyscallPOS(bot),
            bottisota.protocol.MSG_CLK: SyscallCLK(bot),
            bottisota.protocol.MSG_DRV: SyscallDRV(bot),
            bottisota.protocol.MSG_SCN: SyscallSCN(bot),
            bottisota.protocol.MSG_MSL: SyscallMSL(bot),
        }

    def shutdown(self):
        self.__link.disconnect()

    def enter_syscall(self):
        try:
            msg, args = self.__link.recv(False)
        except bottisota.net.NoMessageError:
            # Nothing to evaluate, no message was received.
            return

        self.__syscall = self.__syscalls[msg]
        self.__syscall.enter(*args)

    def leave_syscall(self):
        if self.__syscall:
            # Ensure the current syscall is reset, no matter what
            # happens when leaving or sending back the results.
            syscall = self.__syscall
            self.__syscall = None

            values = syscall.leave()
            self.__link.send(syscall.msg, syscall.err, *values)

class Syscall:

    msg = None

    def __init__(self, bot):
        self.bot = bot
        self.err = bottisota.protocol.ERR_UNKNOWN

    def enter(self):
        if self.bot.is_destroyed():
            self.err = bottisota.protocol.ERR_DESTROYED
            return False

        self.err = bottisota.protocol.ERR_UNKNOWN

        return True

    def leave(self):
        return ()

class SyscallMSL(Syscall):

    msg = bottisota.protocol.MSG_MSL

    def enter(self, direction, distance):
        if not super().enter():
            return False

        if distance > bottisota.MSL_DISTANCE_MAX:
            self.err = bottisota.protocol.ERR_BADARG
            return False

        self.err = bottisota.protocol.ERR_OK
        self.bot.launch_missile(direction, distance)

        return True

class SyscallSCN(Syscall):

    msg = bottisota.protocol.MSG_SCN

    def enter(self, direction, resolution):
        if not super().enter():
            return False

        if bottisota.SCN_RESOLUTION_MIN > resolution > bottisota.SCN_RESOLUTION_MAX:
            self.err = bottisota.protocol.ERR_BADARG
            return False

        self.err = bottisota.protocol.ERR_OK
        self.bot.start_scan(direction, resolution)

        return True

    def leave(self):
        return self.bot.stop_scan()

class SyscallDRV(Syscall):

    msg = bottisota.protocol.MSG_DRV

    def enter(self, direction, speed):
        if not super().enter():
            return False

        if speed > bottisota.DRV_SPEED_MAX:
            self.err = bottisota.protocol.ERR_BADARG
            return False

        self.err = bottisota.protocol.ERR_OK
        self.bot.drive(speed, direction)

        return True

    def leave(self):
        return self.bot.speed,

class SyscallCLK(Syscall):

    msg = bottisota.protocol.MSG_CLK

    def enter(self):
        self.err = bottisota.protocol.ERR_OK

    def leave(self):
        return CLOCK.now(),

class SyscallPOS(Syscall):

    msg = bottisota.protocol.MSG_POS

    def enter(self):
        self.err = bottisota.protocol.ERR_OK

    def leave(self):
        return self.bot.x, self.bot.y, self.bot.speed, self.bot.direction

class Battle:

    def __init__(self):
        self.__controllers = []
        self.__is_on = True

    def wait_for_controller(self, serversock):
        controllersock = serversock.accept()[0]
        logger.info("controller connected")

        controller = Controller(controllersock)
        self.__controllers.append(controller)

    def stop(self, signum, stack_frame):
        self.__is_on = False

    def play(self):
        initial_controller_count = len(self.__controllers)
        battle_logger.info("begin")
        try:
            while len(self.__controllers) == initial_controller_count and self.__is_on:
                CLOCK.tick()
                self.__enter_syscalls()
                WORLD.update()
                self.__leave_syscalls()
                WORLD.remove_destroyed_things()

        finally:
            battle_logger.info("end")
            first_error = sys.exc_info()[1]
            while self.__controllers:
                controller = self.__controllers.pop()
                try:
                    controller.shutdown()
                except Exception as e:
                    first_error = first_error or e
                    continue

            if first_error:
                raise first_error

    def __enter_syscalls(self):
        failed_controllers = []

        for controller in self.__controllers:
            try:
                controller.enter_syscall()
            except:
                logger.exception("failed to read a syscall from a controller")
                failed_controllers.append(controller)

        for failed_controller in failed_controllers:
            self.__controllers.remove(failed_controller)
            try:
                failed_controller.shutdown()
            except:
                logger.exception("failed to shutdown a failed controller")
                continue

    def __leave_syscalls(self):
        for controller in self.__controllers:
            controller.leave_syscall()

class Server:

    def __init__(self, address=bottisota.net.SOCKET_ADDRESS):
        self.__address = address
        self.__sock = None
        self.__monitor_sock = None

    def __enter__(self):
        sock = socket.socket(socket.AF_UNIX)

        try:
            os.remove(self.__address)
        except OSError as e:
            if e.errno != errno.ENOENT:
                raise

        try:
            sock.bind(self.__address)
            sock.listen(2)
        except:
            sock.close()
            os.remove(self.__address)
            raise

        self.__sock = sock
        return self

    def __exit__(self, *args):
        sock = self.__sock
        self.__sock = None

        first_error = None

        try:
            os.remove(sock.getsockname())
        except Exception as e:
            first_error = first_error or e

        try:
            sock.close()
        except Exception as e:
            first_error = first_error or e

        if first_error:
            raise first_error

    def run(self):
        battle = Battle()

        logger.info("waiting for controllers")
        battle.wait_for_controller(self.__sock)
        battle.wait_for_controller(self.__sock)

        signal.signal(signal.SIGUSR1, battle.stop)
        signal.signal(signal.SIGUSR2, battle.stop)
        signal.signal(signal.SIGALRM, battle.stop)
        signal.signal(signal.SIGHUP, battle.stop)
        signal.signal(signal.SIGINT, battle.stop)
        signal.signal(signal.SIGTERM, battle.stop)

        battle.play()

def main():
    logger.info("started")
    with Server() as server:
        server.run()

if __name__ == "__main__":
    main()
