use glc::{
    matrix::Matrix4f,
    misc::Bindable,
    shader::{Program, Type},
    vector::Vector3f,
};
use shrev::{EventChannel, ReaderId};
use space_crush_common::{
    misc::{LogResult, WorldHelper as _},
    resources::Global,
};
use specs::{prelude::*, ReadExpect, System, World, WriteExpect};

use crate::{
    constants::{UNIFORM_BUFFER_INDEX_CAMERA, UNIFORM_BUFFER_INDEX_UNIFORM},
    misc::{ControlEvent, MouseButton, MouseEvent, WorldHelper},
    resources::{Camera, Config, Geometry, InputState, Uniform},
    Error,
};

pub struct Init {
    program: Program,
    resolution: (u32, u32),
    mouse_event_id: ReaderId<MouseEvent>,
}

impl Init {
    pub fn new(world: &mut World) -> Result<Self, Error> {
        let program = world.load_program(vec![
            (Type::Vertex, "resources/shader/noise/vert.glsl"),
            (Type::Fragment, "resources/shader/noise/frag.glsl"),
        ])?;
        let resolution = (0, 0);
        let mouse_event_id = world.register_event_reader::<MouseEvent>()?;

        world
            .resource::<Camera>()?
            .bind(UNIFORM_BUFFER_INDEX_CAMERA)?;
        world
            .resource::<Uniform>()?
            .bind(UNIFORM_BUFFER_INDEX_UNIFORM)?;

        Ok(Self {
            program,
            resolution,
            mouse_event_id,
        })
    }
}

#[derive(SystemData)]
pub struct InitData<'a> {
    camera: WriteExpect<'a, Camera>,
    uniform: WriteExpect<'a, Uniform>,
    control_events: WriteExpect<'a, EventChannel<ControlEvent>>,

    global: ReadExpect<'a, Global>,
    config: ReadExpect<'a, Config>,
    geometry: ReadExpect<'a, Geometry>,
    input_state: ReadExpect<'a, InputState>,
    mouse_events: ReadExpect<'a, EventChannel<MouseEvent>>,
}

impl<'a> System<'a> for Init {
    type SystemData = InitData<'a>;

    fn run(&mut self, data: Self::SystemData) {
        let InitData {
            mut camera,
            mut uniform,
            mut control_events,
            global,
            config,
            geometry,
            input_state,
            mouse_events,
        } = data;

        /* screen size */
        if self.resolution != input_state.resolution {
            self.resolution = input_state.resolution;

            gl::viewport(0, 0, self.resolution.0 as _, self.resolution.1 as _);

            camera
                .resize(self.resolution.0 as _, self.resolution.1 as _)
                .error("Error while updating camera");
        }

        /* zoom */
        let mouse_pos = &input_state.mouse_pos;
        let events = mouse_events.read(&mut self.mouse_event_id);
        for event in events {
            match event {
                MouseEvent::ScrollY(delta) => {
                    let s = config.input.camera_zoom_speed;
                    let z = s / (s - delta);
                    let m = Matrix4f::translate((mouse_pos.0, mouse_pos.1, 0.0).into())
                        * Matrix4f::scale(z.into())
                        * Matrix4f::translate((-mouse_pos.0, -mouse_pos.1, 0.0).into());

                    camera
                        .update_with(move |v| m * v)
                        .error("Error while zooming camera");
                }
                MouseEvent::Delta(x, y) if input_state.button_state(&[MouseButton::Right]) => {
                    let m = Matrix4f::translate((*x, -*y, 0.0).into());

                    camera
                        .update_with(move |v| m * v)
                        .error("Error while zooming camera");
                }
                MouseEvent::ButtonDown(b) if b == &config.input.camera_move_button => {
                    control_events.single_write(ControlEvent::LockMouse);
                }
                MouseEvent::ButtonUp(b) if b == &config.input.camera_move_button => {
                    control_events.single_write(ControlEvent::UnlockMouse);
                }
                _ => (),
            }
        }

        /* move camera */
        let up = input_state.key_state(&config.input.camera_move_key_up);
        let down = input_state.key_state(&config.input.camera_move_key_down);
        let left = input_state.key_state(&config.input.camera_move_key_left);
        let right = input_state.key_state(&config.input.camera_move_key_right);

        if up || down || left || right {
            let s = config.input.camera_move_speed * global.delta;
            let translate = Vector3f::new(
                if left { s } else { 0.0 } + if right { -s } else { 0.0 },
                if up { -s } else { 0.0 } + if down { s } else { 0.0 },
                0.0,
            );

            let m = Matrix4f::translate(translate);
            camera
                .update_with(move |v| m * v)
                .error("Error while moving camera");
        }

        uniform
            .update()
            .error("Error while updating global uniform data");

        /* render background */
        self.program.bind();
        geometry.render_quad();
        self.program.unbind();
    }
}
