# ==============================================================================
# Copyright 2019 - Philip Paquette. All Rights Reserved.
#
# NOTICE:  All information contained herein is, and remains the property of the authors
#   listed above. The intellectual and technical concepts contained herein are proprietary
#   and may be covered by U.S. and Foreign Patents, patents in process, and are protected
#   by trade secret or copyright law. Dissemination of this information or reproduction of
#   this material is strictly forbidden unless prior written permission is obtained.
# ==============================================================================
""" Orders - Contains utilities to convert orders between string format and webdiplomacy.net format """
import logging
from queue import Queue
from diplomacy import Map
from diplomacy.integration.webdiplomacy_net.utils import CACHE

# Constants
LOGGER = logging.getLogger(__name__)

def is_adjacent_for_convoy(loc_1, loc_2, map_object):
    """ Checks if two locations are adjacent (for convoy purposes)
            - If loc_1 and loc_2 are water, loc_1 and loc_2 must be adjacent
            - If loc_1 or loc_2 is land, then they are not adjacent
            - If loc_1 or loc_2 are coast, then the other locations needs to be a water loc at most 1 loc away

        :type map_object: diplomacy.Map
    """
    area_1 = map_object.area_type(loc_1)
    area_2 = map_object.area_type(loc_2)

    # Lands can't be used for convoys, so they are never adjacent
    if area_1 == 'LAND' or area_2 == 'LAND':
        return False

    # If both units are on water, checking if they are adjacent
    if area_1 == 'WATER' and area_2 == 'WATER':
        return map_object.abuts('F', loc_1, '-', loc_2)

    # Moving from coast to water or vice-versa
    if area_2 == 'COAST' and area_1 == 'WATER':
        return is_adjacent_for_convoy(loc_2, loc_1, map_object)
    if area_1 == 'COAST' and area_2 == 'WATER':
        for loc_with_coast in map_object.find_coasts(loc_1):
            if map_object.abuts('F', loc_with_coast, '-', loc_2):
                return True

    # Otherwise, not adjacent
    return False

def find_convoy_path(src, dest, map_object, game=None):
    """ Finds a convoy path from src to dest
        :param src: The source location (e.g. 'BRE')
        :param dest: The destination location (e.g. 'LON')
        :param map_object: A diplomacy.Map object representation of the current map
        :param game: Optional. The current game object to retrieve the list of fleets.
        :return: Either an empty list if a convoy is not possible between src and dest
                 or a list of [src, fleet1, fleet2, ..., fleet_n, dest] to use to convoy A `src` - `dest`.
        :type map_object: diplomacy.Map
        :type game: diplomacy.Game
    """
    if map_object.area_type(src) != 'COAST' or map_object.area_type(dest) != 'COAST':
        return []

    # Finding all water locs
    water_locs = {loc.upper() for loc in map_object.locs if map_object.area_type(loc.upper()) == 'WATER'}

    # Finding all convoyers
    convoyers = water_locs
    if game is not None:
        convoyers = set()
        for power_units in game.get_units().values():
            convoyers |= {unit[2:] for unit in power_units if unit[0] == 'F' and unit[2:] in water_locs}

    # Finding the minimum set of units that would allow a convoy
    fleets_in_convoy = set()
    for nb_fleets in map_object.convoy_paths:
        for start_loc, fleet_locs, dest_locs in map_object.convoy_paths[nb_fleets]:
            if start_loc != src or dest not in dest_locs:                   # Src or dest do not match
                continue
            if not fleet_locs.issubset(convoyers):                          # Missing some convoyers to use this path
                continue
            fleets_in_convoy = fleet_locs
            break
        if fleets_in_convoy:
            break
    else:
        return []                                                           # No convoy path found.

    # Finding a path from src to dest using those convoys
    # Using breadth first search
    queue = Queue()
    item = (src,), fleets_in_convoy
    queue.put(item)

    while not queue.empty():
        current_path, remaining_fleets = queue.get()

        # Checking if this path is valid
        if len(current_path) > 1 and is_adjacent_for_convoy(current_path[-1], dest, map_object):
            return list(current_path) + [dest]

        # Trying to add all remaining fleets
        for fleet in remaining_fleets:
            if not is_adjacent_for_convoy(current_path[-1], fleet, map_object):
                continue
            new_path = tuple(list(current_path) + [fleet])
            item = new_path, remaining_fleets - {fleet}
            queue.put(item)

    # No paths found
    return []


class Order():
    """ Class to convert order from string representation to dictionary (webdiplomacy.net) representation """

    def __init__(self, order, map_id=None, map_name=None, phase_type=None, game=None):
        """ Constructor
            :param order: An order (either as a string 'A PAR H' or as a dictionary)
            :param map_id: Optional. The map id of the map where orders are submitted (webdiplomacy format)
            :param map_name: Optional. The map name of the map where orders are submitted.
            :param phase_type: Optional. The phase type ('M', 'R', 'A') to disambiguate orders to send.
            :param game: Optional. The diplomacy.Game object to build the correct convoy path.
        """
        self.map_name = 'standard'
        self.phase_type = 'M'
        self.order_str = ''
        self.order_dict = {}

        # Detecting the map name
        if map_id is not None:
            if map_id not in CACHE['ix_to_map']:
                raise ValueError('Map with id %s is not supported.' % map_id)
            self.map_name = CACHE['ix_to_map'][map_id]
        elif map_name is not None:
            if map_name not in CACHE['map_to_ix']:
                raise ValueError('Map with name %s is not supported.' % map_name)
            self.map_name = map_name

        # Detecting the phase type
        if isinstance(phase_type, str) and phase_type in 'MRA':
            self.phase_type = phase_type

        # Building the order
        if isinstance(order, str):
            self._build_from_string(order, game=game)
        elif isinstance(order, dict):
            self._build_from_dict(order)
        else:
            raise ValueError('Expected order to be a string or a dictionary.')

    def _build_from_string(self, order, game=None):
        """ Builds this object from a string
            :type order: str
            :type game: diplomacy.Game
        """
        # pylint: disable=too-many-return-statements,too-many-branches,too-many-statements
        words = order.split()

        # --- Wait / Waive ---
        # [{"id": "56", "unitID": null, "type": "Wait", "toTerrID": "", "fromTerrID": "", "viaConvoy": ""}]
        if len(words) == 1 and words[0] == 'WAIVE':
            self.order_str = 'WAIVE'
            self.order_dict = {'terrID': None,
                               'unitType': '',
                               'type': 'Wait',
                               'toTerrID': '',
                               'fromTerrID': '',
                               'viaConvoy': ''}
            return

        # Validating
        if len(words) < 3:
            LOGGER.error('Unable to parse the order "%s". Require at least 3 words', order)
            return

        short_unit_type, loc_name, order_type = words[:3]
        if short_unit_type not in 'AF':
            LOGGER.error('Unable to parse the order "%s". Valid unit types are "A" and "F".', order)
            return
        if order_type not in 'H-SCRBD':
            LOGGER.error('Unable to parse the order "%s". Valid order types are H-SCRBD', order)
            return
        if loc_name not in CACHE[self.map_name]['loc_to_ix']:
            LOGGER.error('Received invalid loc "%s" for map "%s".', loc_name, self.map_name)
            return

        # Extracting territories
        unit_type = {'A': 'Army', 'F': 'Fleet'}[short_unit_type]
        terr_id = CACHE[self.map_name]['loc_to_ix'][loc_name]

        # --- Hold ---
        # {"id": "76", "unitID": "19", "type": "Hold", "toTerrID": "", "fromTerrID": "", "viaConvoy": ""}
        if order_type == 'H':
            self.order_str = '%s %s H' % (short_unit_type, loc_name)
            self.order_dict = {'terrID': terr_id,
                               'unitType': unit_type,
                               'type': 'Hold',
                               'toTerrID': '',
                               'fromTerrID': '',
                               'viaConvoy': ''}

        # --- Move ---
        # {"id": "73", "unitID": "16", "type": "Move", "toTerrID": "25", "fromTerrID": "", "viaConvoy": "Yes",
        # "convoyPath": ["22", "69"]},
        # {"id": "74", "unitID": "17", "type": "Move", "toTerrID": "69", "fromTerrID": "", "viaConvoy": "No"}
        elif order_type == '-':
            if len(words) < 4:
                LOGGER.error('[Move] Unable to parse the move order "%s". Require at least 4 words', order)
                LOGGER.error(order)
                return

            # Building map
            map_object = Map(self.map_name)
            convoy_path = []

            # Getting destination
            to_loc_name = words[3]
            to_terr_id = CACHE[self.map_name]['loc_to_ix'].get(to_loc_name, None)

            # Deciding if this move is doable by convoy or not
            if unit_type != 'Army':
                via_flag = ''
            else:
                reachable_by_land = map_object.abuts('A', loc_name, '-', to_loc_name)
                via_convoy = bool(words[-1] == 'VIA') or not reachable_by_land
                via_flag = ' VIA' if via_convoy else ''
                convoy_path = find_convoy_path(loc_name, to_loc_name, map_object, game)

            if to_loc_name is None:
                LOGGER.error('[Move] Received invalid to loc "%s" for map "%s".', to_terr_id, self.map_name)
                LOGGER.error(order)
                return

            self.order_str = '%s %s - %s%s' % (short_unit_type, loc_name, to_loc_name, via_flag)
            self.order_dict = {'terrID': terr_id,
                               'unitType': unit_type,
                               'type': 'Move',
                               'toTerrID': to_terr_id,
                               'fromTerrID': '',
                               'viaConvoy': 'Yes' if via_flag else 'No'}
            if convoy_path:
                self.order_dict['convoyPath'] = [CACHE[self.map_name]['loc_to_ix'][loc] for loc in convoy_path[:-1]]

        # --- Support hold ---
        # {"id": "73", "unitID": "16", "type": "Support hold", "toTerrID": "24", "fromTerrID": "", "viaConvoy": ""}
        elif order_type == 'S' and '-' not in words:
            if len(words) < 5:
                LOGGER.error('[Support H] Unable to parse the support hold order "%s". Require at least 5 words', order)
                LOGGER.error(order)
                return

            # Getting supported unit
            to_loc_name = words[4][:3]
            to_terr_id = CACHE[self.map_name]['loc_to_ix'].get(to_loc_name, None)

            if to_loc_name is None:
                LOGGER.error('[Support H] Received invalid to loc "%s" for map "%s".', to_terr_id, self.map_name)
                LOGGER.error(order)
                return

            self.order_str = '%s %s S %s' % (short_unit_type, loc_name, to_loc_name)
            self.order_dict = {'terrID': terr_id,
                               'unitType': unit_type,
                               'type': 'Support hold',
                               'toTerrID': to_terr_id,
                               'fromTerrID': '',
                               'viaConvoy': ''}

        # --- Support move ---
        # {"id": "73", "unitID": "16", "type": "Support move", "toTerrID": "24", "fromTerrID": "69", "viaConvoy": ""}
        elif order_type == 'S':
            if len(words) < 6:
                LOGGER.error('Unable to parse the support move order "%s". Require at least 6 words', order)
                return

            # Building map
            map_object = Map(self.map_name)
            convoy_path = []

            # Getting supported unit
            move_index = words.index('-')
            to_loc_name = words[move_index + 1][:3]                         # Removing coast from dest
            from_loc_name = words[move_index - 1]
            to_terr_id = CACHE[self.map_name]['loc_to_ix'].get(to_loc_name, None)
            from_terr_id = CACHE[self.map_name]['loc_to_ix'].get(from_loc_name, None)

            if to_loc_name is None:
                LOGGER.error('[Support M] Received invalid to loc "%s" for map "%s".', to_terr_id, self.map_name)
                LOGGER.error(order)
                return
            if from_loc_name is None:
                LOGGER.error('[Support M] Received invalid from loc "%s" for map "%s".', from_terr_id, self.map_name)
                LOGGER.error(order)
                return

            # Deciding if we are support a move by convoy or not
            if words[move_index - 2] != 'F' and map_object.area_type(from_loc_name) == 'COAST':
                convoy_path = find_convoy_path(from_loc_name, to_loc_name, map_object, game)

            self.order_str = '%s %s S %s - %s' % (short_unit_type, loc_name, from_loc_name, to_loc_name)
            self.order_dict = {'terrID': terr_id,
                               'unitType': unit_type,
                               'type': 'Support move',
                               'toTerrID': to_terr_id,
                               'fromTerrID': from_terr_id,
                               'viaConvoy': ''}
            if convoy_path:
                self.order_dict['convoyPath'] = [CACHE[self.map_name]['loc_to_ix'][loc] for loc in convoy_path[:-1]]

        # --- Convoy ---
        # {"id": "79", "unitID": "22", "type": "Convoy", "toTerrID": "24", "fromTerrID": "20", "viaConvoy": "",
        # "convoyPath": ["20", "69"]}
        elif order_type == 'C':
            if len(words) < 6:
                LOGGER.error('[Convoy] Unable to parse the convoy order "%s". Require at least 6 words', order)
                LOGGER.error(order)
                return

            # Building map
            map_object = Map(self.map_name)

            # Getting supported unit
            move_index = words.index('-')
            to_loc_name = words[move_index + 1]
            from_loc_name = words[move_index - 1]
            to_terr_id = CACHE[self.map_name]['loc_to_ix'].get(to_loc_name, None)
            from_terr_id = CACHE[self.map_name]['loc_to_ix'].get(from_loc_name, None)

            if to_loc_name is None:
                LOGGER.error('[Convoy] Received invalid to loc "%s" for map "%s".', to_terr_id, self.map_name)
                LOGGER.error(order)
                return
            if from_loc_name is None:
                LOGGER.error('[Convoy] Received invalid from loc "%s" for map "%s".', from_terr_id, self.map_name)
                LOGGER.error(order)
                return

            # Finding convoy path
            convoy_path = find_convoy_path(from_loc_name, to_loc_name, map_object, game)

            self.order_str = '%s %s C A %s - %s' % (short_unit_type, loc_name, from_loc_name, to_loc_name)
            self.order_dict = {'terrID': terr_id,
                               'unitType': unit_type,
                               'type': 'Convoy',
                               'toTerrID': to_terr_id,
                               'fromTerrID': from_terr_id,
                               'viaConvoy': ''}
            if convoy_path:
                self.order_dict['convoyPath'] = [CACHE[self.map_name]['loc_to_ix'][loc] for loc in convoy_path[:-1]]

        # --- Retreat ---
        # {"id": "152", "unitID": "18", "type": "Retreat", "toTerrID": "75", "fromTerrID": "", "viaConvoy": ""}
        elif order_type == 'R':
            if len(words) < 4:
                LOGGER.error('[Retreat] Unable to parse the move order "%s". Require at least 4 words', order)
                LOGGER.error(order)
                return

            # Getting destination
            to_loc_name = words[3]
            to_terr_id = CACHE[self.map_name]['loc_to_ix'].get(to_loc_name, None)

            if to_loc_name is None:
                return

            self.order_str = '%s %s R %s' % (short_unit_type, loc_name, to_loc_name)
            self.order_dict = {'terrID': terr_id,
                               'unitType': unit_type,
                               'type': 'Retreat',
                               'toTerrID': to_terr_id,
                               'fromTerrID': '',
                               'viaConvoy': ''}

        # --- Disband (R phase) ---
        # {"id": "152", "unitID": "18", "type": "Disband", "toTerrID": "", "fromTerrID": "", "viaConvoy": ""}
        elif order_type == 'D' and self.phase_type == 'R':
            loc_name = loc_name[:3]
            terr_id = CACHE[self.map_name]['loc_to_ix'][loc_name]
            self.order_str = '%s %s D' % (short_unit_type, loc_name)
            self.order_dict = {'terrID': terr_id,
                               'unitType': unit_type,
                               'type': 'Disband',
                               'toTerrID': '',
                               'fromTerrID': '',
                               'viaConvoy': ''}

        # --- Build Army ---
        # [{"id": "56", "unitID": null, "type": "Build Army", "toTerrID": "37", "fromTerrID": "", "viaConvoy": ""}]
        elif order_type == 'B' and short_unit_type == 'A':
            self.order_str = 'A %s B' % loc_name
            self.order_dict = {'terrID': terr_id,
                               'unitType': 'Army',
                               'type': 'Build Army',
                               'toTerrID': terr_id,
                               'fromTerrID': '',
                               'viaConvoy': ''}

        # -- Build Fleet ---
        # [{"id": "56", "unitID": null, "type": "Build Fleet", "toTerrID": "37", "fromTerrID": "", "viaConvoy": ""}]
        elif order_type == 'B' and short_unit_type == 'F':
            self.order_str = 'F %s B' % loc_name
            self.order_dict = {'terrID': terr_id,
                               'unitType': 'Fleet',
                               'type': 'Build Fleet',
                               'toTerrID': terr_id,
                               'fromTerrID': '',
                               'viaConvoy': ''}

        # Disband (A phase)
        # {"id": "152", "unitID": null, "type": "Destroy", "toTerrID": "18", "fromTerrID": "", "viaConvoy": ""}
        elif order_type == 'D':
            loc_name = loc_name[:3]
            terr_id = CACHE[self.map_name]['loc_to_ix'][loc_name]
            self.order_str = '%s %s D' % (short_unit_type, loc_name)
            self.order_dict = {'terrID': terr_id,
                               'unitType': unit_type,
                               'type': 'Destroy',
                               'toTerrID': terr_id,
                               'fromTerrID': '',
                               'viaConvoy': ''}

    def _build_from_dict(self, order):
        """ Builds this object from a dictionary
            :type order: dict
        """
        # pylint: disable=too-many-return-statements
        terr_id = order.get('terrID', None)
        unit_type = order.get('unitType', None)
        order_type = order.get('type', None)
        to_terr_id = order.get('toTerrID', '')
        from_terr_id = order.get('fromTerrID', '')
        via_convoy = order.get('viaConvoy', '')

        # Using to_terr_id if terr_id is None
        terr_id = terr_id if terr_id is not None else to_terr_id

        # Overriding unit type for builds
        if order_type == 'Build Army':
            unit_type = 'Army'
        elif order_type == 'Build Fleet':
            unit_type = 'Fleet'
        elif order_type in ('Destroy', 'Wait') and unit_type not in ('Army', 'Fleet'):
            unit_type = '?'

        # Validating order
        if unit_type not in ('Army', 'Fleet', '?'):
            LOGGER.error('Received invalid unit type "%s". Expected "Army" or "Fleet".', unit_type)
            return
        if order_type not in ('Hold', 'Move', 'Support hold', 'Support move', 'Convoy', 'Retreat', 'Disband',
                              'Build Army', 'Build Fleet', 'Wait', 'Destroy'):
            LOGGER.error('Received invalid order type "%s".', order_type)
            return
        if terr_id not in CACHE[self.map_name]['ix_to_loc'] and terr_id is not None:
            LOGGER.error('Received invalid loc "%s" for map "%s".', terr_id, self.map_name)
            return
        if via_convoy not in ('Yes', 'No', '', None):
            LOGGER.error('Received invalid via convoy "%s". Expected "Yes" or "No" or "".', via_convoy)
            return

        # Extracting locations
        loc_name = CACHE[self.map_name]['ix_to_loc'].get(terr_id, None)
        to_loc_name = CACHE[self.map_name]['ix_to_loc'].get(to_terr_id, None)
        from_loc_name = CACHE[self.map_name]['ix_to_loc'].get(from_terr_id, None)

        # Building order
        # --- Hold ---
        # {"id": "76", "unitID": "19", "type": "Hold", "toTerrID": "", "fromTerrID": "", "viaConvoy": ""}
        if order_type == 'Hold':
            self.order_str = '%s %s H' % (unit_type[0], loc_name)
            self.order_dict = {'terrID': terr_id,
                               'unitType': unit_type,
                               'type': 'Hold',
                               'toTerrID': '',
                               'fromTerrID': '',
                               'viaConvoy': ''}

        # --- Move ---
        # {"id": "73", "unitID": "16", "type": "Move", "toTerrID": "25", "fromTerrID": "", "viaConvoy": "Yes",
        # "convoyPath": ["22", "69"]},
        # {"id": "74", "unitID": "17", "type": "Move", "toTerrID": "69", "fromTerrID": "", "viaConvoy": "No"}
        elif order_type == 'Move':
            if to_loc_name is None:
                LOGGER.error('[Move] Received invalid to loc "%s" for map "%s".', to_terr_id, self.map_name)
                LOGGER.error(order)
                return

            # We don't need to set the "convoyPath" property if we are converting from an order_dict
            via_flag = ' VIA' if via_convoy == 'Yes' else ''
            self.order_str = '%s %s - %s%s' % (unit_type[0], loc_name, to_loc_name, via_flag)
            self.order_dict = {'terrID': terr_id,
                               'unitType': unit_type,
                               'type': 'Move',
                               'toTerrID': to_terr_id,
                               'fromTerrID': '',
                               'viaConvoy': via_convoy}

        # --- Support hold ---
        # {"id": "73", "unitID": "16", "type": "Support hold", "toTerrID": "24", "fromTerrID": "", "viaConvoy": ""}
        elif order_type == 'Support hold':
            if to_loc_name is None:
                LOGGER.error('[Support H] Received invalid to loc "%s" for map "%s".', to_terr_id, self.map_name)
                LOGGER.error(order)
                return
            self.order_str = '%s %s S %s' % (unit_type[0], loc_name, to_loc_name)
            self.order_dict = {'terrID': terr_id,
                               'unitType': unit_type,
                               'type': 'Support hold',
                               'toTerrID': to_terr_id,
                               'fromTerrID': '',
                               'viaConvoy': ''}

        # --- Support move ---
        # {"id": "73", "unitID": "16", "type": "Support move", "toTerrID": "24", "fromTerrID": "69", "viaConvoy": ""}
        elif order_type == 'Support move':
            if to_loc_name is None:
                LOGGER.error('[Support M] Received invalid to loc "%s" for map "%s".', to_terr_id, self.map_name)
                LOGGER.error(order)
                return
            if from_loc_name is None:
                LOGGER.error('[Support M] Received invalid from loc "%s" for map "%s".', from_terr_id, self.map_name)
                LOGGER.error(order)
                return
            self.order_str = '%s %s S %s - %s' % (unit_type[0], loc_name, from_loc_name, to_loc_name)
            self.order_dict = {'terrID': terr_id,
                               'unitType': unit_type,
                               'type': 'Support move',
                               'toTerrID': to_terr_id,
                               'fromTerrID': from_terr_id,
                               'viaConvoy': ''}

        # --- Convoy ---
        # {"id": "79", "unitID": "22", "type": "Convoy", "toTerrID": "24", "fromTerrID": "20", "viaConvoy": "",
        # "convoyPath": ["20", "69"]}
        elif order_type == 'Convoy':
            if to_loc_name is None:
                LOGGER.error('[Convoy] Received invalid to loc "%s" for map "%s".', to_terr_id, self.map_name)
                LOGGER.error(order)
                return
            if from_loc_name is None:
                LOGGER.error('[Convoy] Received invalid from loc "%s" for map "%s".', from_terr_id, self.map_name)
                LOGGER.error(order)
                return

            # We don't need to set the "convoyPath" property if we are converting from an order_dict
            self.order_str = '%s %s C A %s - %s' % (unit_type[0], loc_name, from_loc_name, to_loc_name)
            self.order_dict = {'terrID': terr_id,
                               'unitType': unit_type,
                               'type': 'Convoy',
                               'toTerrID': to_terr_id,
                               'fromTerrID': from_terr_id,
                               'viaConvoy': ''}

        # --- Retreat ---
        # {"id": "152", "unitID": "18", "type": "Retreat", "toTerrID": "75", "fromTerrID": "", "viaConvoy": ""}
        elif order_type == 'Retreat':
            if to_loc_name is None:
                return
            self.order_str = '%s %s R %s' % (unit_type[0], loc_name, to_loc_name)
            self.order_dict = {'terrID': terr_id,
                               'unitType': unit_type,
                               'type': 'Retreat',
                               'toTerrID': to_terr_id,
                               'fromTerrID': '',
                               'viaConvoy': ''}

        # --- Disband ---
        # {"id": "152", "unitID": "18", "type": "Disband", "toTerrID": "", "fromTerrID": "", "viaConvoy": ""}
        elif order_type == 'Disband':
            self.order_str = '%s %s D' % (unit_type[0], loc_name)
            self.order_dict = {'terrID': terr_id,
                               'unitType': unit_type,
                               'type': 'Disband',
                               'toTerrID': '',
                               'fromTerrID': '',
                               'viaConvoy': ''}

        # --- Build Army ---
        # [{"id": "56", "unitID": null, "type": "Build Army", "toTerrID": "37", "fromTerrID": "", "viaConvoy": ""}]
        elif order_type == 'Build Army':
            self.order_str = 'A %s B' % loc_name
            self.order_dict = {'terrID': terr_id,
                               'unitType': 'Army',
                               'type': 'Build Army',
                               'toTerrID': terr_id,
                               'fromTerrID': '',
                               'viaConvoy': ''}

        # --- Build Fleet ---
        # [{"id": "56", "unitID": null, "type": "Build Fleet", "toTerrID": "37", "fromTerrID": "", "viaConvoy": ""}]
        elif order_type == 'Build Fleet':
            self.order_str = 'F %s B' % loc_name
            self.order_dict = {'terrID': terr_id,
                               'unitType': 'Fleet',
                               'type': 'Build Fleet',
                               'toTerrID': terr_id,
                               'fromTerrID': '',
                               'viaConvoy': ''}

        # --- Wait / Waive ---
        # [{"id": "56", "unitID": null, "type": "Wait", "toTerrID": "", "fromTerrID": "", "viaConvoy": ""}]
        elif order_type == 'Wait':
            self.order_str = 'WAIVE'
            self.order_dict = {'terrID': None,
                               'unitType': '',
                               'type': 'Wait',
                               'toTerrID': '',
                               'fromTerrID': '',
                               'viaConvoy': ''}

        # Disband (A phase)
        # {"id": "152", "unitID": null, "type": "Destroy", "toTerrID": "18", "fromTerrID": "", "viaConvoy": ""}
        elif order_type == 'Destroy':
            self.order_str = '%s %s D' % (unit_type[0], loc_name)
            self.order_dict = {'terrID': terr_id,
                               'unitType': unit_type,
                               'type': 'Destroy',
                               'toTerrID': terr_id,
                               'fromTerrID': '',
                               'viaConvoy': ''}

    def __bool__(self):
        """ Returns True if an order was parsed, False otherwise """
        return bool(self.order_str != '')

    def __str__(self):
        """ Returns the string representation of the order """
        return self.order_str

    def to_string(self):
        """ Returns the string representation of the order """
        return self.order_str

    def to_norm_string(self):
        """ Returns a normalized order string """
        if self.order_str[-2:] == ' D':
            order_str = '? ' + self.order_str[2:]
        else:
            order_str = self.order_str
        return order_str\
            .replace(' S A ', ' S ')\
            .replace(' S F ', ' S ') \
            .replace(' C A ', ' C ') \
            .replace(' C F ', ' C ') \
            .replace(' VIA', '')

    def to_dict(self):
        """ Returns the dictionary representation of the order """
        return self.order_dict