# ==============================================================================
# Copyright (C) 2019 - Philip Paquette
#
#  This program is free software: you can redistribute it and/or modify it under
#  the terms of the GNU Affero General Public License as published by the Free
#  Software Foundation, either version 3 of the License, or (at your option) any
#  later version.
#
#  This program is distributed in the hope that it will be useful, but WITHOUT
#  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
#  FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more
#  details.
#
#  You should have received a copy of the GNU Affero General Public License along
#  with this program.  If not, see <https://www.gnu.org/licenses/>.
# ==============================================================================
""" DAIDE Notifications - Contains a list of responses sent by the server to the client """
from diplomacy import Map
from diplomacy.daide.clauses import String, Power, Province, Turn, Unit, add_parentheses, strip_parentheses, \
    parse_string
from diplomacy.daide import tokens
from diplomacy.daide.tokens import Token
from diplomacy.daide.utils import bytes_to_str, str_to_bytes

class DaideNotification():
    """ Represents a DAIDE response. """
    def __init__(self, **kwargs):
        """ Constructor """
        del kwargs                      # Unused kwargs
        self._bytes = b''
        self._str = ''

    def __bytes__(self):
        """ Returning the bytes representation of the response """
        return self._bytes

    def __str__(self):
        """ Returning the string representation of the response """
        return bytes_to_str(self._bytes)

    def to_bytes(self):
        """ Returning the bytes representation of the response """
        return bytes(self)

    def to_string(self):
        """ Returning the string representation of the response """
        return str(self)

class MapNameNotification(DaideNotification):
    """ Represents a MAP DAIDE response. Sends the name of the current map to the client.
        Syntax:
            MAP ('name')
    """
    def __init__(self, map_name, **kwargs):
        """ Builds the response
            :param map_name: String. The name of the current map.
        """
        super(MapNameNotification, self).__init__(**kwargs)
        self._bytes = bytes(tokens.MAP) \
                      + bytes(parse_string(String, map_name))

class HelloNotification(DaideNotification):
    """ Represents a HLO DAIDE response. Sends the power to be played by the client with the passcode to rejoin the
        game and the details of the game.
        Syntax:
            HLO (power) (passcode) (variant) (variant) ...
        Variant syntax:
            LVL n           # Level of the syntax accepted
            MTL seconds     # Movement time limit
            RTL seconds     # Retreat time limit
            BTL seconds     # Build time limit
            DSD             # Disables the time limit when a client disconects
            AOA             # Any orders accepted
        LVL 10:
        Variant syntax:
            PDA             # Accept partial draws
            NPR             # No press during retreat phases
            NPB             # No press during build phases
            PTL seconds     # Press time limit
    """
    def __init__(self, power_name, passcode, level, deadline, rules, **kwargs):
        """ Builds the response
            :param power_name: The name of the power being played.
            :param passcode: Integer. A passcode to rejoin the game.
            :param level: Integer. The daide syntax level of the game
            :param deadline: Integer. The number of seconds per turn (0 to disable)
            :param rules: The list of game rules.
        """
        super(HelloNotification, self).__init__(**kwargs)
        power = parse_string(Power, power_name)
        passcode = Token(from_int=passcode)

        if 'NO_PRESS' in rules:
            level = 0
        variants = add_parentheses(bytes(tokens.LVL) + bytes(Token(from_int=level)))

        if deadline > 0:
            variants += add_parentheses(bytes(tokens.MTL) + bytes(Token(from_int=deadline)))
            variants += add_parentheses(bytes(tokens.RTL) + bytes(Token(from_int=deadline)))
            variants += add_parentheses(bytes(tokens.BTL) + bytes(Token(from_int=deadline)))

        if 'NO_CHECK' in rules:
            variants += add_parentheses(bytes(tokens.AOA))

        self._bytes = bytes(tokens.HLO) \
                      + add_parentheses(bytes(power)) \
                      + add_parentheses(bytes(passcode)) \
                      + add_parentheses(bytes(variants))

class SupplyCenterNotification(DaideNotification):
    """ Represents a SCO DAIDE notification. Sends the current supply centre ownership.
        Syntax:
            SCO (power centre centre ...) (power centre centre ...) ...
    """
    def __init__(self, powers_centers, map_name, **kwargs):
        """ Builds the notification
            :param powers_centers: A dict of {power_name: centers} objects
            :param map_name: The name of the map
        """
        super(SupplyCenterNotification, self).__init__(**kwargs)
        remaining_scs = Map(map_name).scs[:]
        all_powers_bytes = []

        # Parsing each power
        for power_name in sorted(powers_centers):
            centers = sorted(powers_centers[power_name])
            power_clause = parse_string(Power, power_name)
            power_bytes = bytes(power_clause)

            for center in centers:
                sc_clause = parse_string(Province, center)
                power_bytes += bytes(sc_clause)
                remaining_scs.remove(center)

            all_powers_bytes += [power_bytes]

        # Parsing unowned center
        uno_token = tokens.UNO
        power_bytes = bytes(uno_token)

        for center in remaining_scs:
            sc_clause = parse_string(Province, center)
            power_bytes += bytes(sc_clause)

        all_powers_bytes += [power_bytes]

        # Storing full response
        self._bytes = bytes(tokens.SCO) \
                      + b''.join([add_parentheses(power_bytes) for power_bytes in all_powers_bytes])

class CurrentPositionNotification(DaideNotification):
    """ Represents a NOW DAIDE notification. Sends the current turn, and the current unit positions.
        Syntax:
            NOW (turn) (unit) (unit) ...
        Unit syntax:
            power unit_type province
            power unit_type province MRT (province province ...)
    """
    def __init__(self, phase_name, powers_units, powers_retreats, **kwargs):
        """ Builds the notification
            :param phase_name: The name of the current phase (e.g. 'S1901M')
            :param powers: A list of `diplomacy.engine.power.Power` objects
        """
        super(CurrentPositionNotification, self).__init__(**kwargs)
        units_bytes_buffer = []

        # Turn
        turn_clause = parse_string(Turn, phase_name)

        # Units
        for power_name, units in sorted(powers_units.items()):
            # Regular units
            for unit in units:
                unit_clause = parse_string(Unit, '%s %s' % (power_name, unit))
                units_bytes_buffer += [bytes(unit_clause)]

            # Dislodged units
            for unit, retreat_provinces in sorted(powers_retreats[power_name].items()):
                unit_clause = parse_string(Unit, '%s %s' % (power_name, unit))
                retreat_clauses = [parse_string(Province, province) for province in retreat_provinces]
                units_bytes_buffer += [add_parentheses(strip_parentheses(bytes(unit_clause))
                                                       + bytes(tokens.MRT)
                                                       + add_parentheses(b''.join([bytes(province)
                                                                                   for province in retreat_clauses])))]

        # Storing full response
        self._bytes = bytes(tokens.NOW) + bytes(turn_clause) + b''.join(units_bytes_buffer)

class MissingOrdersNotification(DaideNotification):
    """ Represents a MIS DAIDE response. Sends the list of unit for which an order is missing or indication about
        required disbands or builds.
        Syntax:
            MIS (unit) (unit) ...
            MIS (unit MRT (province province ...)) (unit MRT (province province ...)) ...
            MIS (number)
    """
    def __init__(self, phase_name, power, **kwargs):
        """ Builds the response
            :param phase_name: The name of the current phase (e.g. 'S1901M')
            :param power: The power to check for missing orders
            :type power: diplomacy.engine.power.Power
        """
        super(MissingOrdersNotification, self).__init__(**kwargs)
        assert phase_name[-1] in 'MRA', 'Invalid phase "%s"' & phase_name
        {'M': self._build_movement_phase,
         'R': self._build_retreat_phase,
         'A': self._build_adjustment_phase}[phase_name[-1]](power)

    def _build_movement_phase(self, power):
        """ Builds the missing orders response for a movement phase """
        units_with_no_order = [unit for unit in power.units]

        # Removing units for which we have orders
        for key, value in power.orders.items():
            unit = key                              # Regular game {e.g. 'A PAR': '- BUR')
            if key[0] in 'RIO':                     # No-check game (key is INVALID, ORDER x, REORDER x)
                unit = ' '.join(value.split()[:2])
            if unit in units_with_no_order:
                units_with_no_order.remove(unit)

        # Storing full response
        self._bytes = bytes(tokens.MIS) + \
                      b''.join([bytes(parse_string(Unit, '%s %s' % (power.name, unit)))
                                for unit in units_with_no_order])

    def _build_retreat_phase(self, power):
        """ Builds the missing orders response for a retreat phase """
        units_bytes_buffer = []

        units_with_no_order = {unit: retreat_provinces for unit, retreat_provinces in power.retreats.items()}

        # Removing units for which we have orders
        for key, value in power.orders.items():
            unit = key                              # Regular game {e.g. 'A PAR': '- BUR')
            if key[0] in 'RIO':                     # No-check game (key is INVALID, ORDER x, REORDER x)
                unit = ' '.join(value.split()[:2])
            if unit in units_with_no_order:
                del units_with_no_order[unit]

        for unit, retreat_provinces in sorted(units_with_no_order.items(),
                                              key=lambda key_val: key_val[0].split()[-1]):
            unit_clause = parse_string(Unit, '%s %s' % (power.name, unit))
            retreat_clauses = [parse_string(Province, province)
                               for province in retreat_provinces]
            units_bytes_buffer += [add_parentheses(strip_parentheses(bytes(unit_clause))
                                                   + bytes(tokens.MRT)
                                                   + add_parentheses(b''.join([bytes(province)
                                                                               for province in retreat_clauses])))]

        self._bytes = bytes(tokens.MIS) + b''.join(units_bytes_buffer)

    def _build_adjustment_phase(self, power):
        """ Builds the missing orders response for a build phase """
        disbands_status = len(power.units) - len(power.centers)

        if disbands_status < 0:
            available_homes = power.homes[:]

            # Removing centers for which it's impossible to build
            for unit in [unit.split() for unit in power.units]:
                province = unit[1]
                if province in available_homes:
                    available_homes.remove(province)

            disbands_status = max(-len(available_homes), disbands_status)

        self._bytes += bytes(tokens.MIS) + add_parentheses(bytes(Token(from_int=disbands_status)))

class OrderResultNotification(DaideNotification):
    """ Represents a ORD DAIDE response. Sends the result of an order after the turn has been processed.
        Syntax:
            ORD (turn) (order) (result)
            ORD (turn) (order) (result RET)
        Result syntax:
            SUC         # Order succeeded (can apply to any order).
            BNC         # Move bounced (only for MTO, CTO or RTO orders).
            CUT         # Support cut (only for SUP orders).
            DSR         # Move via convoy failed due to dislodged convoying fleet (only for CTO orders).
            NSO         # No such order (only for SUP, CVY or CTO orders).
            RET         # Unit was dislodged and must retreat.
    """
    def __init__(self, phase_name, order_bytes, results, **kwargs):
        """ Builds the response
            :param phase_name: The name of the current phase (e.g. 'S1901M')
            :param order_bytes: The bytes received for the order
            :param results: An array containing the error codes.
        """
        super(OrderResultNotification, self).__init__(**kwargs)
        turn_clause = parse_string(Turn, phase_name)
        if not results or 0 in results:                 # Order success response
            result_clause = tokens.SUC
        else:                                           # Generic order failure response
            result_clause = tokens.NSO

        self._bytes = bytes(tokens.ORD) \
                      + bytes(turn_clause) \
                      + add_parentheses(order_bytes) \
                      + add_parentheses(bytes(result_clause))

class TimeToDeadlineNotification(DaideNotification):
    """ Represents a TME DAIDE response. Sends the time to the next deadline.
        Syntax:
            TME (seconds)
    """
    def __init__(self, seconds, **kwargs):
        """ Builds the response
            :param seconds: Integer. The number of seconds before deadline
        """
        super(TimeToDeadlineNotification, self).__init__(**kwargs)
        self._bytes = bytes(tokens.TME) + add_parentheses(bytes(tokens.Token(from_int=seconds)))

class PowerInCivilDisorderNotification(DaideNotification):
    """ Represents a CCD DAIDE response. Sends the name of the power in civil disorder.
        Syntax:
            CCD (power)
    """
    def __init__(self, power_name, **kwargs):
        """ Builds the response
            :param power_name: The name of the power being played.
        """
        super(PowerInCivilDisorderNotification, self).__init__(**kwargs)
        power = parse_string(Power, power_name)
        self._bytes = bytes(tokens.CCD) + add_parentheses(bytes(power))

class PowerIsEliminatedNotification(DaideNotification):
    """ Represents a OUT DAIDE response. Sends the name of the power eliminated.
        Syntax:
            OUT (power)
    """
    def __init__(self, power_name, **kwargs):
        """ Builds the response
            :param power_name: The name of the power being played.
        """
        super(PowerIsEliminatedNotification, self).__init__(**kwargs)
        power = parse_string(Power, power_name)
        self._bytes = bytes(tokens.OUT) + add_parentheses(bytes(power))

class DrawNotification(DaideNotification):
    """ Represents a DRW DAIDE response. Indicates that the game has ended due to a draw
        Syntax:
            DRW
    """
    def __init__(self, **kwargs):
        """ Builds the response
        """
        super(DrawNotification, self).__init__(**kwargs)
        self._bytes = bytes(tokens.DRW)

class MessageFromNotification(DaideNotification):
    """ Represents a FRM DAIDE response. Indicates that the game has ended due to a draw
        Syntax:
            FRM (power) (power power ...) (press_message)
            FRM (power) (power power ...) (reply)
    """
    def __init__(self, from_power_name, to_power_names, message, **kwargs):
        """ Builds the response
        """
        super(MessageFromNotification, self).__init__(**kwargs)

        from_power_clause = bytes(parse_string(Power, from_power_name))
        to_powers_clause = b''.join([bytes(parse_string(Power, power_name)) for power_name in to_power_names])
        message_clause = str_to_bytes(message)

        self._bytes = bytes(tokens.FRM) \
                      + b''.join([add_parentheses(clause)
                                  for clause in [from_power_clause, to_powers_clause, message_clause]])

class SoloNotification(DaideNotification):
    """ Represents a SLO DAIDE response. Indicates that the game has ended due to a solo by the specified power
        Syntax:
            SLO (power)
    """
    def __init__(self, power_name, **kwargs):
        """ Builds the response
            :param power_name: The name of the power being solo.
        """
        super(SoloNotification, self).__init__(**kwargs)
        power = parse_string(Power, power_name)
        self._bytes = bytes(tokens.SLO) + add_parentheses(bytes(power))

class SummaryNotification(DaideNotification):
    """ Represents a SMR DAIDE response. Sends the summary for each power at the end of the game
        Syntax:
            SMR (turn) (power_summary) ...
        power_summary syntax:
            power ('name') ('version') number_of_centres
            power ('name') ('version') number_of_centres year_of_elimination
    """
    def __init__(self, phase_name, powers, daide_users, years_of_elimination, **kwargs):
        """ Builds the Notification """
        super(SummaryNotification, self).__init__(**kwargs)
        powers_smrs_clause = []

        # Turn
        turn_clause = parse_string(Turn, phase_name)

        for power, daide_user, year_of_elimination in zip(powers, daide_users, years_of_elimination):
            power_smr_clause = []

            name = daide_user.client_name if daide_user else power.get_controller()
            version = daide_user.client_version if daide_user else 'v0.0.0'

            power_name_clause = bytes(parse_string(Power, power.name))
            power_smr_clause.append(power_name_clause)

            # (name)
            name_clause = bytes(parse_string(String, name))
            power_smr_clause.append(name_clause)

            # (version)
            version_clause = bytes(parse_string(String, version))
            power_smr_clause.append(version_clause)

            number_of_centres_clause = bytes(Token(from_int=len(power.centers)))
            power_smr_clause.append(number_of_centres_clause)

            if not power.centers:
                year_of_elimination_clause = bytes(Token(from_int=year_of_elimination))
                power_smr_clause.append(year_of_elimination_clause)

            power_smr_clause = add_parentheses(b''.join(power_smr_clause))
            powers_smrs_clause.append(power_smr_clause)

        self._bytes = bytes(tokens.SMR) + bytes(turn_clause) + b''.join(powers_smrs_clause)

class TurnOffNotification(DaideNotification):
    """ Represents an OFF DAIDE response. Requests a client to exit
        Syntax:
            OFF
    """
    def __init__(self, **kwargs):
        """ Builds the response """
        super(TurnOffNotification, self).__init__(**kwargs)
        self._bytes = bytes(tokens.OFF)

MAP = MapNameNotification
HLO = HelloNotification
SCO = SupplyCenterNotification
NOW = CurrentPositionNotification
MIS = MissingOrdersNotification
ORD = OrderResultNotification
TME = TimeToDeadlineNotification
CCD = PowerInCivilDisorderNotification
OUT = PowerIsEliminatedNotification
DRW = DrawNotification
FRM = MessageFromNotification
SLO = SoloNotification
SMR = SummaryNotification
OFF = TurnOffNotification