#!/usr/bin/env python3

import logging
import logging.handlers
import sys

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"))

bot_logger = logging.getLogger("bottisota.bot")
bot_logger.addHandler(logging.StreamHandler(sys.stdout))

class BotStateMachine:

    def __init__(self):
        self.link = bottisota.net.Link(bottisota.net.connect_socket(),
                                       bottisota.protocol.BotStack())

    @staticmethod
    def state(func):
        def wrapper(*args):
            return func(*args)

        wrapper.label = func.__name__
        wrapper.__doc__ = func.__doc__
        wrapper.__name__ = func.__name__

        return wrapper

    def run(self):
        state_transitions = {}

        for state, symbol, next_state in self.STATE_TRANSITIONS:
            key = (state.label, symbol)
            if key in state_transitions:
                raise RuntimeError("duplicate state transition", key)
            state_transitions[key] = next_state

        state = self.STATE_TRANSITIONS[0][0]

        while True:
            symbol = state(self)
            state = state_transitions[(state.label, symbol)]

    STATE_TRANSITIONS = ()

class Controller(BotStateMachine):

    def __init__(self):
        BotStateMachine.__init__(self)

        self._loc = None
        self._speed = 0
        self._direction = 0
        self._tick = 0

        self._scn_direction = 0
        self._scn_resolution = bottisota.SCN_RESOLUTION_MAX

        self._enemy_idnum = None
        self._enemy_heading = 0
        self._enemy_speed = 0
        self._enemy_loc = None
        self._enemy_loc_tick = None

    def _syscall_pos(self):
        x, y, self._speed, self._direction = self.link.syscall(bottisota.protocol.MSG_POS)
        self._tick += 1
        self._guesstimate_loc()
        self._loc = x, y

    def _syscall_drv(self, direction, speed):
        self.link.syscall(bottisota.protocol.MSG_DRV, direction, speed)
        self._tick += 1
        self._direction = direction
        self._speed = speed
        self._guesstimate_loc()

    def _move_to_center(self, speed):
        center_loc = (bottisota.ARENA_WIDTH // 2, bottisota.ARENA_HEIGHT // 2)
        self._syscall_drv(bottisota.math.direction(self._loc, center_loc), speed)

    def _scan(self):
        enemy_distance, enemy_idnum = self.link.syscall(bottisota.protocol.MSG_SCN, self._scn_direction, self._scn_resolution)
        self._tick += 1
        self._guesstimate_loc()

        if enemy_distance:
            self._guesstimate_enemy_loc(enemy_distance, enemy_idnum)
            return True

        return False

    def _guesstimate_enemy_loc(self, enemy_distance, enemy_idnum):
        self._enemy_idnum = enemy_idnum
        if self._loc is not None:
            x, y = self._loc
            last_enemy_loc_tick = self._enemy_loc_tick
            last_enemy_loc = self._enemy_loc
            enemy_x, enemy_y = self._enemy_loc = bottisota.math.travel(x, y, self._scn_direction, enemy_distance)
            self._enemy_loc_tick = self._tick
            if last_enemy_loc is not None:
                self._enemy_speed = bottisota.math.distance(last_enemy_loc, self._enemy_loc) // (self._tick - last_enemy_loc_tick)
                self._enemy_heading = bottisota.math.direction(last_enemy_loc, self._enemy_loc)
            bot_logger.info('I guess bot %d is at %d, %d, heading to %d with speed %d', self._enemy_idnum, enemy_x, enemy_y, self._enemy_heading, self._enemy_speed)

    def _guesstimate_loc(self):
        if self._loc is not None:
            x, y = self._loc
            x, y = self._loc = bottisota.math.travel(x, y, self._direction, self._speed)
            bot_logger.info('I guess I am at %d, %d, heading to %d', x, y, self._direction)

    ## Operation states below

    @BotStateMachine.state
    def sync(self):
        self._tick, = self.link.syscall(bottisota.protocol.MSG_CLK)
        self._guesstimate_loc()

        return ""

    @BotStateMachine.state
    def move_to_center_fast(self):
        self._move_to_center(bottisota.DRV_SPEED_MAX)

        return ""

    @BotStateMachine.state
    def move_to_center_slowly(self):
        self._move_to_center(max(self._speed // 2, 1))

        return ""

    @BotStateMachine.state
    def stop(self):
        self._syscall_drv(self._direction, 0)
        self._syscall_pos()

        return ""

    @BotStateMachine.state
    def find_center(self):
        center_loc = (bottisota.ARENA_WIDTH // 2, bottisota.ARENA_HEIGHT // 2)
        self._syscall_pos()

        distance_to_center = bottisota.math.distance(self._loc, center_loc)

        if distance_to_center < 5:
            return "CLOSE_ENOUGH"

        if distance_to_center < 5 * self._speed:
            return "NEARBY"

        return "FARAWAY"

    @BotStateMachine.state
    def scan_wide(self):
        self._scn_resolution = bottisota.SCN_RESOLUTION_MAX
        if self._scan():
            return "ACK"

        return "NACK"

    @BotStateMachine.state
    def change_scan_sector(self):
        self._scn_direction = (self._scn_direction + bottisota.SCN_RESOLUTION_MAX * 2) % 360
        return ""

    @BotStateMachine.state
    def narrow_scan_sector(self):
        self._scn_resolution = max(5, self._scn_resolution // 2)

        if self._scn_resolution == 5:
            return "MIN"

        return ""

    @BotStateMachine.state
    def scan_sector(self):
        if self._scan():
            return "ACK"

        return "NACK"

    @BotStateMachine.state
    def scan_left_sector(self):
        self._scn_direction = (self._scn_direction - 2 * self._scn_resolution) % 360
        if self._scan():
            return "ACK"

        return "NACK"

    @BotStateMachine.state
    def scan_right_sector(self):
        self._scn_direction = (self._scn_direction + 4 * self._scn_resolution) % 360
        if self._scan():
            return "ACK"

        return "NACK"

    @BotStateMachine.state
    def widen_scan_sector(self):
        self._scn_resolution = bottisota.SCN_RESOLUTION_MAX # min(bottisota.SCN_RESOLUTION_MAX, self._scn_resolution * 2)

        if self._scn_resolution == bottisota.SCN_RESOLUTION_MAX:
            return "MAX"

        return ""

    @BotStateMachine.state
    def eval_enemy_motion(self):
        last_enemy_loc = self._enemy_loc

        if not self._scan():
            return "NACK"

        if bottisota.math.distance(self._loc, last_enemy_loc) >= bottisota.math.distance(self._loc, self._enemy_loc):
            return "APPROACHING"

        return "DISTANCING"

    @BotStateMachine.state
    def standby_and_shoot(self):
        enemy_distance = bottisota.math.distance(self._loc, self._enemy_loc)
        msl_distance = min(bottisota.MSL_DISTANCE_MAX, enemy_distance - 2 * self._enemy_speed)
        msl_direction = bottisota.math.direction(self._loc, self._enemy_loc)
        if msl_distance > 10:
            self.link.syscall(bottisota.protocol.MSG_MSL, msl_direction, msl_distance)
            self._tick += 1
            self._guesstimate_loc()
        self._syscall_drv(self._direction, 0)
        return ""

    @BotStateMachine.state
    def follow_and_shoot(self):
        enemy_distance = bottisota.math.distance(self._loc, self._enemy_loc)
        msl_distance = min(bottisota.MSL_DISTANCE_MAX, enemy_distance + 2 * self._enemy_speed)
        msl_direction = bottisota.math.direction(self._loc, self._enemy_loc)
        self.link.syscall(bottisota.protocol.MSG_MSL, msl_direction, msl_distance)
        self._tick += 1
        self._guesstimate_loc()
        self._syscall_drv(msl_direction, bottisota.DRV_SPEED_MAX)
        return ""

    STATE_TRANSITIONS = (
        (sync                 , ""            , find_center          ),
        (find_center          , "FARAWAY"     , move_to_center_fast  ),
        (move_to_center_fast  , ""            , find_center          ),
        (find_center          , "CLOSE_ENOUGH", stop                 ),
        (find_center          , "NEARBY"      , move_to_center_slowly),
        (move_to_center_slowly, ""            , find_center          ),
        (stop                 , ""            , scan_wide            ),
        (scan_wide            , "ACK"         , narrow_scan_sector   ),
        (narrow_scan_sector   , ""            , scan_sector          ),
        (narrow_scan_sector   , "MIN"         , eval_enemy_motion    ),
        (eval_enemy_motion    , "APPROACHING" , standby_and_shoot    ),
        (eval_enemy_motion    , "DISTANCING"  , follow_and_shoot     ),
        (standby_and_shoot    , ""            , eval_enemy_motion    ),
        (follow_and_shoot     , ""            , eval_enemy_motion    ),
        (eval_enemy_motion    , "NACK"        , scan_sector          ),
        (scan_sector          , "ACK"         , narrow_scan_sector   ),
        (scan_sector          , "NACK"        , scan_left_sector     ),
        (scan_left_sector     , "NACK"        , scan_right_sector    ),
        (scan_left_sector     , "ACK"         , narrow_scan_sector   ),
        (scan_right_sector    , "NACK"        , widen_scan_sector    ),
        (scan_right_sector    , "ACK"         , narrow_scan_sector   ),
        (widen_scan_sector    , ""            , scan_sector          ),
        (widen_scan_sector    , "MAX"         , scan_wide            ),
        (scan_wide            , "NACK"        , change_scan_sector   ),
        (change_scan_sector   , ""            , scan_wide            ),
    )

Controller().run()
