import pygame
import settings
import variables
import utils
import gui
import random
import math
from sprites import Sprite, Animation
from datetime import datetime
from copy import copy


class MainObject(object):

    def __init__(self, position, angle, parametres, sprite=None):
        self.id = str(id(self))
        self.collision_objects = []
        self.collision_exceptions = []
        self.collision_radius = None

        self.angle = None
        self.set_angle(angle)

        self.main_sprite = None
        if sprite is not None:
            self.main_sprite = sprite
        if parametres.get('sprites'):
            self.load_sprite(parametres['sprites']['main'])

        self.animations = None
        if parametres.get('animations'):
            self.load_animations(parametres.get('animations'))

        self.x = None
        self.y = None
        self.set_position(position)

        self.status = 'Alive'  # Alive, Dies, Dead, Explosion
        self.owner = None
        self.type = parametres['type']
        self.species = parametres['species']

        self.speed = parametres['speed']
        self.damage = parametres['damage']

        if self.type == 'ship':
            self.hull = parametres['hull']
            self.max_hull = self.hull
            self.max_energy = parametres['energy']
            self.current_energy = parametres['energy']
            self.shield_active = False

        self.guns = {}

        # debug options
        self.collision_radius_color = 'green'
        self.collision_border = 1

    @property
    def get_x(self):
        return self.x

    @property
    def get_y(self):
        return self.y

    @property
    def get_position(self):
        return self.x, self.y

    @property
    def has_body(self):
        if self.status == 'Alive':
            return True
        if self.status == 'Explosion':
            return True
        return False

    @property
    def has_live(self):
        if self.status == 'Alive':
            return True
        if self.status == 'Dies':
            return True
        if self.status == 'Explosion':
            return True
        return False

    def load_sprite(self, path):
        self.main_sprite = Sprite(path)
        self.main_sprite.rotate(self.angle)
        self.main_sprite.scale2x()
        self.set_collision_radius()

    def load_animations(self, paths):
        self.animations = {}
        for key, value in paths.items():
            self.animations[key] = Animation(value)
            self.animations[key].rotate(self.angle)
            self.animations[key].scale2x()

    def add_collision_exceptions(self, exception):
        self.collision_exceptions.append(exception)

    def set_x(self, x: float):
        self.x = x
        if self.main_sprite is not None:
            self.main_sprite.set_x(x - self.main_sprite.get_width / 2)

    def set_y(self, y: float):
        self.y = y
        if self.main_sprite is not None:
            self.main_sprite.set_y(y - self.main_sprite.get_width / 2)

    def set_position(self, position):
        self.set_x(position[0])
        self.set_y(position[1])

    def set_angle(self, angle):
        self.angle = angle

    def set_collision_radius(self):
        if self.main_sprite.get_width > self.main_sprite.get_height:
            self.collision_radius = self.main_sprite.get_width / 2
        else:
            self.collision_radius = self.main_sprite.get_height / 2

    def check_ammo(self):
        if len(self.guns) > 0:
            for gun in self.guns.values():
                if gun.shell_remainder <= 0:
                    if gun.gun['reload'] is not None:
                        if utils.delay(gun.gun['reload'], _id=self.id + 'reload', once=True):
                            gun.shell_remainder = gun.gun['clip_size']

    def add_collision(self, collision_object):
        self.collision_objects.append(collision_object)
        self.collision_radius_color = 'red'

    def delete_collisions(self):
        self.collision_border = 1
        self.collision_radius_color = 'green'

    def draw_collision(self):
        pygame.draw.circle(variables.screen,
                           self.collision_radius_color,
                           (self.get_x, self.get_y),
                           self.collision_radius, self.collision_border)

    def check_collisions(self):
        for collision_object in self.collision_objects:
            if not self.collision_exceptions.count(collision_object.owner):
                offset = (collision_object.main_sprite.get_x - self.main_sprite.get_x,
                          collision_object.main_sprite.get_y - self.main_sprite.get_y)
                if self.main_sprite.mask.overlap_area(collision_object.main_sprite.mask, offset):
                    if self.shield_active:
                        self.current_energy -= collision_object.damage
                    else:
                        self.hull -= collision_object.damage

                    self.collision_border = 5

        self.collision_objects = []

    def render(self):
        if self.status == 'Alive':

            if self.get_x + self.main_sprite.get_width > 0 and \
                    self.get_x - self.main_sprite.get_width < settings.SCREEN_WIDTH:

                if self.get_y + self.main_sprite.get_height > 0 and \
                        self.get_y - self.main_sprite.get_height < settings.SCREEN_HEIGHT:

                    for gun in self.guns.values():
                        gun.render((self.x, self.y))

                    if self.animations.get('engine'):
                        self.animations['engine'].render(variables.screen, self.get_position, settings.TICK)

                    if len(self.guns) > 0:
                        for gun in self.guns.values():
                            gun.render(self.get_position)

                    self.main_sprite.render(variables.screen, self.get_position)

                    if self.shield_active:
                        self.animations['shield'].render(variables.screen, self.get_position, settings.TICK)

        if self.status == 'Dies':
            if self.animations['destruction'].render(variables.screen, self.get_position, settings.TICK):
                self.status = 'Dead'

        if settings.DEBUG:
            self.draw_collision()
            if self.type == 'ship':
                gui.text(str(self.hull), self.get_x - 32, self.get_y - 48)
                gui.text(str(self.current_energy), self.get_x, self.get_y - 48)


class Gun(object):

    def __init__(self, angle, parametres):
        self.id = str(id(self))
        self.angle = angle
        self.owner = None
        self.type = parametres['type']
        self.location = parametres['location']
        self.collaboration = parametres['collaboration']
        self.gun = variables.armament_parameters['guns'][self.type]
        self.shell = variables.armament_parameters['shells'][self.gun['shell']]

        self.gun_barrels = []
        for barrel in parametres['barrels']:
            self.add_gun_barrel((barrel['x'], barrel['y']))

        self.shell_remainder = self.gun['clip_size']

        self.counter = 0
        self.list_casings = []
        for gun_barrel in range(len(self.gun_barrels)):
            self.list_casings.append(gun_barrel)

        self.main_sprite = Sprite(path=self.shell['sprites']['main'])
        self.main_sprite.rotate(self.angle)
        self.main_sprite.scale2x()

    @property
    def get_barrel_count(self):
        return len(self.gun_barrels)

    def get_loaded_barrels(self):
        loaded_barrels = []

        if self.collaboration == 'parallel':
            for index, gun_barrel in enumerate(self.gun_barrels):
                loaded_barrels.append(index)

        if self.collaboration == 'alternately':
            index = self.counter - (int(self.counter / self.get_barrel_count) * self.get_barrel_count)
            loaded_barrels.append(index)
            self.counter += 1

        if self.collaboration == 'random':
            if len(self.list_casings) == 0:
                for gun_barrel in range(len(self.gun_barrels)):
                    self.list_casings.append(gun_barrel)

            index = random.randint(0, len(self.list_casings) - 1)
            loaded_barrels.append(self.list_casings[index])
            self.list_casings.pop(index)

        return loaded_barrels

    def set_angle(self, angle):
        self.angle = angle

    def add_gun_barrel(self, position):
        self.gun_barrels.append(position)

    def set_location(self, location):
        self.location = location
        if self.location == 'external':
            shell = variables.armament_parameters['guns'][self.type]['shell']
            self.main_sprite = Sprite(variables.armament_parameters['shells'][shell]['sprites']['main'])
            self.main_sprite.rotate(self.angle)
            self.main_sprite.scale2x()

    def set_collaboration(self, collaboration):
        self.collaboration = collaboration

    def set_owner(self, owner):
        self.owner = owner

    def create_shell(self, position):
        x, y = position
        variables.objects.add(Shell((x, y), self.angle, self.shell, self.owner))

    def shot(self, position):
        if self.shell_remainder > 0:
            if utils.delay(self.gun['rate_of_fire'], _id=self.id + 'shot'):
                loaded_barrels = self.get_loaded_barrels()
                for index in loaded_barrels:
                    barrel = self.gun_barrels[index]
                    x = position[0] + barrel[0]
                    y = position[1] + barrel[1]
                    self.create_shell((x, y))

                self.shell_remainder -= len(loaded_barrels)

    def update(self):
        if self.shell_remainder <= 0:
            if self.gun['reload'] is not None:
                if utils.delay(self.gun['reload'], _id=self.id + 'reload', once=True):
                    self.shell_remainder = self.gun['clip_size']

    def render(self, position):
        if self.location == 'external':
            for index in self.list_casings:
                barrel = self.gun_barrels[index]
                x = position[0] + barrel[0]
                y = position[1] + barrel[1]
                self.main_sprite.render(variables.screen, (x, y))


class Shell(MainObject):

    def __init__(self, position, angle, parametres, owner):
        super().__init__(position, angle, parametres)
        self.owner = owner
        self.add_collision_exceptions(self.owner)
        self.damage_radius = parametres['damage_radius']
        '''if self.damage_radius is not None:
            self.damage = 0'''
        self.ttl = parametres['ttl']
        self.create = datetime.now()

    def check_collisions(self):

        for collision_object in self.collision_objects:
            if not self.collision_exceptions.count(collision_object.owner):
                offset = (collision_object.main_sprite.get_x - self.main_sprite.get_x,
                          collision_object.main_sprite.get_y - self.main_sprite.get_y)
                if self.main_sprite.mask.overlap_area(collision_object.main_sprite.mask, offset):

                    if self.damage_radius is not None:
                        #variables.sound_explosion[random.randint(0, len(variables.sound_explosion) - 1)].play()
                        self.explosion()

                    if self.status != 'Explosion':
                        self.status = 'Dead'

        self.collision_objects = []

    def explosion(self):
        parametres = {
            'type': 'shell',
            'species': 'explosion',
            'speed': 0,
            'damage': self.damage,
            'damage_radius': None,
            'ttl': self.ttl
        }

        explosion = Shell((self.x, self.y), self.angle, parametres, None)
        explosion.status = 'Explosion'
        explosion.main_sprite = Sprite(sprite=self.animations['explosions'].get_sprite(0))
        explosion.set_collision_radius()
        explosion.animations = {'explosions': self.animations['explosions']}
        explosion.animations['explosions'].rotate(random.randint(1, 360))
        explosion.collision_radius = explosion.main_sprite.get_width / 2
        explosion.set_x(explosion.get_x)
        explosion.set_y(explosion.get_y)
        variables.objects.add(explosion)

    def check_climb_out(self):
        if self.get_x - self.main_sprite.get_width / 2 < 0:
            self.status = 'Dead'
        if self.get_x + self.main_sprite.get_width / 2 > settings.SCREEN_WIDTH:
            self.status = 'Dead'
        if self.get_y - self.main_sprite.get_height / 2 < 0:
            self.status = 'Dead'
        if self.get_y + self.main_sprite.get_height / 2 > settings.SCREEN_HEIGHT:
            self.status = 'Dead'

    def update(self):
        self.check_climb_out()
        self.set_x(self.get_x + math.sin(math.radians(self.angle)) * self.speed)

        if self.status == 'Explosion' and utils.delay(0.5, _id=self.id + 'explosion', once=True):
            self.damage = 0

        if utils.get_seconds(self.create, datetime.now()) > self.ttl:
            self.status = 'Dead'

    def render(self):
        if self.status == 'Alive':
            correction_x = self.main_sprite.get_width
            correction_y = self.main_sprite.get_height
            if self.get_x + correction_x > 0 and self.get_x - correction_x < settings.SCREEN_WIDTH:
                if self.get_y + correction_y > 0 and self.get_y - correction_y < settings.SCREEN_HEIGHT:
                    self.animations['main'].render(variables.screen, self.get_position, settings.TICK)

        if settings.DEBUG:
            self.draw_collision()

        if self.status == 'Explosion':
            if self.animations['explosions'].render(variables.screen, self.get_position, settings.TICK):
                self.status = 'Dead'


class Player(MainObject):

    def __init__(self, parametres: dict):
        super().__init__((100, settings.SCREEN_HEIGHT / 2), 90, parametres)
        self.owner = 'player'
        self.add_collision_exceptions(self.owner)
        self.shield_consumption = parametres['shield_consumption']
        self.shield_restoration = parametres['shield_restoration']
        self.controls = parametres['controls']

        self.text = {}

        for key, value in parametres['guns'].items():
            self.guns[key] = Gun(self.angle, value)
            self.guns[key].set_angle(self.angle)
            self.guns[key].set_owner(self.owner)

    def key_press(self):
        if self.status == 'Alive':
            keys = pygame.key.get_pressed()
            for key, value in self.controls.items():
                if keys[pygame.key.key_code(key)]:
                    self.actions(value)

    def actions(self, action):
        if action == 'up':
            self.set_y(self.get_y - self.speed)

        if action == 'right':
            self.set_x(self.get_x + self.speed)

        if action == 'down':
            self.set_y(self.get_y + self.speed)

        if action == 'left':
            self.set_x(self.get_x - self.speed)

        if action == 'shield':
            if self.current_energy >= self.shield_consumption:
                self.shield_active = True

        if self.guns.get(action):
            self.guns.get(action).shot((self.x, self.y))

    def check_climb_out(self):
        if self.get_x < self.main_sprite.get_width / 2:
            self.set_x(self.main_sprite.get_width / 2)
        if self.get_x > variables.playground_width - self.main_sprite.get_width / 2:
            self.set_x(variables.playground_width - self.main_sprite.get_width / 2)
        if self.get_y < self.main_sprite.get_height / 2 + variables.interface:
            self.set_y(self.main_sprite.get_height / 2 + variables.interface)
        if self.get_y > variables.playground_height - self.main_sprite.get_height / 2 + variables.interface:
            self.set_y(variables.playground_height - self.main_sprite.get_height / 2 + variables.interface)

    def get_gun_status_text(self, gun_name, gun_title):
        gun = self.guns[gun_name]
        if gun.shell_remainder <= 0:
            text = 'Перезарядка'
        else:
            text = f"{str(gun.shell_remainder)} из {gun.gun['clip_size']}"
        return gun_title + ': ' + text

    def update(self):
        self.check_ammo()

        self.shield_active = False
        self.key_press()
        self.check_climb_out()

        divider = 10
        if utils.delay(1 / divider, _id=self.id + 'player_shield'):
            if self.shield_active:
                if self.current_energy > 0:
                    self.current_energy -= self.shield_consumption / divider

                if self.current_energy < 0:
                    self.current_energy = 0
            else:
                if self.current_energy < self.max_energy:
                    self.current_energy += self.shield_restoration / divider

                if self.current_energy > self.max_energy:
                    self.current_energy = self.max_energy

        if self.hull <= 0:
            self.hull = 0
            self.status = 'Dies'

        self.text['hull'] = 'Корпус: ' + str(int(self.hull))
        self.text['energy'] = 'Энергия: ' + str(int(self.current_energy))
        self.text['player_cannon'] = self.get_gun_status_text('player_cannon', 'Автопушка')
        self.text['player_launcher'] = self.get_gun_status_text('player_launcher', 'Ракетница')
        self.text['player_score'] = 'Счёт: ' + str(variables.player_score)


class Portal(object):

    def __init__(self):
        pass

    @property
    def random_vector(self):
        pos1 = (random.uniform(variables.playground_width, variables.playground_width + 100) + 100,
                random.uniform(0, variables.playground_height))

        pos2 = (random.uniform(0, - 100) - 100,
                random.uniform(0, variables.playground_height))

        return pos1, pos2

    @property
    def straight_vector(self):
        y = random.uniform(0, variables.playground_height)

        pos1 = (random.uniform(variables.playground_width, variables.playground_width + 100) + 100,
                y)

        pos2 = (random.uniform(0, - 100) - 100,
                y)

        return pos1, pos2

    @property
    def diagonal_vector(self):
        y = random.uniform(0, 50)
        if random.randint(0, 1) == 0:
            pos1 = (random.uniform(variables.playground_width, variables.playground_width + 100) + 100,
                    y)

            pos2 = (random.uniform(0, - 100) - 100,
                    variables.playground_height - y)
        else:
            pos1 = (random.uniform(variables.playground_width, variables.playground_width + 100) + 100,
                    variables.playground_height - y)

            pos2 = (random.uniform(0, - 100) - 100,
                    y)

        return pos1, pos2

    @staticmethod
    def motion_modifier_direct(x, y, _vars):
        return x, y

    @staticmethod
    def motion_modifier_sin(x, y, _vars):
        _vars['speed'] = math.sin(math.radians(_vars['angle'])) * _vars['speed']
        x = x + _vars['speed']
        y = math.sin(math.radians(x / _vars['number1'])) * _vars['number2']
        y += variables.playground_height / _vars['number3']
        return x, y

    @staticmethod
    def motion_modifier_cos(x, y, _vars):
        _vars['speed'] = math.sin(math.radians(_vars['angle'])) * _vars['speed']
        x = x + _vars['speed']
        y = math.cos(math.radians(x / _vars['number1'])) * _vars['number2']
        y += variables.playground_height / _vars['number3']
        return x, y

    def add(self):
        angle = 270
        delay = variables.delay_spawn - variables.global_tick / 10000
        if delay < 0.5:
            delay = 0.5

        if utils.delay(delay, _id="portal"):
            random_list = [0, 0, 0, 0, 0, 0, 0, 1, 1, 1]
            cube = random_list[random.randint(0, len(random_list) - 1)]

            index = random.randint(0, len(variables.enemies_keys) - 1)
            key = variables.enemies_keys[index]

            _vars = {
                'number1': random.randint(3, 5),
                'number2': random.randint(200, 400),
                'number3': random.uniform(1.5, 3.0)
            }
            modifiers = [self.motion_modifier_direct, self.motion_modifier_sin, self.motion_modifier_cos]
            modifier = modifiers[random.randint(0, len(modifiers) - 1)]

            if key == 'scout':
                if cube == 0:
                    variables.objects.add(
                        Enemy(
                            (0, 0),
                            angle,
                            variables.enemies_parameters[key],
                            self.random_vector,
                            0,
                            modifier,
                            _vars
                        )
                    )
                elif cube == 1:

                    for i in range(random.randint(3, 10)):
                        variables.objects.add(
                            Enemy(
                                (0, 0),
                                angle,
                                variables.enemies_parameters[key],
                                self.straight_vector,
                                -i * 150,
                                self.motion_modifier_sin,
                                _vars
                            )
                        )
            elif key == 'frigate':
                if cube == 0:
                    variables.objects.add(
                        Enemy(
                            (0, 0),
                            angle,
                            variables.enemies_parameters[key],
                            self.random_vector,
                            0,
                            self.motion_modifier_direct,
                            _vars
                        )
                    )
                elif cube == 1:
                    for i in range(random.randint(2, 3)):
                        variables.objects.add(
                            Enemy(
                                (0, 0),
                                angle,
                                variables.enemies_parameters[key],
                                self.diagonal_vector,
                                -i * 200,
                                self.motion_modifier_direct,
                                _vars
                              )
                        )
            elif key == 'fighter':
                if cube == 0:
                    for i in range(random.randint(1, 2)):
                        variables.objects.add(
                            Enemy(
                                (0, 0),
                                angle,
                                variables.enemies_parameters[key],
                                self.random_vector,
                                0,
                                self.motion_modifier_direct,
                                _vars
                            )
                        )
                elif cube == 1:
                    variables.objects.add(
                        Enemy(
                            (0, 0),
                            angle,
                            variables.enemies_parameters[key],
                            self.straight_vector,
                            0,
                            self.motion_modifier_sin,
                            _vars
                        )
                    )
            elif key == 'torpedo':
                if cube == 0:
                    variables.objects.add(
                        Enemy(
                            (0, 0),
                            angle,
                            variables.enemies_parameters[key],
                            self.random_vector,
                            0,
                            modifier,
                            _vars
                        )
                    )
                elif cube == 1:
                    for i in range(random.randint(3, 5)):
                        variables.objects.add(
                            Enemy(
                                (0, 0),
                                angle,
                                variables.enemies_parameters[key],
                                self.diagonal_vector,
                                -i * 300,
                                self.motion_modifier_direct,
                                _vars
                            )
                        )
            else:
                variables.objects.add(
                    Enemy(
                        (0, 0),
                        angle,
                        variables.enemies_parameters[key],
                        self.random_vector,
                        0,
                        self.motion_modifier_direct
                    )
                )


class Enemy(MainObject):

    def __init__(self, position, angle, parametres, vector, distance, motion_modifier, _vars={}):
        super().__init__(position, angle, parametres)
        #self.speed = 0
        self.owner = 'enemy'
        self.add_collision_exceptions(self.owner)
        self.start_shoot = random.randint(self.main_sprite.get_width, self.main_sprite.get_width * 2)

        self.pos1, self.pos2 = vector
        self.distance = distance
        self.motion_modifier = motion_modifier
        self.vars = _vars

        for key, value in parametres['guns'].items():
            self.guns[key] = Gun(self.angle, value)
            self.guns[key].set_angle(self.angle)
            self.guns[key].set_owner(self.owner)

    def point_to_line(self):
        x1, y1 = self.pos1
        x2, y2 = self.pos2
        rab = math.sqrt(math.pow(x2 - x1, 2) + math.pow(y2 - y1, 2))
        k = self.distance / rab
        xc = x1 + (x2 - x1) * k
        yc = y1 + (y2 - y1) * k
        return xc, yc

    def update(self):
        self.check_ammo()

        if self.status == 'Alive':
            self.distance += self.speed
            x, y = self.point_to_line()
            self.vars['speed'] = self.speed
            self.vars['angle'] = self.angle
            self.set_position(self.motion_modifier(x, y, self.vars))
            #self.set_position(self.move_model(self.x, self.y, self.speed, self.angle, self.rnd))

        if self.hull <= 0 and self.status != 'Dies':
            #variables.sound_enemy_dead[random.randint(0, len(variables.sound_enemy_dead) - 1)].play()
            variables.player_score += self.max_hull
            self.status = 'Dies'

        if self.get_x < -200:
            self.status = 'Dead'

        if self.x < variables.playground_width - self.start_shoot:
            for gun in self.guns.values():
                gun.shot((self.x, self.y))