import logging

from game import db
from game.settings import Settings
from .conflictgen import *
from .naming import *
from .triggergen import TRIGGER_WAYPOINT_OFFSET

from dcs.mission import *
from dcs.unitgroup import *
from dcs.unittype import *
from dcs.task import *
from dcs.terrain.terrain import NoParkingSlotError

SPREAD_DISTANCE_FACTOR = 1, 2
ESCORT_ENGAGEMENT_MAX_DIST = 100000
WORKAROUND_WAYP_DIST = 1000

WARM_START_HELI_AIRSPEED = 120
WARM_START_HELI_ALT = 500

WARM_START_ALTITUDE = 3000
WARM_START_AIRSPEED = 550

INTERCEPTION_AIRSPEED = 1000
BARCAP_RACETRACK_DISTANCE = 20000

ATTACK_CIRCLE_ALT = 1000
ATTACK_CIRCLE_DURATION = 15

CAS_ALTITUDE = 800
RTB_ALTITUDE = 800
RTB_DISTANCE = 5000
HELI_ALT = 500

TRANSPORT_LANDING_ALT = 2000

DEFENCE_ENGAGEMENT_MAX_DISTANCE = 60000
INTERCEPT_MAX_DISTANCE = 200000

GROUP_VERTICAL_OFFSET = 300


class AircraftConflictGenerator:
    escort_targets = [] # type: typing.List[typing.Tuple[FlyingGroup, int]]
    vertical_offset = None  # type: int

    def __init__(self, mission: Mission, conflict: Conflict, settings: Settings):
        self.m = mission
        self.settings = settings
        self.conflict = conflict
        self.vertical_offset = 0
        self.escort_targets = []

    def _start_type(self) -> StartType:
        return self.settings.cold_start and StartType.Cold or StartType.Warm

    def _group_point(self, point) -> Point:
        distance = randint(
                int(self.conflict.size * SPREAD_DISTANCE_FACTOR[0]),
                int(self.conflict.size * SPREAD_DISTANCE_FACTOR[1]),
                )
        return point.random_point_within(distance, self.conflict.size * SPREAD_DISTANCE_FACTOR[0])

    def _split_to_groups(self, dict: db.PlaneDict, clients: db.PlaneDict = None) -> typing.Collection[typing.Tuple[FlyingType, int, int]]:
        for flying_type, count in dict.items():
            if clients:
                client_count = clients.get(flying_type, 0)
            else:
                client_count = 0

            if flying_type == F_14B:
                # workaround since 2 and 3 tomcat collide on carrier
                group_size = 2
            else:
                group_size = 4

            while count > 0:
                group_size = min(count, group_size)
                client_size = max(min(client_count, group_size), 0)

                yield (flying_type, group_size, client_size)
                count -= group_size
                client_count -= client_size

    def _setup_group(self, group: FlyingGroup, for_task: typing.Type[Task], client_count: int):
        did_load_loadout = False
        unit_type = group.units[0].unit_type
        if unit_type in db.PLANE_PAYLOAD_OVERRIDES:
            override_loadout = db.PLANE_PAYLOAD_OVERRIDES[unit_type]
            if type(override_loadout) == dict:
                if for_task in db.PLANE_PAYLOAD_OVERRIDES[unit_type]:
                    payload_name = db.PLANE_PAYLOAD_OVERRIDES[unit_type][for_task]
                    group.load_loadout(payload_name)
                    did_load_loadout = True
                    logging.info("Loaded overridden payload for {} - {} for task {}".format(unit_type, payload_name, for_task))
                elif "*" in db.PLANE_PAYLOAD_OVERRIDES[unit_type]:
                    payload_name = db.PLANE_PAYLOAD_OVERRIDES[unit_type]["*"]
                    group.load_loadout(payload_name)
                    did_load_loadout = True
                    logging.info("Loaded overridden payload for {} - {} for task {}".format(unit_type, payload_name, for_task))
            elif issubclass(override_loadout, MainTask):
                group.load_task_default_loadout(override_loadout)
                did_load_loadout = True

        if not did_load_loadout:
            group.load_task_default_loadout(for_task)

        if unit_type in db.PLANE_LIVERY_OVERRIDES:
            for unit_instance in group.units:
                unit_instance.livery_id = db.PLANE_LIVERY_OVERRIDES[unit_type]

        single_client = client_count == 1
        for idx in range(0, min(len(group.units), client_count)):
            if single_client:
                group.units[idx].set_player()
            else:
                group.units[idx].set_client()

        group.points[0].tasks.append(OptReactOnThreat(OptReactOnThreat.Values.EvadeFire))

        if unit_type in helicopters.helicopter_map.values():
            print(unit_type)
            group.set_frequency(127.5)
        else:
            group.set_frequency(251.0)

    def _generate_at_airport(self, name: str, side: Country, unit_type: FlyingType, count: int, client_count: int, airport: Airport = None) -> FlyingGroup:
        assert count > 0
        assert unit is not None

        logging.info("airgen: {} for {} at {}".format(unit_type, side.id, airport))
        return self.m.flight_group_from_airport(
            country=side,
            name=name,
            aircraft_type=unit_type,
            airport=self.m.terrain.airport_by_id(airport.id),
            maintask=None,
            start_type=self._start_type(),
            group_size=count,
            parking_slots=None)

    def _generate_inflight(self, name: str, side: Country, unit_type: FlyingType, count: int, client_count: int, at: Point) -> FlyingGroup:
        assert count > 0
        assert unit is not None

        self.vertical_offset += GROUP_VERTICAL_OFFSET
        if unit_type in helicopters.helicopter_map.values():
            alt = WARM_START_HELI_ALT + self.vertical_offset
            speed = WARM_START_HELI_AIRSPEED
        else:
            alt = WARM_START_ALTITUDE + self.vertical_offset
            speed = WARM_START_AIRSPEED

        pos = Point(at.x + random.randint(100, 1000), at.y + random.randint(100, 1000))

        logging.info("airgen: {} for {} at {} at {}".format(unit_type, side.id, alt, speed))
        group = self.m.flight_group(
            country=side,
            name=name,
            aircraft_type=unit_type,
            airport=None,
            position=pos,
            altitude=alt,
            speed=speed,
            maintask=None,
            start_type=self._start_type(),
            group_size=count)

        group.points[0].alt_type = "RADIO"
        return group

    def _generate_at_group(self, name: str, side: Country, unit_type: FlyingType, count: int, client_count: int, at: typing.Union[ShipGroup, StaticGroup]) -> FlyingGroup:
        assert count > 0
        assert unit is not None

        logging.info("airgen: {} for {} at unit {}".format(unit_type, side.id, at))
        return self.m.flight_group_from_unit(
            country=side,
            name=name,
            aircraft_type=unit_type,
            pad_group=at,
            maintask=None,
            start_type=self._start_type(),
            group_size=count)

    def _generate_group(self, name: str, side: Country, unit_type: FlyingType, count: int, client_count: int, at: db.StartingPosition):
        if isinstance(at, Point):
            return self._generate_inflight(name, side, unit_type, count, client_count, at)
        elif isinstance(at, Group):
            takeoff_ban = unit_type in db.CARRIER_TAKEOFF_BAN
            ai_ban = client_count == 0 and self.settings.only_player_takeoff

            if not takeoff_ban and not ai_ban:
                return self._generate_at_group(name, side, unit_type, count, client_count, at)
            else:
                return self._generate_inflight(name, side, unit_type, count, client_count, at.position)
        elif issubclass(at, Airport):
            takeoff_ban = unit_type in db.TAKEOFF_BAN
            ai_ban = client_count == 0 and self.settings.only_player_takeoff

            if not takeoff_ban and not ai_ban:
                try:
                    return self._generate_at_airport(name, side, unit_type, count, client_count, at)
                except NoParkingSlotError:
                    pass
            return self._generate_inflight(name, side, unit_type, count, client_count, at.position)
        else:
            assert False

    def _add_radio_waypoint(self, group: FlyingGroup, position, altitude: int, airspeed: int = 600):
        point = group.add_waypoint(position, altitude, airspeed)
        point.alt_type = "RADIO"
        return point

    def _rtb_for(self, group: FlyingGroup, cp: ControlPoint, at: db.StartingPosition = None):
        if not at:
            at = cp.at
        position = at if isinstance(at, Point) else at.position

        last_waypoint = group.points[-1]
        if last_waypoint is not None:
            heading = position.heading_between_point(last_waypoint.position)
            tod_location = position.point_from_heading(heading, RTB_DISTANCE)
            self._add_radio_waypoint(group, tod_location, last_waypoint.alt)

        destination_waypoint = self._add_radio_waypoint(group, position, RTB_ALTITUDE)
        if isinstance(at, Airport):
            group.land_at(at)
        return destination_waypoint

    def _at_position(self, at) -> Point:
        if isinstance(at, Point):
            return at
        elif isinstance(at, ShipGroup):
            return at.position
        elif issubclass(at, Airport):
            return at.position
        else:
            assert False

    def _generate_escort(self, side: Country, units: db.PlaneDict, clients: db.PlaneDict, at: db.StartingPosition, is_quick=False, should_orbit=False):
        groups = []
        for flying_type, count, client_count in self._split_to_groups(units, clients):
            group = self._generate_group(
                name=namegen.next_unit_name(side, flying_type),
                side=side,
                unit_type=flying_type,
                count=count,
                client_count=client_count,
                at=at)

            group.task = Escort.name
            self._setup_group(group, CAP, client_count)

            for escorted_group, waypoint_index in self.escort_targets:
                waypoint_index += 1
                if not is_quick:
                    waypoint_index += TRIGGER_WAYPOINT_OFFSET

                group.points[0].tasks.append(EscortTaskAction(escorted_group.id, engagement_max_dist=ESCORT_ENGAGEMENT_MAX_DIST, lastwpt=waypoint_index))

            if should_orbit:
                orbit_task = ControlledTask(OrbitAction(ATTACK_CIRCLE_ALT, pattern=OrbitAction.OrbitPattern.Circle))
                orbit_task.stop_after_duration(ATTACK_CIRCLE_DURATION * 60)

                orbit_waypoint = self._add_radio_waypoint(group, self.conflict.position, CAS_ALTITUDE)
                orbit_waypoint.tasks.append(orbit_task)
                orbit_waypoint.tasks.append(EngageTargets(max_distance=DEFENCE_ENGAGEMENT_MAX_DISTANCE))

            groups.append(group)
        return groups

    def generate_cas_strikegroup(self, attackers: db.PlaneDict, clients: db.PlaneDict, at: db.StartingPosition = None, escort=True):
        assert not escort or len(self.escort_targets) == 0

        for flying_type, count, client_count in self._split_to_groups(attackers, clients):
            group = self._generate_group(
                    name=namegen.next_unit_name(self.conflict.attackers_country, flying_type),
                    side=self.conflict.attackers_country,
                    unit_type=flying_type,
                    count=count,
                    client_count=client_count,
                    at=at and at or self._group_point(self.conflict.air_attackers_location))

            waypoint = self._add_radio_waypoint(group, self.conflict.position, CAS_ALTITUDE, WARM_START_AIRSPEED)
            if self.conflict.is_vector:
                self._add_radio_waypoint(group, self.conflict.tail, CAS_ALTITUDE, WARM_START_AIRSPEED)

            group.task = CAS.name
            self._setup_group(group, CAS, client_count)
            if escort:
                self.escort_targets.append((group, group.points.index(waypoint)))
            self._rtb_for(group, self.conflict.from_cp, at)

    def generate_ground_attack_strikegroup(self, strikegroup: db.PlaneDict, clients: db.PlaneDict, targets: typing.List[typing.Tuple[str, Point]], at: db.StartingPosition = None, escort=True):
        assert not escort or len(self.escort_targets) == 0

        for flying_type, count, client_count in self._split_to_groups(strikegroup, clients):
            group = self._generate_group(
                name=namegen.next_unit_name(self.conflict.attackers_country, flying_type),
                side=self.conflict.attackers_country,
                unit_type=flying_type,
                count=count,
                client_count=client_count,
                at=at and at or self._group_point(self.conflict.air_attackers_location))

            escort_until_waypoint = None

            for name, pos in targets:
                waypoint = group.add_waypoint(pos, 0, WARM_START_AIRSPEED, self.m.translation.create_string(name))
                waypoint.tasks.append(Bombing(pos, attack_qty=2))
                if escort_until_waypoint is None:
                    escort_until_waypoint = waypoint

            group.task = GroundAttack.name
            self._setup_group(group, GroundAttack, client_count)
            if escort:
                self.escort_targets.append((group, group.points.index(escort_until_waypoint)))
            self._rtb_for(group, self.conflict.from_cp, at)

    def generate_sead_strikegroup(self, strikegroup: db.PlaneDict, clients: db.PlaneDict, targets: typing.List[typing.Tuple[str, Point]], at: db.StartingPosition, escort=True):
        assert not escort or len(self.escort_targets) == 0

        for flying_type, count, client_count in self._split_to_groups(strikegroup, clients):
            group = self._generate_group(
                name=namegen.next_unit_name(self.conflict.attackers_country, flying_type),
                side=self.conflict.attackers_country,
                unit_type=flying_type,
                count=count,
                client_count=client_count,
                at=at and at or self._group_point(self.conflict.air_attackers_location))

            escort_until_waypoint = None

            for name, pos in targets:
                waypoint = group.add_waypoint(pos, 0, WARM_START_AIRSPEED, self.m.translation.create_string(name))
                if escort_until_waypoint is None:
                    escort_until_waypoint = waypoint

            group.task = SEAD.name
            self._setup_group(group, SEAD, client_count)
            if escort:
                self.escort_targets.append((group, group.points.index(escort_until_waypoint)))

            self._rtb_for(group, self.conflict.from_cp, at)

    def generate_defenders_cas(self, defenders: db.PlaneDict, clients: db.PlaneDict, at: db.StartingPosition = None, escort=True):
        assert not escort or len(self.escort_targets) == 0

        for flying_type, count, client_count in self._split_to_groups(defenders, clients):
            group = self._generate_group(
                name=namegen.next_unit_name(self.conflict.attackers_country, flying_type),
                side=self.conflict.defenders_country,
                unit_type=flying_type,
                count=count,
                client_count=client_count,
                at=at and at or self._group_point(self.conflict.air_defenders_location))

            location = self._group_point(self.conflict.air_defenders_location)
            insertion_point = self.conflict.find_insertion_point(location)
            waypoint = self._add_radio_waypoint(group, insertion_point, CAS_ALTITUDE, WARM_START_AIRSPEED)

            if self.conflict.is_vector:
                destination_tail = self.conflict.tail.distance_to_point(insertion_point) > self.conflict.position.distance_to_point(insertion_point)
                self._add_radio_waypoint(group, destination_tail and self.conflict.tail or self.conflict.position, CAS_ALTITUDE, WARM_START_AIRSPEED)

            group.task = CAS.name
            self._setup_group(group, CAS, client_count)
            if escort:
                self.escort_targets.append((group, group.points.index(waypoint)))
            self._rtb_for(group, self.conflict.to_cp, at)

    def generate_ship_strikegroup(self, attackers: db.PlaneDict, clients: db.PlaneDict, target_groups: typing.Collection[ShipGroup], at: db.StartingPosition = None, escort=True):
        assert not escort or len(self.escort_targets) == 0

        for flying_type, count, client_count in self._split_to_groups(attackers, clients):
            group = self._generate_group(
                name=namegen.next_unit_name(self.conflict.attackers_country, flying_type),
                side=self.conflict.attackers_country,
                unit_type=flying_type,
                count=count,
                client_count=client_count,
                at=at and at or self._group_point(self.conflict.air_attackers_location))

            wayp = self._add_radio_waypoint(group, self.conflict.position, CAS_ALTITUDE, WARM_START_AIRSPEED)
            for target_group in target_groups:
                wayp.tasks.append(AttackGroup(target_group.id))

            group.task = AntishipStrike.name
            self._setup_group(group, AntishipStrike, client_count)
            if escort:
                self.escort_targets.append((group, group.points.index(wayp)))
            self._rtb_for(group, self.conflict.from_cp, at)

    def generate_attackers_escort(self, attackers: db.PlaneDict, clients: db.PlaneDict, at: db.StartingPosition = None):
        for g in self._generate_escort(
                side=self.conflict.attackers_country,
                units=attackers,
                clients=clients,
                at=at and at or self._group_point(self.conflict.air_attackers_location),
                is_quick=at is None,
                should_orbit=True):
            self._rtb_for(g, self.conflict.from_cp, at)

    def generate_defenders_escort(self, escort: db.PlaneDict, clients: db.PlaneDict, at: db.StartingPosition = None):
        for g in self._generate_escort(
                side=self.conflict.defenders_country,
                units=escort,
                clients=clients,
                at=at and at or self._group_point(self.conflict.air_defenders_location),
                is_quick=at is None,
                should_orbit=False):
            self._rtb_for(g, self.conflict.to_cp, at)

    def generate_defense(self, defenders: db.PlaneDict, clients: db.PlaneDict, at: db.StartingPosition = None):
        for flying_type, count, client_count in self._split_to_groups(defenders, clients):
            group = self._generate_group(
                name=namegen.next_unit_name(self.conflict.attackers_country, flying_type),
                side=self.conflict.defenders_country,
                unit_type=flying_type,
                count=count,
                client_count=client_count,
                at=at and at or self._group_point(self.conflict.air_defenders_location))

            group.task = CAP.name
            wayp = self._add_radio_waypoint(group, self.conflict.position, CAS_ALTITUDE, WARM_START_AIRSPEED)
            wayp.tasks.append(dcs.task.EngageTargets(max_distance=DEFENCE_ENGAGEMENT_MAX_DISTANCE))
            wayp.tasks.append(dcs.task.OrbitAction(ATTACK_CIRCLE_ALT, pattern=OrbitAction.OrbitPattern.Circle))
            self._setup_group(group, CAP, client_count)
            self._rtb_for(group, self.conflict.to_cp, at)

    def generate_migcap(self, patrol: db.PlaneDict, clients: db.PlaneDict, at: db.StartingPosition = None):
        for flying_type, count, client_count in self._split_to_groups(patrol, clients):
            group = self._generate_group(
                name=namegen.next_unit_name(self.conflict.attackers_country, flying_type),
                side=self.conflict.attackers_country,
                unit_type=flying_type,
                count=count,
                client_count=client_count,
                at=at and at or self._group_point(self.conflict.air_attackers_location))

            waypoint = self._add_radio_waypoint(group, self.conflict.position, WARM_START_ALTITUDE, WARM_START_AIRSPEED)
            if self.conflict.is_vector:
                self._add_radio_waypoint(group, self.conflict.tail, WARM_START_ALTITUDE, WARM_START_AIRSPEED)

            group.task = CAP.name
            self._setup_group(group, CAP, client_count)
            self._rtb_for(group, self.conflict.from_cp, at)

    def generate_barcap(self, patrol: db.PlaneDict, clients: db.PlaneDict, at: db.StartingPosition = None):
        for flying_type, count, client_count in self._split_to_groups(patrol, clients):
            group = self._generate_group(
                name=namegen.next_unit_name(self.conflict.attackers_country, flying_type),
                side=self.conflict.defenders_country,
                unit_type=flying_type,
                count=count,
                client_count=client_count,
                at=at and at or self._group_point(self.conflict.air_defenders_location))

            waypoint = self._add_radio_waypoint(group, self.conflict.position, WARM_START_ALTITUDE, WARM_START_AIRSPEED)
            if self.conflict.is_vector:
                self._add_radio_waypoint(group, self.conflict.tail, WARM_START_ALTITUDE, WARM_START_AIRSPEED)
            else:
                heading = group.position.heading_between_point(self.conflict.position)
                waypoint = self._add_radio_waypoint(group, self.conflict.position.point_from_heading(heading, BARCAP_RACETRACK_DISTANCE),
                                                    WARM_START_ALTITUDE,
                                                    WARM_START_AIRSPEED)
                waypoint.tasks.append(OrbitAction(WARM_START_ALTITUDE, WARM_START_AIRSPEED))

            group.task = CAP.name
            self._setup_group(group, CAP, client_count)
            self._rtb_for(group, self.conflict.to_cp, at)

    def generate_transport(self, transport: db.PlaneDict, destination: Airport, escort=True):
        assert not escort or len(self.escort_targets) == 0

        for flying_type, count, client_count in self._split_to_groups(transport):
            group = self._generate_group(
                name=namegen.next_unit_name(self.conflict.attackers_country, flying_type),
                side=self.conflict.defenders_country,
                unit_type=flying_type,
                count=count,
                client_count=client_count,
                at=self._group_point(self.conflict.air_defenders_location))

            waypoint = self._rtb_for(group, self.conflict.to_cp)
            if escort:
                self.escort_targets.append((group, group.points.index(waypoint)))

            self._add_radio_waypoint(group, destination.position, RTB_ALTITUDE)
            group.task = Transport.name
            group.land_at(destination)

    def generate_interception(self, interceptors: db.PlaneDict, clients: db.PlaneDict, at: db.StartingPosition = None):
        for flying_type, count, client_count in self._split_to_groups(interceptors, clients):
            group = self._generate_group(
                name=namegen.next_unit_name(self.conflict.attackers_country, flying_type),
                side=self.conflict.attackers_country,
                unit_type=flying_type,
                count=count,
                client_count=client_count,
                at=at and at or self._group_point(self.conflict.air_attackers_location))

            group.task = CAP.name
            group.points[0].tasks.append(EngageTargets(max_distance=INTERCEPT_MAX_DISTANCE))

            wayp = self._add_radio_waypoint(group, self.conflict.position, WARM_START_ALTITUDE, INTERCEPTION_AIRSPEED)
            wayp.tasks.append(EngageTargets(max_distance=INTERCEPT_MAX_DISTANCE))
            
            if self.conflict.is_vector:
                self._add_radio_waypoint(group, self.conflict.tail, CAS_ALTITUDE, WARM_START_ALTITUDE)

            self._setup_group(group, CAP, client_count)
            self._rtb_for(group, self.conflict.from_cp, at)

    def generate_passenger_transport(self, helis: db.HeliDict, clients: db.HeliDict, at: db.StartingPosition):
        for heli_type, count, client_count in self._split_to_groups(helis, clients):
            group = self._generate_group(
                name=namegen.next_unit_name(self.conflict.attackers_country, heli_type),
                side=self.conflict.attackers_country,
                unit_type=heli_type,
                count=count,
                client_count=client_count,
                at=at and at or self._group_point(self.conflict.air_attackers_location)
            )

            self._add_radio_waypoint(group, self.conflict.position, HELI_ALT)
            self._setup_group(group, Transport, client_count)
