from dcs.task import *

from gen import namegen
from gen.ground_forces.ai_ground_planner import CombatGroupRole, DISTANCE_FROM_FRONTLINE
from .conflictgen import *

SPREAD_DISTANCE_FACTOR = 0.1, 0.3
SPREAD_DISTANCE_SIZE_FACTOR = 0.1

FRONTLINE_CAS_FIGHTS_COUNT = 16, 24
FRONTLINE_CAS_GROUP_MIN = 1, 2
FRONTLINE_CAS_PADDING = 12000

RETREAT_DISTANCE = 20000
BREAKTHROUGH_OFFENSIVE_DISTANCE = 35000
AGGRESIVE_MOVE_DISTANCE = 16000

FIGHT_DISTANCE = 3500

class GroundConflictGenerator:

    def __init__(self, mission: Mission, conflict: Conflict, game, player_planned_combat_groups, enemy_planned_combat_groups, player_stance):
        self.m = mission
        self.conflict = conflict
        self.enemy_planned_combat_groups = enemy_planned_combat_groups
        self.player_planned_combat_groups = player_planned_combat_groups
        self.player_stance = CombatStance(player_stance)
        self.enemy_stance = random.choice([CombatStance.AGGRESIVE, CombatStance.AGGRESIVE, CombatStance.AGGRESIVE, CombatStance.ELIMINATION, CombatStance.BREAKTHROUGH]) if len(enemy_planned_combat_groups) > len(player_planned_combat_groups) else random.choice([CombatStance.DEFENSIVE, CombatStance.DEFENSIVE, CombatStance.DEFENSIVE, CombatStance.AMBUSH, CombatStance.AGGRESIVE])
        self.game = game

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

    def generate(self):

        player_groups = []
        enemy_groups = []

        combat_width = self.conflict.distance/2
        if combat_width > 500000:
            combat_width = 500000
        if combat_width < 35000:
            combat_width = 35000

        position = Conflict.frontline_position(self.game.theater, self.conflict.from_cp, self.conflict.to_cp)

        # Create player groups at random position
        for group in self.player_planned_combat_groups:
            if group.role == CombatGroupRole.ARTILLERY:
                distance_from_frontline = self.get_artilery_group_distance_from_frontline(group)
            else:
                distance_from_frontline = DISTANCE_FROM_FRONTLINE[group.role]
            final_position = self.get_valid_position_for_group(position, True, combat_width, distance_from_frontline)

            if final_position is not None:
                g = self._generate_group(
                    side=self.m.country(self.game.player_country),
                    unit=group.units[0],
                    heading=self.conflict.heading+90,
                    count=len(group.units),
                    at=final_position)
                g.set_skill(self.game.settings.player_skill)
                player_groups.append((g,group))

                self.gen_infantry_group_for_group(g, True, self.m.country(self.game.player_country), self.conflict.heading + 90)

        # Create enemy groups at random position
        for group in self.enemy_planned_combat_groups:
            if group.role == CombatGroupRole.ARTILLERY:
                distance_from_frontline = self.get_artilery_group_distance_from_frontline(group)
            else:
                distance_from_frontline = DISTANCE_FROM_FRONTLINE[group.role]
            final_position = self.get_valid_position_for_group(position, False, combat_width, distance_from_frontline)

            if final_position is not None:
                g = self._generate_group(
                    side=self.m.country(self.game.enemy_country),
                    unit=group.units[0],
                    heading=self.conflict.heading - 90,
                    count=len(group.units),
                    at=final_position)
                g.set_skill(self.game.settings.enemy_vehicle_skill)
                enemy_groups.append((g, group))

                self.gen_infantry_group_for_group(g, False, self.m.country(self.game.enemy_country), self.conflict.heading - 90)


        # Plan combat actions for groups
        self.plan_action_for_groups(self.player_stance, player_groups, enemy_groups, self.conflict.heading + 90, self.conflict.from_cp, self.conflict.to_cp)
        self.plan_action_for_groups(self.enemy_stance, enemy_groups, player_groups, self.conflict.heading - 90, self.conflict.to_cp, self.conflict.from_cp)



    def gen_infantry_group_for_group(self, group, is_player, side:Country, forward_heading):

        # Disable infantry unit gen if disabled
        if not self.game.settings.perf_infantry:
            return

        infantry_position = group.points[0].position.random_point_within(250, 50)

        if side == self.conflict.attackers_country:
            cp = self.conflict.from_cp
        else:
            cp = self.conflict.to_cp

        if is_player:
            faction = self.game.player_name
        else:
            faction = self.game.enemy_name

        possible_infantry_units = db.find_infantry(faction)
        if len(possible_infantry_units) == 0:
            return

        u = random.choice(possible_infantry_units)
        self.m.vehicle_group(
                side,
                namegen.next_infantry_name(side, cp, u), u,
                position=infantry_position,
                group_size=1,
                heading=forward_heading,
                move_formation=PointAction.OffRoad)

        for i in range(randint(3, 10)):
            u = random.choice(possible_infantry_units)
            position = infantry_position.random_point_within(55, 5)
            self.m.vehicle_group(
                side,
                namegen.next_infantry_name(side, cp, u), u,
                position=position,
                group_size=1,
                heading=forward_heading,
                move_formation=PointAction.OffRoad)


    def plan_action_for_groups(self, stance, ally_groups, enemy_groups, forward_heading, from_cp, to_cp):

        if not self.game.settings.perf_moving_units:
            return

        for dcs_group, group in ally_groups:
            if group.role == CombatGroupRole.ARTILLERY:
                # Fire on any ennemy in range
                if self.game.settings.perf_artillery:
                    target = self.get_artillery_target_in_range(dcs_group, group, enemy_groups)
                    if target is not None:
                        dcs_group.points[0].tasks.append(FireAtPoint(target, len(group.units) * 10, 100))
            elif group.role in [CombatGroupRole.TANK, CombatGroupRole.IFV]:
                if stance == CombatStance.AGGRESIVE:
                    # Attack nearest enemy if any
                    # Then move forward OR Attack enemy base if it is not too far away
                    target = self.find_nearest_enemy_group(dcs_group, enemy_groups)
                    if target is not None:
                        rand_offset = Point(random.randint(-50, 50), random.randint(-50, 50))
                        dcs_group.add_waypoint(target.points[0].position + rand_offset, PointAction.OffRoad)
                        dcs_group.points[1].tasks.append(AttackGroup(target.id))

                    if to_cp.position.distance_to_point(dcs_group.points[0].position) <= AGGRESIVE_MOVE_DISTANCE:
                        attack_point = to_cp.position.random_point_within(500, 0)
                    else:
                        attack_point = self.find_offensive_point(dcs_group, forward_heading, AGGRESIVE_MOVE_DISTANCE)
                    dcs_group.add_waypoint(attack_point, PointAction.OnRoad)
                elif stance == CombatStance.BREAKTHROUGH:
                    # In breakthrough mode, the units will move forward
                    # If the enemy base is close enough, the units will attack the base
                    if to_cp.position.distance_to_point(
                            dcs_group.points[0].position) <= BREAKTHROUGH_OFFENSIVE_DISTANCE:
                        attack_point = to_cp.position.random_point_within(500, 0)
                    else:
                        attack_point = self.find_offensive_point(dcs_group, forward_heading, BREAKTHROUGH_OFFENSIVE_DISTANCE)
                    dcs_group.add_waypoint(attack_point, PointAction.OnRoad)
                elif stance == CombatStance.ELIMINATION:
                    # In elimination mode, the units focus on destroying as much enemy groups as possible
                    targets = self.find_n_nearest_enemy_groups(dcs_group, enemy_groups, 3)
                    i = 1
                    for target in targets:
                        rand_offset = Point(random.randint(-50, 50), random.randint(-50, 50))
                        dcs_group.add_waypoint(target.points[0].position+rand_offset,PointAction.OffRoad)
                        dcs_group.points[i].tasks.append(AttackGroup(target.id))
                        i = i + 1
                    if to_cp.position.distance_to_point(dcs_group.points[0].position) <= AGGRESIVE_MOVE_DISTANCE:
                        attack_point = to_cp.position.random_point_within(500, 0)
                        dcs_group.add_waypoint(attack_point)
            elif group.role in [CombatGroupRole.APC, CombatGroupRole.ATGM]:

                if stance in [CombatStance.AGGRESIVE, CombatStance.BREAKTHROUGH, CombatStance.ELIMINATION]:
                    # APC & ATGM will never move too much forward, but will follow along any offensive
                    if to_cp.position.distance_to_point(dcs_group.points[0].position) <= AGGRESIVE_MOVE_DISTANCE:
                        attack_point = to_cp.position.random_point_within(500, 0)
                    else:
                        attack_point = self.find_offensive_point(dcs_group, forward_heading, AGGRESIVE_MOVE_DISTANCE)
                    dcs_group.add_waypoint(attack_point, PointAction.OnRoad)

            if stance == CombatStance.RETREAT:
                # In retreat mode, the units will fall back
                # If the ally base is close enough, the units will even regroup there
                if from_cp.position.distance_to_point(dcs_group.points[0].position) <= RETREAT_DISTANCE:
                    retreat_point = from_cp.position.random_point_within(500, 250)
                else:
                    retreat_point = self.find_retreat_point(dcs_group, forward_heading)
                reposition_point = retreat_point.point_from_heading(forward_heading, 10)  # Another point to make the unit face the enemy
                dcs_group.add_waypoint(retreat_point, PointAction.OnRoad)
                dcs_group.add_waypoint(reposition_point, PointAction.OffRoad)


    def find_retreat_point(self, dcs_group, frontline_heading):
        """
        Find a point to retreat to
        :param dcs_group: DCS mission group we are searching a retreat point for
        :param frontline_heading: Heading of the frontline
        :return: dcs.mapping.Point object with the desired position
        """
        return dcs_group.points[0].position.point_from_heading(frontline_heading-180, RETREAT_DISTANCE)

    def find_offensive_point(self, dcs_group, frontline_heading, distance):
        """
        Find a point to attack
        :param dcs_group:  DCS mission group we are searching an attack point for
        :param frontline_heading: Heading of the frontline
        :param distance: Distance of the offensive (how far unit should move)
        :return: dcs.mapping.Point object with the desired position
        """
        return dcs_group.points[0].position.point_from_heading(frontline_heading, distance)

    def find_n_nearest_enemy_groups(self, player_group, enemy_groups, n):
        """
        Return the neaarest enemy group for the player group
        @param group Group for which we should find the nearest ennemies
        @param enemy_groups Potential enemy groups
        @param n number of nearby groups to take
        """
        targets = []
        sorted_list = sorted(enemy_groups, key=lambda group: player_group.points[0].position.distance_to_point(group[0].points[0].position))
        for i in range(n):
            if len(sorted_list) <= i:
                break
            else:
                targets.append(sorted_list[i][0])
        return targets


    def find_nearest_enemy_group(self, player_group, enemy_groups):
        """
        Search the enemy groups for a potential target suitable to armored assault
        @param group Group for which we should find the nearest ennemy
        @param enemy_groups Potential enemy groups
        """
        min_distance = 99999999
        target = None
        for dcs_group, group in enemy_groups:
            dist = player_group.points[0].position.distance_to_point(dcs_group.points[0].position)
            if dist < min_distance:
                min_distance = dist
                target = dcs_group
        return target


    def get_artillery_target_in_range(self, dcs_group, group, enemy_groups):
        """
        Search the enemy groups for a potential target suitable to an artillery unit
        """
        rng = group.units[0].threat_range
        if len(enemy_groups) == 0:
            return None
        for o in range(10):
            potential_target = random.choice(enemy_groups)[0]
            distance_to_target = dcs_group.points[0].position.distance_to_point(potential_target.points[0].position)
            if distance_to_target < rng:
                return potential_target.points[0].position
        return None


    def get_artilery_group_distance_from_frontline(self, group):
        """
        For artilery group, decide the distance from frontline with the range of the unit
        """
        rg = group.units[0].threat_range - 7500
        if rg > DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY]:
            rg = DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY]
        if rg < DISTANCE_FROM_FRONTLINE[CombatGroupRole.TANK]:
            rg = DISTANCE_FROM_FRONTLINE[CombatGroupRole.TANK] + 100
        return rg


    def get_valid_position_for_group(self, conflict_position, isplayer, combat_width, distance_from_frontline):
        i = 0
        while i < 25: # 25 attempt for valid position
            heading_diff = -90 if isplayer else 90
            shifted = conflict_position[0].point_from_heading(self.conflict.heading,
                                                     random.randint((int)(-combat_width / 2), (int)(combat_width / 2)))
            final_position = shifted.point_from_heading(self.conflict.heading + heading_diff, distance_from_frontline)

            if self.conflict.theater.is_on_land(final_position):
                return final_position
            else:
                i = i + 1
                continue
        return None

    def _generate_group(self, side: Country, unit: VehicleType, count: int, at: Point, move_formation: PointAction = PointAction.OffRoad, heading=0):

        if side == self.conflict.attackers_country:
            cp = self.conflict.from_cp
        else:
            cp = self.conflict.to_cp

        logging.info("armorgen: {} for {}".format(unit, side.id))
        group = self.m.vehicle_group(
                side,
                namegen.next_unit_name(side, cp.id, unit), unit,
                position=self._group_point(at),
                group_size=count,
                heading=heading,
                move_formation=move_formation)

        for c in range(count):
            vehicle: Vehicle = group.units[c]
            vehicle.player_can_drive = True

        return group