use super::*;

use std::mem::transmute;

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum EnableOption {
    Blend,
    ColorLogicOperation,
    CullFace,
    DepthClamp,
    DepthTest,
    Dither,
    FramebufferSRGB,
    LineSmooth,
    Multisample,
    SampleShading,
    SampleMask,
    ScissorTest,
    StencilTest,
    TextureCubeMapSeamless,
    ProgramPointSize,
}

impl From<EnableOption> for u32 {
    fn from(option: EnableOption) -> Self {
        match option {
            EnableOption::Blend => gl::BLEND,
            EnableOption::ColorLogicOperation => gl::COLOR_LOGIC_OP,
            EnableOption::CullFace => gl::CULL_FACE,
            EnableOption::DepthClamp => gl::DEPTH_CLAMP,
            EnableOption::DepthTest => gl::DEPTH_TEST,
            EnableOption::Dither => gl::DITHER,
            EnableOption::FramebufferSRGB => gl::FRAMEBUFFER_SRGB,
            EnableOption::LineSmooth => gl::LINE_SMOOTH,
            EnableOption::Multisample => gl::MULTISAMPLE,
            EnableOption::SampleShading => gl::SAMPLE_SHADING,
            EnableOption::SampleMask => gl::SAMPLE_MASK,
            EnableOption::ScissorTest => gl::SCISSOR_TEST,
            EnableOption::StencilTest => gl::STENCIL_TEST,
            EnableOption::TextureCubeMapSeamless => gl::TEXTURE_CUBE_MAP_SEAMLESS,
            EnableOption::ProgramPointSize => gl::PROGRAM_POINT_SIZE,
        }
    }
}

impl From<EnableOption> for usize {
    fn from(option: EnableOption) -> Self {
        match option {
            EnableOption::Blend => 0,
            EnableOption::ColorLogicOperation => 1,
            EnableOption::CullFace => 2,
            EnableOption::DepthClamp => 3,
            EnableOption::DepthTest => 4,
            EnableOption::Dither => 5,
            EnableOption::FramebufferSRGB => 6,
            EnableOption::LineSmooth => 7,
            EnableOption::Multisample => 8,
            EnableOption::SampleShading => 9,
            EnableOption::SampleMask => 10,
            EnableOption::ScissorTest => 11,
            EnableOption::StencilTest => 12,
            EnableOption::TextureCubeMapSeamless => 13,
            EnableOption::ProgramPointSize => 14,
        }
    }
}

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum StencilTest {
    /// The stencil test always passes.
    AlwaysPass,

    /// The stencil test always fails.
    AlwaysFail,

    /// `(ref & mask) < (stencil & mask)`
    Less {
        /// The reference value that is and'ed with the mask clamped to the
        /// range [0, 2^n - 1], where *n* is the number of bitplanes in the stencil buffer
        reference: i32,
        /// The mask that is and'ed with the stencil buffer
        mask: u32,
    },

    /// `(ref & mask) <= (stencil & mask)`
    LessOrEqual {
        /// The reference value that is and'ed with the mask clamped to the
        /// range [0, 2^n - 1], where *n* is the number of bitplanes in the stencil buffer
        reference: i32,
        /// The mask that is and'ed with the stencil buffer
        mask: u32,
    },

    /// `(ref & mask) > (stencil & mask)`
    Greater {
        /// The reference value that is and'ed with the mask clamped to the
        /// range [0, 2^n - 1], where *n* is the number of bitplanes in the stencil buffer
        reference: i32,
        /// The mask that is and'ed with the stencil buffer
        mask: u32,
    },

    /// `(ref & mask) >= (stencil & mask)`
    GreaterOrEqual {
        /// The reference value that is and'ed with the mask clamped to the
        /// range [0, 2^n - 1], where *n* is the number of bitplanes in the stencil buffer
        reference: i32,
        /// The mask that is and'ed with the stencil buffer
        mask: u32,
    },

    /// `(ref & mask) == (stencil & mask)`
    Equal {
        /// The reference value that is and'ed with the mask clamped to the
        /// range [0, 2^n - 1], where *n* is the number of bitplanes in the stencil buffer
        reference: i32,
        /// The mask that is and'ed with the stencil buffer
        mask: u32,
    },

    /// `(ref & mask) != (stencil & mask)`
    NotEqual {
        /// The reference value that is and'ed with the mask clamped to the
        /// range [0, 2^n - 1], where *n* is the number of bitplanes in the stencil buffer
        reference: i32,
        /// The mask that is and'ed with the stencil buffer
        mask: u32,
    },
}

impl StencilTest {
    pub fn reference(&self) -> i32 {
        match *self {
            StencilTest::AlwaysPass | StencilTest::AlwaysFail => 0,
            StencilTest::Less { reference, mask: _ } => reference,
            StencilTest::LessOrEqual { reference, mask: _ } => reference,
            StencilTest::Greater { reference, mask: _ } => reference,
            StencilTest::GreaterOrEqual { reference, mask: _ } => reference,
            StencilTest::Equal { reference, mask: _ } => reference,
            StencilTest::NotEqual { reference, mask: _ } => reference,
        }
    }

    pub fn mask(&self) -> u32 {
        match *self {
            StencilTest::AlwaysPass | StencilTest::AlwaysFail => 0,
            StencilTest::Less { reference: _, mask } => mask,
            StencilTest::LessOrEqual { reference: _, mask } => mask,
            StencilTest::Greater { reference: _, mask } => mask,
            StencilTest::GreaterOrEqual { reference: _, mask } => mask,
            StencilTest::Equal { reference: _, mask } => mask,
            StencilTest::NotEqual { reference: _, mask } => mask,
        }
    }
}

impl Default for StencilTest {
    fn default() -> StencilTest {
        StencilTest::AlwaysPass
    }
}


#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DepthTest {
    /// Never replace the target pixel
    Ignore,

    /// Always replace the target pixel
    ///
    /// This is the default mode
    Overwrite,

    /// Replace if the z-value of the source is less than the destination
    Less,

    /// Replace if the z-value of the source is equal to the destination
    Equal,

    /// Replace if the z-value of the source is less than, or equal to the destination
    LessOrEqual,

    /// Replace if the z-value of the source is more than the destination
    Greater,

    /// Replace if the z-value of the source is different than the destination.
    NotEqual,

    /// Replace if the z-value of the source is more than, or equal to the destination.
    GreaterOrEqual,
}

impl From<DepthTest> for gl::types::GLenum {
    fn from(depth_test: DepthTest) -> Self {
        match depth_test {
            DepthTest::Ignore => gl::NEVER,
            DepthTest::Overwrite => gl::ALWAYS,
            DepthTest::Less => gl::LESS,
            DepthTest::LessOrEqual => gl::LEQUAL,
            DepthTest::Greater => gl::GREATER,
            DepthTest::GreaterOrEqual => gl::GEQUAL,
            DepthTest::Equal => gl::EQUAL,
            DepthTest::NotEqual => gl::NOTEQUAL,
        }
    }
}

impl Default for DepthTest {
    fn default() -> DepthTest {
        DepthTest::Less
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DepthMask {
    Enabled,
    Disabled,
}

impl From<DepthMask> for bool {
    fn from(mask: DepthMask) -> Self {
        match mask {
            DepthMask::Enabled => true,
            DepthMask::Disabled => false,
        }
    }
}

impl From<DepthMask> for u8 {
    fn from(mask: DepthMask) -> Self {
        match mask {
            DepthMask::Enabled => 1,
            DepthMask::Disabled => 0,
        }
    }
}

impl Default for DepthMask {
    fn default() -> DepthMask {
        DepthMask::Enabled
    }
}

#[derive(Clone, Copy, Debug)]
pub struct DepthRange {
    pub near: f64,
    pub far: f64,
}

impl PartialEq for DepthRange {
    fn eq(&self, other: &DepthRange) -> bool {
        unsafe {
            transmute::<f64, u64>(self.near) == transmute::<f64, u64>(other.near) &&
                transmute::<f64, u64>(self.far) == transmute::<f64, u64>(other.far)
        }
    }
}

impl Eq for DepthRange {}

impl Default for DepthRange {
    fn default() -> DepthRange {
        DepthRange {
            near: 0.0,
            far: 1.0,
        }
    }
}

/// Bitwise operation between incoming pixel S and framebuffer pixel D
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum LogicOperation {
    /// Does nothing
    NoOperation,

    /// Clears the pixel to value 0
    Clear,

    /// Sets the pixel value to 0
    Set,

    /// Copies the incoming pixel value S into the framebuffer
    CopyIncoming,

    /// Copies the value ~S
    CopyIncomingInverted,

    /// Result: ~D
    Invert,

    /// Result: S & D
    And,

    /// Result: ~(S & D)
    NotAnd,

    /// Result: S | D
    Or,

    /// Result:  ~(S | D)
    Nor,

    /// Result: S ^ D
    Xor,

    /// Result :~(S ^ D)
    Equivalent,

    /// Result: S | ~D
    AndReverse,

    /// Result: ~S & D
    AndInverted,

    /// Result: S | ~D
    OrReverse,

    /// Result: ~S | D
    OrInverted,
}

impl From<LogicOperation> for gl::types::GLenum {
    fn from(logic_operation: LogicOperation) -> Self {
        match logic_operation {
            LogicOperation::NoOperation => gl::NOOP,
            LogicOperation::Clear => gl::CLEAR,
            LogicOperation::Set => gl::SET,
            LogicOperation::CopyIncoming => gl::COPY,
            LogicOperation::CopyIncomingInverted => gl::COPY_INVERTED,
            LogicOperation::Invert => gl::INVERT,
            LogicOperation::And => gl::AND,
            LogicOperation::NotAnd => gl::NAND,
            LogicOperation::Or => gl::OR,
            LogicOperation::Nor => gl::NOR,
            LogicOperation::Xor => gl::XOR,
            LogicOperation::Equivalent => gl::EQUIV,
            LogicOperation::AndReverse => gl::AND_REVERSE,
            LogicOperation::AndInverted => gl::AND_INVERTED,
            LogicOperation::OrReverse => gl::OR_REVERSE,
            LogicOperation::OrInverted => gl::OR_INVERTED,
        }
    }
}

impl Default for LogicOperation {
    fn default() -> LogicOperation {
        LogicOperation::NoOperation
    }
}

/// Equation that the GPU will use for blending.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BlendingEquation {
    /// For each individual component (red, green, blue, and alpha), the minimum value is chosen
    /// between the source and the destination.
    ///
    /// For example writing `(0.5, 0.9, 0.4, 0.2)` over `(0.9, 0.1, 0.4, 0.3)` will
    /// result in `(0.5, 0.1, 0.4, 0.2)`.
    Min,

    /// For each individual component (red, green, blue, and alpha), the maximum value is chosen
    /// between the source and the destination.
    ///
    /// For example writing `(0.5, 0.9, 0.4, 0.2)` over `(0.9, 0.1, 0.4, 0.3)` will
    /// result in `(0.9, 0.9, 0.4, 0.3)`.
    Max,

    /// For each individual component (red, green, blue, and alpha), a weighted addition
    /// between the source and the destination.
    ///
    /// The result is equal to `source_component * source_factor + dest_component * dest_factor`,
    /// where `source_factor` and `dest_factor` are the values of `source` and `destination` of
    /// this enum.
    Addition,

    /// For each individual component (red, green, blue, and alpha), a weighted substraction
    /// of the source by the destination.
    ///
    /// The result is equal to `source_component * source_factor - dest_component * dest_factor`,
    /// where `source_factor` and `dest_factor` are the values of `source` and `destination` of
    /// this enum.
    Subtraction,

    /// For each individual component (red, green, blue, and alpha), a weighted substraction
    /// of the destination by the source.
    ///
    /// The result is equal to `-source_component * source_factor + dest_component * dest_factor`,
    /// where `source_factor` and `dest_factor` are the values of `source` and `destination` of
    /// this enum.
    ReverseSubtraction,
}

impl From<BlendingEquation> for gl::types::GLenum {
    fn from(function: BlendingEquation) -> Self {
        match function {
            BlendingEquation::Min => gl::MIN,
            BlendingEquation::Max => gl::MAX,
            BlendingEquation::Addition => gl::FUNC_ADD,
            BlendingEquation::Subtraction => gl::FUNC_SUBTRACT,
            BlendingEquation::ReverseSubtraction => gl::FUNC_REVERSE_SUBTRACT,
        }
    }
}

impl Default for BlendingEquation {
    fn default() -> BlendingEquation {
        BlendingEquation::Addition
    }
}

/// Indicates which value to multiply each component with.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LinearBlendingFactor {
    /// Multiply the source or destination component by zero, which always
    /// gives `0.0`.
    Zero,

    /// Multiply the source or destination component by one, which always
    /// gives you the original value.
    One,

    /// Multiply the source or destination component by its corresponding value
    /// in the source.
    ///
    /// If you apply this to the source components, you get the values squared.
    SourceColor,

    /// Equivalent to `1 - SourceColor`.
    OneMinusSourceColor,

    /// Multiply the source or destination component by its corresponding value
    /// in the destination.
    ///
    /// If you apply this to the destination components, you get the values squared.
    DestinationColor,

    /// Equivalent to `1 - DestinationColor`.
    OneMinusDestinationColor,

    /// Multiply the source or destination component by the alpha value of the source.
    SourceAlpha,

    /// Multiply the source or destination component by the smallest value of
    /// `SourceAlpha` and `1 - DestinationAlpha`.
    SourceAlphaSaturate,

    /// Multiply the source or destination component by `1.0` minus the alpha value of the source.
    OneMinusSourceAlpha,

    /// Multiply the source or destination component by the alpha value of the destination.
    DestinationAlpha,

    /// Multiply the source or destination component by `1.0` minus the alpha value of the
    /// destination.
    OneMinusDestinationAlpha,

    /// Multiply the source or destination component by the corresponding value
    /// in `Blend::const_value`.
    ConstantColor,

    /// Multiply the source or destination compoent by `1.0` minus the corresponding
    /// value in `Blend::const_value`.
    OneMinusConstantColor,

    /// Multiply the source or destination component by the alpha value of `Blend::const_value`.
    ConstantAlpha,

    /// Multiply the source or destination componet by `1.0` minus the alpha value of
    /// `Blend::const_value`.
    OneMinusConstantAlpha,
}

impl From<LinearBlendingFactor> for gl::types::GLenum {
    fn from(factor: LinearBlendingFactor) -> Self {
        match factor {
            LinearBlendingFactor::Zero => gl::ZERO,
            LinearBlendingFactor::One => gl::ONE,
            LinearBlendingFactor::SourceColor => gl::SRC_COLOR,
            LinearBlendingFactor::OneMinusSourceColor => gl::ONE_MINUS_SRC_COLOR,
            LinearBlendingFactor::DestinationColor => gl::DST_COLOR,
            LinearBlendingFactor::OneMinusDestinationColor => gl::ONE_MINUS_DST_COLOR,
            LinearBlendingFactor::SourceAlpha => gl::SRC_ALPHA,
            LinearBlendingFactor::OneMinusSourceAlpha => gl::ONE_MINUS_SRC_ALPHA,
            LinearBlendingFactor::DestinationAlpha => gl::DST_ALPHA,
            LinearBlendingFactor::OneMinusDestinationAlpha => gl::ONE_MINUS_DST_ALPHA,
            LinearBlendingFactor::SourceAlphaSaturate => gl::SRC_ALPHA_SATURATE,
            LinearBlendingFactor::ConstantColor => gl::CONSTANT_COLOR,
            LinearBlendingFactor::OneMinusConstantColor => gl::ONE_MINUS_CONSTANT_COLOR,
            LinearBlendingFactor::ConstantAlpha => gl::CONSTANT_ALPHA,
            LinearBlendingFactor::OneMinusConstantAlpha => gl::ONE_MINUS_CONSTANT_ALPHA,
        }
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FaceOrientation {
    Clockwise,
    CounterClockwise,
}

impl From<FaceOrientation> for gl::types::GLenum {
    fn from(orientation: FaceOrientation) -> Self {
        match orientation {
            FaceOrientation::Clockwise => gl::CW,
            FaceOrientation::CounterClockwise => gl::CCW,
        }
    }
}

impl Default for FaceOrientation {
    fn default() -> FaceOrientation {
        FaceOrientation::CounterClockwise
    }
}

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct Viewport {
    pub x: u32,
    pub y: u32,
    pub width: u32,
    pub height: u32,
}

impl Default for Viewport {
    fn default() -> Viewport {
        Viewport {
            x: 0,
            y: 0,
            width: 0,
            height: 0,
        }
    }
}

#[derive(Debug, Copy, Clone)]
pub struct ClearColor {
    pub r: f32,
    pub g: f32,
    pub b: f32,
    pub a: f32,
}

impl PartialEq for ClearColor {
    fn eq(&self, other: &ClearColor) -> bool {
        unsafe {
            transmute::<f32, u32>(self.r) == transmute::<f32, u32>(other.r) &&
                transmute::<f32, u32>(self.g) == transmute::<f32, u32>(other.g) &&
                transmute::<f32, u32>(self.b) == transmute::<f32, u32>(other.b) &&
                transmute::<f32, u32>(self.a) == transmute::<f32, u32>(other.a)
        }
    }
}

impl Eq for ClearColor {}

impl Default for ClearColor {
    fn default() -> ClearColor {
        ClearColor {
            r: 0.0,
            g: 0.0,
            b: 0.0,
            a: 1.0,
        }
    }
}


pub fn enable(option: EnableOption) {
    unsafe {
        gl::Enable(option.into());
    }
}

pub fn disable(option: EnableOption) {
    unsafe {
        gl::Disable(option.into());
    }
}

pub fn viewport(viewport: Viewport) {
    unsafe {
        gl::Viewport(
            viewport.x as i32,
            viewport.y as i32,
            viewport.width as i32,
            viewport.height as i32,
        );
    }
}

pub fn stencil_func_separate(face: Face, test: StencilTest) {
    unsafe {
        match test {
            StencilTest::AlwaysPass => {
                gl::StencilFuncSeparate(face.into(), gl::ALWAYS, 0, u32::max_value())
            }
            StencilTest::AlwaysFail => {
                gl::StencilFuncSeparate(face.into(), gl::NEVER, 0, u32::max_value())
            }
            StencilTest::Less { reference, mask } => {
                gl::StencilFuncSeparate(face.into(), gl::LESS, reference, mask)
            }
            StencilTest::LessOrEqual { reference, mask } => {
                gl::StencilFuncSeparate(face.into(), gl::LEQUAL, reference, mask)
            }
            StencilTest::Greater { reference, mask } => {
                gl::StencilFuncSeparate(face.into(), gl::GREATER, reference, mask)
            }
            StencilTest::GreaterOrEqual { reference, mask } => {
                gl::StencilFuncSeparate(face.into(), gl::GEQUAL, reference, mask)
            }
            StencilTest::Equal { reference, mask } => {
                gl::StencilFuncSeparate(face.into(), gl::EQUAL, reference, mask)
            }
            StencilTest::NotEqual { reference, mask } => {
                gl::StencilFuncSeparate(face.into(), gl::NOTEQUAL, reference, mask)
            }
        }
    }
}

pub fn depth_function(test: DepthTest) {
    unsafe {
        gl::DepthFunc(test.into());
    }
}

pub fn depth_mask(depth_mask: DepthMask) {
    unsafe {
        gl::DepthMask(depth_mask.into());
    }
}

pub fn depth_range(depth_range: DepthRange) {
    unsafe {
        gl::DepthRange(depth_range.near, depth_range.far);
    }
}

pub fn logic_op(logic_operation: LogicOperation) {
    unsafe {
        gl::LogicOp(logic_operation.into());
    }
}

pub fn scissor(x: u32, y: u32, width: usize, height: usize) {
    unsafe {
        gl::Scissor(x as i32, y as i32, width as i32, height as i32);
    }
}

pub fn blend_color(red: f32, green: f32, blue: f32, alpha: f32) {
    unsafe {
        gl::BlendColor(red, green, blue, alpha);
    }
}

pub fn blend_equation_separate(rgb: BlendingEquation, alpha: BlendingEquation) {
    unsafe {
        gl::BlendEquationSeparate(rgb.into(), alpha.into());
    }
}

pub fn blend_func_separate(
    source_rgb: LinearBlendingFactor,
    destination_rgb: LinearBlendingFactor,
    source_alpha: LinearBlendingFactor,
    destination_alpha: LinearBlendingFactor,
) {
    unsafe {
        gl::BlendFuncSeparate(
            source_rgb.into(),
            destination_rgb.into(),
            source_alpha.into(),
            destination_alpha.into(),
        );
    }
}


pub fn front_face(orientation: FaceOrientation) {
    unsafe {
        gl::FrontFace(orientation.into());
    }
}

pub fn cull_face(face: Face) {
    unsafe {
        gl::CullFace(face.into());
    }
}

pub fn clear_color(color: ClearColor) {
    unsafe {
        gl::ClearColor(color.r, color.g, color.b, color.a);
    }
}
