/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

use api::{ColorU, ImageFormat, ImageBufferKind};
use api::units::*;
use crate::debug_font_data;
use crate::device::{Device, Program, Texture, TextureSlot, VertexDescriptor, ShaderError, VAO};
use crate::device::{TextureFilter, VertexAttribute, VertexAttributeKind, VertexUsageHint};
use euclid::{Point2D, Rect, Size2D, Transform3D, default};
use crate::internal_types::Swizzle;
use std::f32;

#[derive(Debug, Copy, Clone)]
enum DebugSampler {
    Font,
}

impl Into<TextureSlot> for DebugSampler {
    fn into(self) -> TextureSlot {
        match self {
            DebugSampler::Font => TextureSlot(0),
        }
    }
}

const DESC_FONT: VertexDescriptor = VertexDescriptor {
    vertex_attributes: &[
        VertexAttribute {
            name: "aPosition",
            count: 2,
            kind: VertexAttributeKind::F32,
        },
        VertexAttribute {
            name: "aColor",
            count: 4,
            kind: VertexAttributeKind::U8Norm,
        },
        VertexAttribute {
            name: "aColorTexCoord",
            count: 2,
            kind: VertexAttributeKind::F32,
        },
    ],
    instance_attributes: &[],
};

const DESC_COLOR: VertexDescriptor = VertexDescriptor {
    vertex_attributes: &[
        VertexAttribute {
            name: "aPosition",
            count: 2,
            kind: VertexAttributeKind::F32,
        },
        VertexAttribute {
            name: "aColor",
            count: 4,
            kind: VertexAttributeKind::U8Norm,
        },
    ],
    instance_attributes: &[],
};

#[repr(C)]
pub struct DebugFontVertex {
    pub x: f32,
    pub y: f32,
    pub color: ColorU,
    pub u: f32,
    pub v: f32,
}

impl DebugFontVertex {
    pub fn new(x: f32, y: f32, u: f32, v: f32, color: ColorU) -> DebugFontVertex {
        DebugFontVertex { x, y, color, u, v }
    }
}

#[repr(C)]
pub struct DebugColorVertex {
    pub x: f32,
    pub y: f32,
    pub color: ColorU,
}

impl DebugColorVertex {
    pub fn new(x: f32, y: f32, color: ColorU) -> DebugColorVertex {
        DebugColorVertex { x, y, color }
    }
}

pub struct DebugRenderer {
    font_vertices: Vec<DebugFontVertex>,
    font_indices: Vec<u32>,
    font_program: Program,
    font_vao: VAO,
    font_texture: Texture,

    tri_vertices: Vec<DebugColorVertex>,
    tri_indices: Vec<u32>,
    tri_vao: VAO,
    line_vertices: Vec<DebugColorVertex>,
    line_vao: VAO,
    color_program: Program,
}

impl DebugRenderer {
    pub fn new(device: &mut Device) -> Result<Self, ShaderError> {
        let font_program = device.create_program_linked(
            "debug_font",
            &[],
            &DESC_FONT,
        )?;
        device.bind_program(&font_program);
        device.bind_shader_samplers(&font_program, &[("sColor0", DebugSampler::Font)]);

        let color_program = device.create_program_linked(
            "debug_color",
            &[],
            &DESC_COLOR,
        )?;

        let font_vao = device.create_vao(&DESC_FONT, 1);
        let line_vao = device.create_vao(&DESC_COLOR, 1);
        let tri_vao = device.create_vao(&DESC_COLOR, 1);

        let font_texture = device.create_texture(
            ImageBufferKind::Texture2D,
            ImageFormat::R8,
            debug_font_data::BMP_WIDTH,
            debug_font_data::BMP_HEIGHT,
            TextureFilter::Linear,
            None,
        );
        device.upload_texture_immediate(
            &font_texture,
            &debug_font_data::FONT_BITMAP
        );

        Ok(DebugRenderer {
            font_vertices: Vec::new(),
            font_indices: Vec::new(),
            line_vertices: Vec::new(),
            tri_vao,
            tri_vertices: Vec::new(),
            tri_indices: Vec::new(),
            font_program,
            color_program,
            font_vao,
            line_vao,
            font_texture,
        })
    }

    pub fn deinit(self, device: &mut Device) {
        device.delete_texture(self.font_texture);
        device.delete_program(self.font_program);
        device.delete_program(self.color_program);
        device.delete_vao(self.tri_vao);
        device.delete_vao(self.line_vao);
        device.delete_vao(self.font_vao);
    }

    pub fn line_height(&self) -> f32 {
        debug_font_data::FONT_SIZE as f32 * 1.1
    }

    /// Draws a line of text at the provided starting coordinates.
    ///
    /// If |bounds| is specified, glyphs outside the bounds are discarded.
    ///
    /// Y-coordinates is relative to screen top, along with everything else in
    /// this file.
    pub fn add_text(
        &mut self,
        x: f32,
        y: f32,
        text: &str,
        color: ColorU,
        bounds: Option<DeviceRect>,
    ) -> default::Rect<f32> {
        let mut x_start = x;
        let ipw = 1.0 / debug_font_data::BMP_WIDTH as f32;
        let iph = 1.0 / debug_font_data::BMP_HEIGHT as f32;

        let mut min_x = f32::MAX;
        let mut max_x = -f32::MAX;
        let mut min_y = f32::MAX;
        let mut max_y = -f32::MAX;

        for c in text.chars() {
            let c = c as usize - debug_font_data::FIRST_GLYPH_INDEX as usize;
            if c < debug_font_data::GLYPHS.len() {
                let glyph = &debug_font_data::GLYPHS[c];

                let x0 = (x_start + glyph.xo + 0.5).floor();
                let y0 = (y + glyph.yo + 0.5).floor();

                let x1 = x0 + glyph.x1 as f32 - glyph.x0 as f32;
                let y1 = y0 + glyph.y1 as f32 - glyph.y0 as f32;

                // If either corner of the glyph will end up out of bounds, drop it.
                if let Some(b) = bounds {
                    let rect = DeviceRect {
                        min: DevicePoint::new(x0, y0),
                        max: DevicePoint::new(x1, y1),
                    };
                    if !b.contains_box(&rect) {
                        continue;
                    }
                }

                let s0 = glyph.x0 as f32 * ipw;
                let t0 = glyph.y0 as f32 * iph;
                let s1 = glyph.x1 as f32 * ipw;
                let t1 = glyph.y1 as f32 * iph;

                x_start += glyph.xa;

                let vertex_count = self.font_vertices.len() as u32;

                self.font_vertices
                    .push(DebugFontVertex::new(x0, y0, s0, t0, color));
                self.font_vertices
                    .push(DebugFontVertex::new(x1, y0, s1, t0, color));
                self.font_vertices
                    .push(DebugFontVertex::new(x0, y1, s0, t1, color));
                self.font_vertices
                    .push(DebugFontVertex::new(x1, y1, s1, t1, color));

                self.font_indices.push(vertex_count + 0);
                self.font_indices.push(vertex_count + 1);
                self.font_indices.push(vertex_count + 2);
                self.font_indices.push(vertex_count + 2);
                self.font_indices.push(vertex_count + 1);
                self.font_indices.push(vertex_count + 3);

                min_x = min_x.min(x0);
                max_x = max_x.max(x1);
                min_y = min_y.min(y0);
                max_y = max_y.max(y1);
            }
        }

        Rect::new(
            Point2D::new(min_x, min_y),
            Size2D::new(max_x - min_x, max_y - min_y),
        )
    }

    pub fn add_quad(
        &mut self,
        x0: f32,
        y0: f32,
        x1: f32,
        y1: f32,
        color_top: ColorU,
        color_bottom: ColorU,
    ) {
        let vertex_count = self.tri_vertices.len() as u32;

        self.tri_vertices
            .push(DebugColorVertex::new(x0, y0, color_top));
        self.tri_vertices
            .push(DebugColorVertex::new(x1, y0, color_top));
        self.tri_vertices
            .push(DebugColorVertex::new(x0, y1, color_bottom));
        self.tri_vertices
            .push(DebugColorVertex::new(x1, y1, color_bottom));

        self.tri_indices.push(vertex_count + 0);
        self.tri_indices.push(vertex_count + 1);
        self.tri_indices.push(vertex_count + 2);
        self.tri_indices.push(vertex_count + 2);
        self.tri_indices.push(vertex_count + 1);
        self.tri_indices.push(vertex_count + 3);
    }

    #[allow(dead_code)]
    pub fn add_line(&mut self, x0: i32, y0: i32, color0: ColorU, x1: i32, y1: i32, color1: ColorU) {
        self.line_vertices
            .push(DebugColorVertex::new(x0 as f32, y0 as f32, color0));
        self.line_vertices
            .push(DebugColorVertex::new(x1 as f32, y1 as f32, color1));
    }


    pub fn add_rect(&mut self, rect: &DeviceIntRect, color: ColorU) {
        let p0 = rect.min;
        let p1 = rect.max;
        self.add_line(p0.x, p0.y, color, p1.x, p0.y, color);
        self.add_line(p1.x, p0.y, color, p1.x, p1.y, color);
        self.add_line(p1.x, p1.y, color, p0.x, p1.y, color);
        self.add_line(p0.x, p1.y, color, p0.x, p0.y, color);
    }

    pub fn render(
        &mut self,
        device: &mut Device,
        viewport_size: Option<DeviceIntSize>,
        scale: f32,
        surface_origin_is_top_left: bool,
    ) {
        if let Some(viewport_size) = viewport_size {
            device.disable_depth();
            device.set_blend(true);
            device.set_blend_mode_premultiplied_alpha();

            let (bottom, top) = if surface_origin_is_top_left {
                (0.0, viewport_size.height as f32 * scale)
            } else {
                (viewport_size.height as f32 * scale, 0.0)
            };

            let projection = Transform3D::ortho(
                0.0,
                viewport_size.width as f32 * scale,
                bottom,
                top,
                device.ortho_near_plane(),
                device.ortho_far_plane(),
            );

            // Triangles
            if !self.tri_vertices.is_empty() {
                device.bind_program(&self.color_program);
                device.set_uniforms(&self.color_program, &projection);
                device.bind_vao(&self.tri_vao);
                device.update_vao_indices(&self.tri_vao, &self.tri_indices, VertexUsageHint::Dynamic);
                device.update_vao_main_vertices(
                    &self.tri_vao,
                    &self.tri_vertices,
                    VertexUsageHint::Dynamic,
                );
                device.draw_triangles_u32(0, self.tri_indices.len() as i32);
            }

            // Lines
            if !self.line_vertices.is_empty() {
                device.bind_program(&self.color_program);
                device.set_uniforms(&self.color_program, &projection);
                device.bind_vao(&self.line_vao);
                device.update_vao_main_vertices(
                    &self.line_vao,
                    &self.line_vertices,
                    VertexUsageHint::Dynamic,
                );
                device.draw_nonindexed_lines(0, self.line_vertices.len() as i32);
            }

            // Glyph
            if !self.font_indices.is_empty() {
                device.bind_program(&self.font_program);
                device.set_uniforms(&self.font_program, &projection);
                device.bind_texture(DebugSampler::Font, &self.font_texture, Swizzle::default());
                device.bind_vao(&self.font_vao);
                device.update_vao_indices(&self.font_vao, &self.font_indices, VertexUsageHint::Dynamic);
                device.update_vao_main_vertices(
                    &self.font_vao,
                    &self.font_vertices,
                    VertexUsageHint::Dynamic,
                );
                device.draw_triangles_u32(0, self.font_indices.len() as i32);
            }
        }

        self.font_indices.clear();
        self.font_vertices.clear();
        self.line_vertices.clear();
        self.tri_vertices.clear();
        self.tri_indices.clear();
    }
}

pub struct LazyInitializedDebugRenderer {
    debug_renderer: Option<DebugRenderer>,
    failed: bool,
}

impl LazyInitializedDebugRenderer {
    pub fn new() -> Self {
        Self {
            debug_renderer: None,
            failed: false,
        }
    }

    pub fn get_mut<'a>(&'a mut self, device: &mut Device) -> Option<&'a mut DebugRenderer> {
        if self.failed {
            return None;
        }
        if self.debug_renderer.is_none() {
            match DebugRenderer::new(device) {
                Ok(renderer) => { self.debug_renderer = Some(renderer); }
                Err(_) => {
                    // The shader compilation code already logs errors.
                    self.failed = true;
                }
            }
        }

        self.debug_renderer.as_mut()
    }

    /// Returns mut ref to `debug::DebugRenderer` if one already exists, otherwise returns `None`.
    pub fn try_get_mut<'a>(&'a mut self) -> Option<&'a mut DebugRenderer> {
        self.debug_renderer.as_mut()
    }

    pub fn deinit(self, device: &mut Device) {
        if let Some(debug_renderer) = self.debug_renderer {
            debug_renderer.deinit(device);
        }
    }
}
