//! Properties related to CSS modules.

use crate::dependencies::Location;
use crate::error::{ParserError, PrinterError};
use crate::printer::Printer;
use crate::traits::{Parse, ToCss};
use crate::values::ident::{CustomIdent, CustomIdentList};
use crate::values::string::CowArcStr;
#[cfg(feature = "visitor")]
use crate::visitor::Visit;
use cssparser::*;
use smallvec::SmallVec;

/// A value for the [composes](https://github.com/css-modules/css-modules/#dependencies) property from CSS modules.
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "visitor", derive(Visit))]
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub struct Composes<'i> {
  /// A list of class names to compose.
  #[cfg_attr(feature = "serde", serde(borrow))]
  pub names: CustomIdentList<'i>,
  /// Where the class names are composed from.
  pub from: Option<Specifier<'i>>,
  /// The source location of the `composes` property.
  pub loc: Location,
}

/// Defines where the class names referenced in the `composes` property are located.
///
/// See [Composes](Composes).
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "visitor", derive(Visit))]
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
#[cfg_attr(
  feature = "serde",
  derive(serde::Serialize, serde::Deserialize),
  serde(tag = "type", content = "value", rename_all = "kebab-case")
)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub enum Specifier<'i> {
  /// The referenced name is global.
  Global,
  /// The referenced name comes from the specified file.
  #[cfg_attr(feature = "serde", serde(borrow))]
  File(CowArcStr<'i>),
  /// The referenced name comes from a source index (used during bundling).
  SourceIndex(u32),
}

impl<'i> Parse<'i> for Composes<'i> {
  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
    let loc = input.current_source_location();
    let mut names = SmallVec::new();
    while let Ok(name) = input.try_parse(parse_one_ident) {
      names.push(name);
    }

    if names.is_empty() {
      return Err(input.new_custom_error(ParserError::InvalidDeclaration));
    }

    let from = if input.try_parse(|input| input.expect_ident_matching("from")).is_ok() {
      Some(Specifier::parse(input)?)
    } else {
      None
    };

    Ok(Composes {
      names,
      from,
      loc: loc.into(),
    })
  }
}

fn parse_one_ident<'i, 't>(
  input: &mut Parser<'i, 't>,
) -> Result<CustomIdent<'i>, ParseError<'i, ParserError<'i>>> {
  let name = CustomIdent::parse(input)?;
  if name.0.eq_ignore_ascii_case("from") {
    return Err(input.new_error_for_next_token());
  }

  Ok(name)
}

impl ToCss for Composes<'_> {
  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
  where
    W: std::fmt::Write,
  {
    let mut first = true;
    for name in &self.names {
      if first {
        first = false;
      } else {
        dest.write_char(' ')?;
      }
      name.to_css(dest)?;
    }

    if let Some(from) = &self.from {
      dest.write_str(" from ")?;
      from.to_css(dest)?;
    }

    Ok(())
  }
}

impl<'i> Parse<'i> for Specifier<'i> {
  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
    if let Ok(file) = input.try_parse(|input| input.expect_string_cloned()) {
      Ok(Specifier::File(file.into()))
    } else {
      input.expect_ident_matching("global")?;
      Ok(Specifier::Global)
    }
  }
}

impl<'i> ToCss for Specifier<'i> {
  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
  where
    W: std::fmt::Write,
  {
    match self {
      Specifier::Global => dest.write_str("global")?,
      Specifier::File(file) => serialize_string(&file, dest)?,
      Specifier::SourceIndex(..) => {}
    }
    Ok(())
  }
}
