use glc::{
    matrix::Matrix4f,
    misc::{BindGuard, Bindable},
    shader::{Program, Type, Uniform},
    texture::Texture,
    vector::Vector4f,
};
use space_crush_common::{
    components::{Player, PlayerOwned, Position, Ship, ShipType, Velocity},
    misc::LogResult,
};
use specs::{prelude::*, ReadExpect, ReadStorage, System, World};

use crate::{
    constants::{
        PLAYER_COLOR_DEFAULT, SHIP_SIZE, UNIFORM_BUFFER_INDEX_CAMERA, UNIFORM_BUFFER_INDEX_UNIFORM,
    },
    misc::WorldHelper,
    resources::Geometry,
    Error,
};

pub struct Ships {
    program: Program,
    texture_bomber: Texture,
    texture_fighter: Texture,
    texture_transporter: Texture,
    location_model: gl::GLint,
    location_glow_color: gl::GLint,
}

impl Ships {
    pub fn new(world: &World) -> Result<Self, Error> {
        let program = world.load_program(vec![
            (Type::Vertex, "resources/shader/ship/vert.glsl"),
            (Type::Fragment, "resources/shader/ship/frag.glsl"),
        ])?;

        program.uniform_block_binding("Camera", UNIFORM_BUFFER_INDEX_CAMERA)?;
        program.uniform_block_binding("Global", UNIFORM_BUFFER_INDEX_UNIFORM)?;

        let location_model = program.uniform_location("uModel")?;
        let location_glow_color = program.uniform_location("uGlowColor")?;

        program.bind();
        program.uniform("uTexture", Uniform::Texture(0))?;
        program.unbind();

        let texture_bomber = world.load_texture("resources/textures/ship_bomber.png")?;
        let texture_fighter = world.load_texture("resources/textures/ship_fighter.png")?;
        let texture_transporter = world.load_texture("resources/textures/ship_transporter.png")?;

        Ok(Self {
            program,
            texture_bomber,
            texture_fighter,
            texture_transporter,
            location_model,
            location_glow_color,
        })
    }
}

#[derive(SystemData)]
pub struct ShipsData<'a> {
    geometry: ReadExpect<'a, Geometry>,
    position: ReadStorage<'a, Position>,
    velocity: ReadStorage<'a, Velocity>,
    ship: ReadStorage<'a, Ship>,
    owned: ReadStorage<'a, PlayerOwned>,
    player: ReadStorage<'a, Player>,
}

impl<'a> System<'a> for Ships {
    type SystemData = ShipsData<'a>;

    fn run(&mut self, data: Self::SystemData) {
        let ShipsData {
            geometry,
            position,
            velocity,
            ship,
            owned,
            player,
        } = data;

        gl::enable(gl::BLEND);
        gl::blend_func(gl::SRC_ALPHA, gl::ONE_MINUS_SRC_ALPHA);

        let _guard = BindGuard::new(&self.program);

        for (p, v, ship, owned) in (&position, &velocity, &ship, owned.maybe()).join() {
            let _guard = match ship.type_ {
                ShipType::Fighter => BindGuard::new(&self.texture_fighter),
                ShipType::Bomber => BindGuard::new(&self.texture_bomber),
                ShipType::Transporter => BindGuard::new(&self.texture_transporter),
            };

            let c = match owned.and_then(|owned| player.get(owned.owner)) {
                Some(pv) => &pv.color,
                None => &*PLAYER_COLOR_DEFAULT,
            };

            let p_x = p.pos.x;
            let p_y = p.pos.y;
            let d_x = v.dir.x;
            let d_y = v.dir.y;
            let s = SHIP_SIZE;

            let m = Matrix4f::new(
                Vector4f::new(-s * d_y, s * d_x, 0.0, 0.0),
                Vector4f::new(-s * d_x, -s * d_y, 0.0, 0.0),
                Vector4f::new(0.0, 0.0, s, 0.0),
                Vector4f::new(p_x, p_y, 0.0, 1.0),
            );

            self.program
                .uniform(self.location_glow_color, Uniform::Vector4f(&c))
                .error("Error while updating glow color");
            self.program
                .uniform(self.location_model, Uniform::Matrix4f(&m))
                .error("Error while updating model matrix");

            geometry.render_quad();
        }

        gl::disable(gl::BLEND);
    }
}
