use std::{
    collections::HashSet,
    env,
    ffi::OsStr,
    fs,
    path::{Path, PathBuf},
    str::FromStr,
};
use anyhow::{anyhow, Context};
use cssparser::{Parser, ParserInput, Token};
use derive_more::Into;
use grass::InputSyntax;
#[cfg_attr(feature = "css-transpile", path = "transpile-enabled.rs")]
#[cfg_attr(not(feature = "css-transpile"), path = "transpile-disabled.rs")]
mod transpile;
use thiserror::Error;
pub use transpile::Version;
pub struct NameMapping {
    pub plain: String,
    pub mangled: String,
}
#[derive(Into)]
pub struct CssSyntax(InputSyntax);
impl Default for CssSyntax {
    fn default() -> Self {
        Self(InputSyntax::Css)
    }
}
impl FromStr for CssSyntax {
    type Err = ();
    fn from_str(syntax: &str) -> Result<Self, Self::Err> {
        let syntax = match syntax {
            "css" => InputSyntax::Css,
            "scss" => InputSyntax::Scss,
            "sass" => InputSyntax::Sass,
            _ => return Err(()),
        };
        Ok(Self(syntax))
    }
}
impl CssSyntax {
    fn from_path(path: impl AsRef<Path>) -> Self {
        path.as_ref()
            .extension()
            .and_then(OsStr::to_str)
            .and_then(|ext| Self::from_str(ext.to_lowercase().as_str()).ok())
            .unwrap_or_default()
    }
}
#[derive(Debug)]
pub struct Css {
    content: String,
    dependency: Option<String>,
}
impl Css {
    pub fn from_content(content: impl Into<String>, syntax: CssSyntax) -> Result<Self, Error> {
        Ok(Self {
            content: Self::css_content(content.into(), syntax)?,
            dependency: None,
        })
    }
    pub fn from_path(path: impl AsRef<Path>, syntax: Option<CssSyntax>) -> Result<Self, Error> {
        let syntax = syntax.unwrap_or_else(|| CssSyntax::from_path(path.as_ref()));
        const CARGO_MANIFEST_DIR: &str = "CARGO_MANIFEST_DIR";
        let root_dir = env::var(CARGO_MANIFEST_DIR)
            .with_context(|| format!("Couldn't read '{CARGO_MANIFEST_DIR}' variable"))?;
        let path = PathBuf::from(root_dir)
            .join(path)
            .into_os_string()
            .into_string()
            .map_err(|filename| anyhow!("Couldn't convert filename to string: '{filename:?}'"))?;
        Ok(Self {
            content: Self::css_content(
                fs::read_to_string(&path)
                    .with_context(|| format!("Couldn't read file '{path}'"))?,
                syntax,
            )?,
            dependency: Some(path),
        })
    }
    fn css_content(source: String, syntax: CssSyntax) -> Result<String, Error> {
        let syntax = syntax.into();
        let css = if syntax != InputSyntax::Css {
            grass::from_string(source, &grass::Options::default().input_syntax(syntax))
                .context("Error parsing CSS")?
        } else {
            source
        };
        Ok(css)
    }
    pub fn transpile(
        &mut self,
        validate: bool,
        transpile: Option<Transpile>,
    ) -> Result<Option<Vec<NameMapping>>, TranspileError> {
        if validate || transpile.is_some() {
            transpile::transpile(self, validate, transpile)
        } else {
            Ok(None)
        }
    }
    pub fn dependency(&self) -> Option<&str> {
        self.dependency.as_deref()
    }
    pub fn content(&self) -> &str {
        &self.content
    }
    pub fn class_names(&self) -> impl Iterator<Item = String> {
        let mut parser_input = ParserInput::new(&self.content);
        let mut input = Parser::new(&mut parser_input);
        let mut classes = HashSet::new();
        let mut prev_dot = false;
        while let Ok(token) = input.next_including_whitespace_and_comments() {
            if prev_dot {
                if let Token::Ident(class) = token {
                    classes.insert(class.to_string());
                }
            }
            prev_dot = matches!(token, Token::Delim('.'));
        }
        classes.into_iter()
    }
    pub fn variable_names(&self) -> impl Iterator<Item = String> {
        let mut parser_input = ParserInput::new(&self.content);
        let mut input = Parser::new(&mut parser_input);
        let mut variables = HashSet::new();
        let mut tokens = Vec::new();
        flattened_tokens(&mut tokens, &mut input);
        for token in tokens {
            if let Token::Ident(ident) = token {
                if let Some(var) = ident.strip_prefix("--") {
                    variables.insert(var.to_string());
                }
            }
        }
        variables.into_iter()
    }
}
pub struct Transpile {
    pub minify: bool,
    pub pretty: bool,
    pub modules: bool,
    pub nesting: bool,
    pub browsers: Option<Browsers>,
}
#[derive(Default)]
pub struct Browsers {
    pub android: Option<Version>,
    pub chrome: Option<Version>,
    pub edge: Option<Version>,
    pub firefox: Option<Version>,
    pub ie: Option<Version>,
    pub ios_saf: Option<Version>,
    pub opera: Option<Version>,
    pub safari: Option<Version>,
    pub samsung: Option<Version>,
}
#[derive(Error, Debug)]
#[error(transparent)]
pub struct Error(#[from] anyhow::Error);
#[derive(Error, Debug)]
pub enum TranspileError {
    #[error("Transpilation requires `css-transpile` feature")]
    Disabled,
    #[error("Transpile failed: {0}")]
    Failed(#[from] Error),
}
impl From<anyhow::Error> for TranspileError {
    fn from(value: anyhow::Error) -> Self {
        Self::Failed(value.into())
    }
}
fn flattened_tokens<'i>(tokens: &mut Vec<Token<'i>>, input: &mut Parser<'i, '_>) {
    while let Ok(token) = input.next_including_whitespace_and_comments() {
        tokens.push(token.clone());
        match token {
            Token::ParenthesisBlock
            | Token::CurlyBracketBlock
            | Token::SquareBracketBlock
            | Token::Function(_) => {
                input
                    .parse_nested_block(|parser| -> Result<(), cssparser::ParseError<()>> {
                        flattened_tokens(tokens, parser);
                        Ok(())
                    })
                    .unwrap();
            }
            _ => (),
        }
    }
}