use std::collections::hash_map::DefaultHasher;
use std::fs::read_to_string;
use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf};

use crate::shader_preprocessor::{Error as ShaderPreprocessorError, ShaderPreprocessor};

/// Shader program
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
pub struct Program
{
    shaders: Vec<Shader>,
}

impl Program
{
    #[must_use]
    pub fn new() -> Self
    {
        Self { shaders: Vec::new() }
    }

    pub fn push_shader(&mut self, shader: Shader)
    {
        self.shaders.push(shader);
    }

    pub fn append_shaders(&mut self, shaders: impl IntoIterator<Item = Shader>)
    {
        self.shaders.extend(shaders);
    }

    #[must_use]
    pub fn shaders(&self) -> &[Shader]
    {
        &self.shaders
    }

    pub(crate) fn u64_hash(&self) -> u64
    {
        let mut hasher = DefaultHasher::new();

        self.hash(&mut hasher);

        hasher.finish()
    }
}

#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub struct Shader
{
    kind: Kind,
    source: String,
    file: PathBuf,
}

impl Shader
{
    /// Reads a shader from the specified source file.
    ///
    /// # Errors
    /// Will return `Err` if:
    /// - Reading the file fails
    /// - The shader source is not ASCII
    pub fn read_shader_file(kind: Kind, shader_file: &Path) -> Result<Self, Error>
    {
        let source = read_to_string(shader_file).map_err(|err| Error::ReadFailed {
            source: err,
            shader_file: shader_file.to_path_buf(),
        })?;

        if !source.is_ascii() {
            return Err(Error::SourceNotAscii);
        }

        Ok(Self {
            kind,
            source,
            file: shader_file.to_path_buf(),
        })
    }

    /// Preprocesses the shaders.
    ///
    /// # Errors
    /// Returns `Err` if preprocessing fails.
    pub fn preprocess(self) -> Result<Self, Error>
    {
        let shader_preprocessor = ShaderPreprocessor::new(
            self.file
                .parent()
                .ok_or(Error::SourcePathHasNoParent)?
                .to_path_buf(),
        );

        let source_preprocessed = shader_preprocessor
            .preprocess(self.source, &self.file)
            .map_err(|err| Error::PreprocessFailed {
                source: err,
                shader_file: self.file.clone(),
            })?;

        Ok(Self {
            kind: self.kind,
            source: source_preprocessed,
            file: self.file.clone(),
        })
    }

    #[must_use]
    pub fn kind(&self) -> Kind
    {
        self.kind
    }

    #[must_use]
    pub fn source(&self) -> &str
    {
        &self.source
    }
}

/// Shader kind.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Kind
{
    Vertex,
    Fragment,
}

/// Shader error
#[derive(Debug, thiserror::Error)]
pub enum Error
{
    #[error("Failed to read shader {}", shader_file.display())]
    ReadFailed
    {
        #[source]
        source: std::io::Error,
        shader_file: PathBuf,
    },

    #[error("Shader source is not ASCII")]
    SourceNotAscii,

    #[error("Failed to preprocess shader {}", shader_file.display())]
    PreprocessFailed
    {
        #[source]
        source: ShaderPreprocessorError,
        shader_file: PathBuf,
    },

    #[error("Shader source path has no parent")]
    SourcePathHasNoParent,
}