Documentation
use std::collections::HashMap;
use std::time::{Instant, Duration};
use std::thread;
use std::cell::Cell;
use std::iter::FromIterator;

use cgmath::{self, SquareMatrix, InnerSpace};
use conrod;
use glium::{self, Surface, DisplayBuild, VertexBuffer, IndexBuffer};
use glium::index::PrimitiveType;
use glium::glutin::{Event, WindowBuilder};
use glium::texture::Texture2d;

use mesh::{Mesh, Vertex};
use game::Game;

pub struct Shader {
    program: glium::Program,
}

pub struct Context {
    display: glium::Display,
    camera: Camera,
    size: (u32, u32),
    mouse: (i32, i32),
}

impl Context {
    pub fn create_mesh(&self, vertices: &[Vertex], indices: &[u32]) -> Mesh {
        Mesh {
            vertices: VertexBuffer::dynamic(&self.display, &vertices).unwrap(),
            indices: IndexBuffer::new(&self.display, PrimitiveType::TrianglesList, indices).unwrap(),
        }
    }

    pub fn create_shader(&self, vertex_path: &str, fragment_path: &str) -> Shader {
        let vertex_src = string_from_file(vertex_path);
        let fragment_src = string_from_file(fragment_path);
        Shader { 
            program: glium::Program::from_source(&self.display, &vertex_src, &fragment_src, None).unwrap()
        }
    }

    pub fn mouse(&self) -> (i32, i32) {
        self.mouse
    }

    pub fn size(&self) -> (u32, u32) {
        self.size
    }

    pub fn screen_point_to_direction(&self, x: i32, y: i32) -> cgmath::Vector3<f32> {
        let w = self.size.0 as f32;
        let h = self.size.1 as f32;
        let x = (2.0 * (x as f32)) / w - 1.0;
        let y = 1.0 - (2.0 * (y as f32)) / h;
        let u = (self.camera.perspective(w / h) * self.camera.view.get()).invert().unwrap();
        let v0 = u * cgmath::Vector4::new(x, y, 0.0, 1.0);
        let v1 = u * cgmath::Vector4::new(x, y, 1.0, 1.0);
        ((v1 / v1.w) - (v0 / v0.w)).truncate().normalize()
    }
    
    pub fn set_view(&self, view: cgmath::Matrix4<f32>) {
        self.camera.view.set(view);
    }
}

pub struct Camera {
    near: f32,
    far: f32,
    fov: cgmath::Rad<f32>,
    view: Cell<cgmath::Matrix4<f32>>,
}

impl Camera {
    pub fn new(near: f32, far: f32, fov: cgmath::Rad<f32>) -> Self {
        Camera {
            near: near,
            far: far,
            fov: fov,
            view: Cell::new(cgmath::Matrix4::identity()),
        }
    }

    pub fn perspective(&self, aspect: f32) -> cgmath::Matrix4<f32> {
        cgmath::perspective(self.fov, aspect, self.near, self.far)
    }
}

struct Conrod {
    conrod: conrod::Ui,
    renderer: conrod::backend::glium::Renderer,
    images: conrod::image::Map<Texture2d>,
    needs_update: bool,
}

pub struct Engine {
    context: Context,
    ui: Conrod,
    last_update: Instant,
}

impl Engine {
    pub fn new(title: &str, size: (u32, u32)) -> Self {
        let display = WindowBuilder::new()
                .with_title(title)
                .with_dimensions(size.0, size.1)
                .with_vsync()
                .build_glium()
                .unwrap();
        let mut conrod = conrod::UiBuilder::new([size.0 as f64, size.1 as f64]).theme(theme()).build();
        conrod.fonts.insert_from_file("assets/fonts/NotoSans/NotoSans-Regular.ttf").unwrap();
        let renderer = conrod::backend::glium::Renderer::new(&display).unwrap();
        Engine {
            context: Context {
                camera: Camera::new(0.1, 1024.0, cgmath::Deg(60.0).into()),
                display: display,
                mouse: (0, 0),
                size: size,
            },
            ui: Conrod {
                conrod: conrod,
                renderer: renderer,
                images: conrod::image::Map::new(),
                needs_update: true,
            },
            last_update: Instant::now(),
        }
    }

    pub fn run<G: Game>(mut self) {
        let mut game = {
            let mut ui_builder = ::ui::UiBuilder {
                conrod: &mut self.ui.conrod,
            };
            G::new(&self.context, &mut ui_builder)
        };
        'main: loop {
            for event in self.get_events() {
                if self.handle_ui_event(&event) {
                    self.ui.needs_update = true;
                }
                match event.clone() {
                    Event::Closed => break 'main,
                    event => if let Some(action) = game.on_event(&self.context, event) {
                        game.on_action(&self.context, action);
                    }
                }
                match event {
                    Event::MouseMoved(x, y) => self.context.mouse = (x, y),
                    Event::Resized(w, h) => self.context.size = (w, h),
                    _ => {},
                }
            }
            {
                let mut ui = self.ui.conrod.set_widgets();
                for action in game.on_ui(&mut ui) {
                    game.on_action(&self.context, action);
                }
            }
            self.draw(&game);
        }
    }

    fn draw<G: Game>(&mut self, game: &G) {
        let target = self.context.display.draw();
        let aspect = aspect(&target);
        let params = glium::DrawParameters {
            depth: glium::Depth {
                test: glium::draw_parameters::DepthTest::IfLess,
                write: true,
                ..Default::default()
            },
            backface_culling: glium::draw_parameters::BackfaceCullingMode::CullCounterClockwise,
            ..Default::default()
        };
        let mut frame = Frame {
            target: target,
            view: self.context.camera.view.get().into(),
            perspective: self.context.camera.perspective(aspect).into(),
            params: params,
        };
        frame.target.clear_color_and_depth((0.0, 0.0, 0.0, 1.0), 1.0);
        game.draw(&mut frame);
        let primitives = self.ui.conrod.draw();
        self.ui.renderer.fill(&self.context.display, primitives, &self.ui.images);
        self.ui.renderer.draw(&self.context.display, &mut frame.target, &self.ui.images).unwrap();
        frame.target.finish().unwrap();
    }

    fn handle_ui_event(&mut self, event: &Event) -> bool {
        if let Some(event) = conrod::backend::winit::convert(event.clone(), &self.context.display) {
            self.ui.conrod.handle_event(event);
            true
        } else {
            false
        }
    }

    fn get_events(&mut self) -> Vec<glium::glutin::Event> {
        let last_update = self.last_update;
        let sixteen_ms = Duration::from_millis(16);
        let duration_since_last_update = Instant::now().duration_since(last_update);
        if duration_since_last_update < sixteen_ms {
            thread::sleep(sixteen_ms - duration_since_last_update);
        }
        let mut events = Vec::from_iter(self.context.display.poll_events());
        if events.is_empty() && !self.ui.needs_update {
            events.extend(self.context.display.wait_events().next());
        }
        self.ui.needs_update = false;
        self.last_update = Instant::now();
        events
    }
}

fn aspect(target: &glium::Frame) -> f32 {
    let (width, height) = target.get_dimensions();
    width as f32 / height as f32
}

pub struct Frame<'a> {
    view: [[f32; 4]; 4],
    perspective: [[f32; 4]; 4],
    target: glium::Frame,
    params: glium::DrawParameters<'a>,
}

impl<'a> Frame<'a> {
    pub fn draw(&mut self, mesh: &Mesh, shader: &Shader, transform: cgmath::Matrix4<f32>) {
        let model: [[f32; 4]; 4] = transform.into();
        self.target
            .draw(&mesh.vertices,
                  &mesh.indices,
                  &shader.program,
                  &uniform! {
                        model: model,
                        view: self.view,
                        perspective: self.perspective,
                  },
                  &self.params)
            .unwrap();
    }
}

fn theme() -> conrod::Theme {
    use conrod::position::{Align, Direction, Padding, Position, Relative};
    conrod::Theme {
        name: "Demo Theme".to_string(),
        padding: Padding::none(),
        x_position: Position::Relative(Relative::Align(Align::Start), None),
        y_position: Position::Relative(Relative::Direction(Direction::Backwards, 20.0), None),
        background_color: conrod::color::DARK_CHARCOAL,
        shape_color: conrod::color::LIGHT_CHARCOAL,
        border_color: conrod::color::BLACK,
        border_width: 0.0,
        label_color: conrod::color::WHITE,
        font_id: None,
        font_size_large: 26,
        font_size_medium: 18,
        font_size_small: 12,
        widget_styling: HashMap::new(),
        mouse_drag_threshold: 0.0,
        double_click_threshold: Duration::from_millis(500),
    }
}

fn string_from_file(path: &str) -> String {
    use std::fs::File;
    use std::io::Read;
    let mut data = String::new();
    let mut file = File::open(path).expect(path);
    file.read_to_string(&mut data).expect(path);
    data
}