//! CSS custom properties and unparsed token values.

use crate::error::{ParserError, PrinterError, PrinterErrorKind};
use crate::macros::enum_property;
use crate::prefixes::Feature;
use crate::printer::Printer;
use crate::properties::PropertyId;
use crate::rules::supports::SupportsCondition;
use crate::stylesheet::ParserOptions;
use crate::targets::{should_compile, Features, Targets};
use crate::traits::{Parse, ParseWithOptions, ToCss};
use crate::values::angle::Angle;
use crate::values::color::{
  parse_hsl_hwb_components, parse_rgb_components, ColorFallbackKind, ComponentParser, CssColor, LightDarkColor,
  HSL, RGB, RGBA,
};
use crate::values::ident::{CustomIdent, DashedIdent, DashedIdentReference, Ident};
use crate::values::length::{serialize_dimension, LengthValue};
use crate::values::number::CSSInteger;
use crate::values::percentage::Percentage;
use crate::values::resolution::Resolution;
use crate::values::string::CowArcStr;
use crate::values::time::Time;
use crate::values::url::Url;
#[cfg(feature = "visitor")]
use crate::visitor::Visit;
use cssparser::color::parse_hash_color;
use cssparser::*;

use super::AnimationName;
#[cfg(feature = "serde")]
use crate::serialization::ValueWrapper;

/// A CSS custom property, representing any unknown property.
#[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 CustomProperty<'i> {
  /// The name of the property.
  #[cfg_attr(feature = "serde", serde(borrow))]
  pub name: CustomPropertyName<'i>,
  /// The property value, stored as a raw token list.
  pub value: TokenList<'i>,
}

impl<'i> CustomProperty<'i> {
  /// Parses a custom property with the given name.
  pub fn parse<'t>(
    name: CustomPropertyName<'i>,
    input: &mut Parser<'i, 't>,
    options: &ParserOptions<'_, 'i>,
  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
    let value = input.parse_until_before(Delimiter::Bang | Delimiter::Semicolon, |input| {
      TokenList::parse(input, options, 0)
    })?;
    Ok(CustomProperty { name, value })
  }
}

/// A CSS custom property name.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "visitor", derive(Visit))]
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
#[cfg_attr(feature = "serde", derive(serde::Serialize), serde(untagged))]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub enum CustomPropertyName<'i> {
  /// An author-defined CSS custom property.
  #[cfg_attr(feature = "serde", serde(borrow))]
  Custom(DashedIdent<'i>),
  /// An unknown CSS property.
  Unknown(Ident<'i>),
}

impl<'i> From<CowArcStr<'i>> for CustomPropertyName<'i> {
  fn from(name: CowArcStr<'i>) -> Self {
    if name.starts_with("--") {
      CustomPropertyName::Custom(DashedIdent(name))
    } else {
      CustomPropertyName::Unknown(Ident(name))
    }
  }
}

impl<'i> From<CowRcStr<'i>> for CustomPropertyName<'i> {
  fn from(name: CowRcStr<'i>) -> Self {
    CustomPropertyName::from(CowArcStr::from(name))
  }
}

impl<'i> AsRef<str> for CustomPropertyName<'i> {
  #[inline]
  fn as_ref(&self) -> &str {
    match self {
      CustomPropertyName::Custom(c) => c.as_ref(),
      CustomPropertyName::Unknown(u) => u.as_ref(),
    }
  }
}

impl<'i> ToCss for CustomPropertyName<'i> {
  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
  where
    W: std::fmt::Write,
  {
    match self {
      CustomPropertyName::Custom(c) => c.to_css(dest),
      CustomPropertyName::Unknown(u) => u.to_css(dest),
    }
  }
}

#[cfg(feature = "serde")]
#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
impl<'i, 'de: 'i> serde::Deserialize<'de> for CustomPropertyName<'i> {
  fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
  where
    D: serde::Deserializer<'de>,
  {
    let name = CowArcStr::deserialize(deserializer)?;
    Ok(name.into())
  }
}

/// A known property with an unparsed value.
///
/// This type is used when the value of a known property could not
/// be parsed, e.g. in the case css `var()` references are encountered.
/// In this case, the raw tokens are stored instead.
#[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(rename_all = "camelCase")
)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub struct UnparsedProperty<'i> {
  /// The id of the property.
  pub property_id: PropertyId<'i>,
  /// The property value, stored as a raw token list.
  #[cfg_attr(feature = "serde", serde(borrow))]
  pub value: TokenList<'i>,
}

impl<'i> UnparsedProperty<'i> {
  /// Parses a property with the given id as a token list.
  pub fn parse<'t>(
    property_id: PropertyId<'i>,
    input: &mut Parser<'i, 't>,
    options: &ParserOptions<'_, 'i>,
  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
    let value = input.parse_until_before(Delimiter::Bang | Delimiter::Semicolon, |input| {
      TokenList::parse(input, options, 0)
    })?;
    Ok(UnparsedProperty { property_id, value })
  }

  pub(crate) fn get_prefixed(&self, targets: Targets, feature: Feature) -> UnparsedProperty<'i> {
    let mut clone = self.clone();
    let prefix = self.property_id.prefix();
    clone.property_id = clone.property_id.with_prefix(targets.prefixes(prefix.or_none(), feature));
    clone
  }

  /// Returns a new UnparsedProperty with the same value and the given property id.
  pub fn with_property_id(&self, property_id: PropertyId<'i>) -> UnparsedProperty<'i> {
    UnparsedProperty {
      property_id,
      value: self.value.clone(),
    }
  }

  /// Substitutes variables and re-parses the property.
  #[cfg(feature = "substitute_variables")]
  #[cfg_attr(docsrs, doc(cfg(feature = "substitute_variables")))]
  pub fn substitute_variables<'x>(
    mut self,
    vars: &std::collections::HashMap<&str, TokenList<'i>>,
  ) -> Result<super::Property<'x>, ()> {
    use super::Property;
    use crate::stylesheet::PrinterOptions;
    use static_self::IntoOwned;

    // Substitute variables in the token list.
    self.value.substitute_variables(vars);

    // Now stringify and re-parse the property to its fully parsed form.
    // Ideally we'd be able to reuse the tokens rather than printing, but cssparser doesn't provide a way to do that.
    let mut css = String::new();
    let mut dest = Printer::new(&mut css, PrinterOptions::default());
    self.value.to_css(&mut dest, false).unwrap();
    let property =
      Property::parse_string(self.property_id.clone(), &css, ParserOptions::default()).map_err(|_| ())?;
    Ok(property.into_owned())
  }
}

/// A raw list of CSS tokens, with embedded parsed values.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "visitor", derive(Visit), visit(visit_token_list, TOKENS))]
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(transparent))]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub struct TokenList<'i>(#[cfg_attr(feature = "serde", serde(borrow))] pub Vec<TokenOrValue<'i>>);

/// A raw CSS token, or a parsed value.
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "visitor", derive(Visit), visit(visit_token, TOKENS), visit_types(TOKENS | COLORS | URLS | VARIABLES | ENVIRONMENT_VARIABLES | FUNCTIONS | LENGTHS | ANGLES | TIMES | RESOLUTIONS | DASHED_IDENTS))]
#[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 TokenOrValue<'i> {
  /// A token.
  #[cfg_attr(feature = "serde", serde(borrow))]
  Token(Token<'i>),
  /// A parsed CSS color.
  Color(CssColor),
  /// A color with unresolved components.
  UnresolvedColor(UnresolvedColor<'i>),
  /// A parsed CSS url.
  Url(Url<'i>),
  /// A CSS variable reference.
  Var(Variable<'i>),
  /// A CSS environment variable reference.
  Env(EnvironmentVariable<'i>),
  /// A custom CSS function.
  Function(Function<'i>),
  /// A length.
  Length(LengthValue),
  /// An angle.
  Angle(Angle),
  /// A time.
  Time(Time),
  /// A resolution.
  Resolution(Resolution),
  /// A dashed ident.
  DashedIdent(DashedIdent<'i>),
  /// An animation name.
  AnimationName(AnimationName<'i>),
}

impl<'i> From<Token<'i>> for TokenOrValue<'i> {
  fn from(token: Token<'i>) -> TokenOrValue<'i> {
    TokenOrValue::Token(token)
  }
}

impl<'i> TokenOrValue<'i> {
  /// Returns whether the token is whitespace.
  pub fn is_whitespace(&self) -> bool {
    matches!(self, TokenOrValue::Token(Token::WhiteSpace(_)))
  }
}

impl<'a> Eq for TokenOrValue<'a> {}

impl<'a> std::hash::Hash for TokenOrValue<'a> {
  fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
    let tag = std::mem::discriminant(self);
    tag.hash(state);
    match self {
      TokenOrValue::Token(t) => t.hash(state),
      _ => {
        // This function is primarily used to deduplicate selectors.
        // Values inside selectors should be exceedingly rare and implementing
        // Hash for them is somewhat complex due to floating point values.
        // For now, we just ignore them, which only means there are more
        // hash collisions. For such a rare case this is probably fine.
      }
    }
  }
}

impl<'i> ParseWithOptions<'i> for TokenList<'i> {
  fn parse_with_options<'t>(
    input: &mut Parser<'i, 't>,
    options: &ParserOptions<'_, 'i>,
  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
    TokenList::parse(input, options, 0)
  }
}

impl<'i> TokenList<'i> {
  pub(crate) fn parse<'t>(
    input: &mut Parser<'i, 't>,
    options: &ParserOptions<'_, 'i>,
    depth: usize,
  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
    let mut tokens = vec![];
    TokenList::parse_into(input, &mut tokens, options, depth)?;

    // Slice off leading and trailing whitespace if there are at least two tokens.
    // If there is only one token, we must preserve it. e.g. `--foo: ;` is valid.
    if tokens.len() >= 2 {
      let mut slice = &tokens[..];
      if matches!(tokens.first(), Some(token) if token.is_whitespace()) {
        slice = &slice[1..];
      }
      if matches!(tokens.last(), Some(token) if token.is_whitespace()) {
        slice = &slice[..slice.len() - 1];
      }
      return Ok(TokenList(slice.to_vec()));
    }

    return Ok(TokenList(tokens));
  }

  pub(crate) fn parse_raw<'t>(
    input: &mut Parser<'i, 't>,
    tokens: &mut Vec<TokenOrValue<'i>>,
    options: &ParserOptions<'_, 'i>,
    depth: usize,
  ) -> Result<(), ParseError<'i, ParserError<'i>>> {
    if depth > 500 {
      return Err(input.new_custom_error(ParserError::MaximumNestingDepth));
    }

    loop {
      let state = input.state();
      match input.next_including_whitespace_and_comments() {
        Ok(token @ &cssparser::Token::ParenthesisBlock)
        | Ok(token @ &cssparser::Token::SquareBracketBlock)
        | Ok(token @ &cssparser::Token::CurlyBracketBlock) => {
          tokens.push(Token::from(token).into());
          let closing_delimiter = match token {
            cssparser::Token::ParenthesisBlock => Token::CloseParenthesis,
            cssparser::Token::SquareBracketBlock => Token::CloseSquareBracket,
            cssparser::Token::CurlyBracketBlock => Token::CloseCurlyBracket,
            _ => unreachable!(),
          };

          input.parse_nested_block(|input| TokenList::parse_raw(input, tokens, options, depth + 1))?;
          tokens.push(closing_delimiter.into());
        }
        Ok(token @ &cssparser::Token::Function(_)) => {
          tokens.push(Token::from(token).into());
          input.parse_nested_block(|input| TokenList::parse_raw(input, tokens, options, depth + 1))?;
          tokens.push(Token::CloseParenthesis.into());
        }
        Ok(token) if token.is_parse_error() => {
          return Err(ParseError {
            kind: ParseErrorKind::Basic(BasicParseErrorKind::UnexpectedToken(token.clone())),
            location: state.source_location(),
          })
        }
        Ok(token) => {
          tokens.push(Token::from(token).into());
        }
        Err(_) => break,
      }
    }

    Ok(())
  }

  fn parse_into<'t>(
    input: &mut Parser<'i, 't>,
    tokens: &mut Vec<TokenOrValue<'i>>,
    options: &ParserOptions<'_, 'i>,
    depth: usize,
  ) -> Result<(), ParseError<'i, ParserError<'i>>> {
    if depth > 500 {
      return Err(input.new_custom_error(ParserError::MaximumNestingDepth));
    }

    let mut last_is_delim = false;
    let mut last_is_whitespace = false;
    loop {
      let state = input.state();
      match input.next_including_whitespace_and_comments() {
        Ok(&cssparser::Token::WhiteSpace(..)) | Ok(&cssparser::Token::Comment(..)) => {
          // Skip whitespace if the last token was a delimiter.
          // Otherwise, replace all whitespace and comments with a single space character.
          if !last_is_delim {
            tokens.push(Token::WhiteSpace(" ".into()).into());
            last_is_whitespace = true;
          }
        }
        Ok(&cssparser::Token::Function(ref f)) => {
          // Attempt to parse embedded color values into hex tokens.
          let f = f.into();
          if let Some(color) = try_parse_color_token(&f, &state, input) {
            tokens.push(TokenOrValue::Color(color));
            last_is_delim = false;
            last_is_whitespace = false;
          } else if let Ok(color) = input.try_parse(|input| UnresolvedColor::parse(&f, input, options)) {
            tokens.push(TokenOrValue::UnresolvedColor(color));
            last_is_delim = true;
            last_is_whitespace = false;
          } else if f == "url" {
            input.reset(&state);
            tokens.push(TokenOrValue::Url(Url::parse(input)?));
            last_is_delim = false;
            last_is_whitespace = false;
          } else if f == "var" {
            let var = input.parse_nested_block(|input| {
              let var = Variable::parse(input, options, depth + 1)?;
              Ok(TokenOrValue::Var(var))
            })?;
            tokens.push(var);
            last_is_delim = true;
            last_is_whitespace = false;
          } else if f == "env" {
            let env = input.parse_nested_block(|input| {
              let env = EnvironmentVariable::parse_nested(input, options, depth + 1)?;
              Ok(TokenOrValue::Env(env))
            })?;
            tokens.push(env);
            last_is_delim = true;
            last_is_whitespace = false;
          } else {
            let arguments = input.parse_nested_block(|input| TokenList::parse(input, options, depth + 1))?;
            tokens.push(TokenOrValue::Function(Function {
              name: Ident(f),
              arguments,
            }));
            last_is_delim = true; // Whitespace is not required after any of these chars.
            last_is_whitespace = false;
          }
        }
        Ok(&cssparser::Token::Hash(ref h)) | Ok(&cssparser::Token::IDHash(ref h)) => {
          if let Ok((r, g, b, a)) = parse_hash_color(h.as_bytes()) {
            tokens.push(TokenOrValue::Color(CssColor::RGBA(RGBA::new(r, g, b, a))));
          } else {
            tokens.push(Token::Hash(h.into()).into());
          }
          last_is_delim = false;
          last_is_whitespace = false;
        }
        Ok(&cssparser::Token::UnquotedUrl(_)) => {
          input.reset(&state);
          tokens.push(TokenOrValue::Url(Url::parse(input)?));
          last_is_delim = false;
          last_is_whitespace = false;
        }
        Ok(&cssparser::Token::Ident(ref name)) if name.starts_with("--") => {
          tokens.push(TokenOrValue::DashedIdent(name.into()));
          last_is_delim = false;
          last_is_whitespace = false;
        }
        Ok(token @ &cssparser::Token::ParenthesisBlock)
        | Ok(token @ &cssparser::Token::SquareBracketBlock)
        | Ok(token @ &cssparser::Token::CurlyBracketBlock) => {
          tokens.push(Token::from(token).into());
          let closing_delimiter = match token {
            cssparser::Token::ParenthesisBlock => Token::CloseParenthesis,
            cssparser::Token::SquareBracketBlock => Token::CloseSquareBracket,
            cssparser::Token::CurlyBracketBlock => Token::CloseCurlyBracket,
            _ => unreachable!(),
          };

          input.parse_nested_block(|input| TokenList::parse_into(input, tokens, options, depth + 1))?;

          tokens.push(closing_delimiter.into());
          last_is_delim = true; // Whitespace is not required after any of these chars.
          last_is_whitespace = false;
        }
        Ok(token @ cssparser::Token::Dimension { .. }) => {
          let value = if let Ok(length) = LengthValue::try_from(token) {
            TokenOrValue::Length(length)
          } else if let Ok(angle) = Angle::try_from(token) {
            TokenOrValue::Angle(angle)
          } else if let Ok(time) = Time::try_from(token) {
            TokenOrValue::Time(time)
          } else if let Ok(resolution) = Resolution::try_from(token) {
            TokenOrValue::Resolution(resolution)
          } else {
            TokenOrValue::Token(token.into())
          };
          tokens.push(value);
          last_is_delim = false;
          last_is_whitespace = false;
        }
        Ok(token) if token.is_parse_error() => {
          return Err(ParseError {
            kind: ParseErrorKind::Basic(BasicParseErrorKind::UnexpectedToken(token.clone())),
            location: state.source_location(),
          })
        }
        Ok(token) => {
          last_is_delim = matches!(token, cssparser::Token::Delim(_) | cssparser::Token::Comma);

          // If this is a delimiter, and the last token was whitespace,
          // replace the whitespace with the delimiter since both are not required.
          if last_is_delim && last_is_whitespace {
            let last = tokens.last_mut().unwrap();
            *last = Token::from(token).into();
          } else {
            tokens.push(Token::from(token).into());
          }

          last_is_whitespace = false;
        }
        Err(_) => break,
      }
    }

    Ok(())
  }
}

#[inline]
fn try_parse_color_token<'i, 't>(
  f: &CowArcStr<'i>,
  state: &ParserState,
  input: &mut Parser<'i, 't>,
) -> Option<CssColor> {
  match_ignore_ascii_case! { &*f,
    "rgb" | "rgba" | "hsl" | "hsla" | "hwb" | "lab" | "lch" | "oklab" | "oklch" | "color" | "color-mix" | "light-dark" => {
      let s = input.state();
      input.reset(&state);
      if let Ok(color) = CssColor::parse(input) {
        return Some(color)
      }
      input.reset(&s);
    },
    _ => {}
  }

  None
}

impl<'i> TokenList<'i> {
  pub(crate) fn to_css<W>(&self, dest: &mut Printer<W>, is_custom_property: bool) -> Result<(), PrinterError>
  where
    W: std::fmt::Write,
  {
    if !dest.minify && self.0.len() == 1 && matches!(self.0.first(), Some(token) if token.is_whitespace()) {
      return Ok(());
    }

    let mut has_whitespace = false;
    for (i, token_or_value) in self.0.iter().enumerate() {
      has_whitespace = match token_or_value {
        TokenOrValue::Color(color) => {
          color.to_css(dest)?;
          false
        }
        TokenOrValue::UnresolvedColor(color) => {
          color.to_css(dest, is_custom_property)?;
          false
        }
        TokenOrValue::Url(url) => {
          if dest.dependencies.is_some() && is_custom_property && !url.is_absolute() {
            return Err(dest.error(
              PrinterErrorKind::AmbiguousUrlInCustomProperty {
                url: url.url.as_ref().to_owned(),
              },
              url.loc,
            ));
          }
          url.to_css(dest)?;
          false
        }
        TokenOrValue::Var(var) => {
          var.to_css(dest, is_custom_property)?;
          self.write_whitespace_if_needed(i, dest)?
        }
        TokenOrValue::Env(env) => {
          env.to_css(dest, is_custom_property)?;
          self.write_whitespace_if_needed(i, dest)?
        }
        TokenOrValue::Function(f) => {
          f.to_css(dest, is_custom_property)?;
          self.write_whitespace_if_needed(i, dest)?
        }
        TokenOrValue::Length(v) => {
          // Do not serialize unitless zero lengths in custom properties as it may break calc().
          let (value, unit) = v.to_unit_value();
          serialize_dimension(value, unit, dest)?;
          false
        }
        TokenOrValue::Angle(v) => {
          v.to_css(dest)?;
          false
        }
        TokenOrValue::Time(v) => {
          v.to_css(dest)?;
          false
        }
        TokenOrValue::Resolution(v) => {
          v.to_css(dest)?;
          false
        }
        TokenOrValue::DashedIdent(v) => {
          v.to_css(dest)?;
          false
        }
        TokenOrValue::AnimationName(v) => {
          v.to_css(dest)?;
          false
        }
        TokenOrValue::Token(token) => match token {
          Token::Delim(d) => {
            if *d == '+' || *d == '-' {
              dest.write_char(' ')?;
              dest.write_char(*d)?;
              dest.write_char(' ')?;
            } else {
              let ws_before = !has_whitespace && (*d == '/' || *d == '*');
              dest.delim(*d, ws_before)?;
            }
            true
          }
          Token::Comma => {
            dest.delim(',', false)?;
            true
          }
          Token::CloseParenthesis | Token::CloseSquareBracket | Token::CloseCurlyBracket => {
            token.to_css(dest)?;
            self.write_whitespace_if_needed(i, dest)?
          }
          Token::Dimension { value, unit, .. } => {
            serialize_dimension(*value, unit, dest)?;
            false
          }
          Token::Number { value, .. } => {
            value.to_css(dest)?;
            false
          }
          _ => {
            token.to_css(dest)?;
            matches!(token, Token::WhiteSpace(..))
          }
        },
      };
    }

    Ok(())
  }

  pub(crate) fn to_css_raw<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
  where
    W: std::fmt::Write,
  {
    for token_or_value in &self.0 {
      match token_or_value {
        TokenOrValue::Token(token) => {
          token.to_css(dest)?;
        }
        _ => {
          return Err(PrinterError {
            kind: PrinterErrorKind::FmtError,
            loc: None,
          })
        }
      }
    }

    Ok(())
  }

  #[inline]
  fn write_whitespace_if_needed<W>(&self, i: usize, dest: &mut Printer<W>) -> Result<bool, PrinterError>
  where
    W: std::fmt::Write,
  {
    if !dest.minify
      && i != self.0.len() - 1
      && !matches!(
        self.0[i + 1],
        TokenOrValue::Token(Token::Comma) | TokenOrValue::Token(Token::CloseParenthesis)
      )
    {
      // Whitespace is removed during parsing, so add it back if we aren't minifying.
      dest.write_char(' ')?;
      Ok(true)
    } else {
      Ok(false)
    }
  }
}

/// A raw CSS token.
// Copied from cssparser to change CowRcStr to CowArcStr
#[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", rename_all = "kebab-case")
)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub enum Token<'a> {
  /// A [`<ident-token>`](https://drafts.csswg.org/css-syntax/#ident-token-diagram)
  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<CowArcStr>"))]
  Ident(#[cfg_attr(feature = "serde", serde(borrow))] CowArcStr<'a>),

  /// A [`<at-keyword-token>`](https://drafts.csswg.org/css-syntax/#at-keyword-token-diagram)
  ///
  /// The value does not include the `@` marker.
  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<CowArcStr>"))]
  AtKeyword(CowArcStr<'a>),

  /// A [`<hash-token>`](https://drafts.csswg.org/css-syntax/#hash-token-diagram) with the type flag set to "unrestricted"
  ///
  /// The value does not include the `#` marker.
  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<CowArcStr>"))]
  Hash(CowArcStr<'a>),

  /// A [`<hash-token>`](https://drafts.csswg.org/css-syntax/#hash-token-diagram) with the type flag set to "id"
  ///
  /// The value does not include the `#` marker.
  #[cfg_attr(feature = "serde", serde(rename = "id-hash", with = "ValueWrapper::<CowArcStr>"))]
  IDHash(CowArcStr<'a>), // Hash that is a valid ID selector.

  /// A [`<string-token>`](https://drafts.csswg.org/css-syntax/#string-token-diagram)
  ///
  /// The value does not include the quotes.
  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<CowArcStr>"))]
  String(CowArcStr<'a>),

  /// A [`<url-token>`](https://drafts.csswg.org/css-syntax/#url-token-diagram)
  ///
  /// The value does not include the `url(` `)` markers.  Note that `url( <string-token> )` is represented by a
  /// `Function` token.
  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<CowArcStr>"))]
  UnquotedUrl(CowArcStr<'a>),

  /// A `<delim-token>`
  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<char>"))]
  Delim(char),

  /// A [`<number-token>`](https://drafts.csswg.org/css-syntax/#number-token-diagram)
  Number {
    /// Whether the number had a `+` or `-` sign.
    ///
    /// This is used is some cases like the <An+B> micro syntax. (See the `parse_nth` function.)
    #[cfg_attr(feature = "serde", serde(skip))]
    has_sign: bool,

    /// The value as a float
    value: f32,

    /// If the origin source did not include a fractional part, the value as an integer.
    #[cfg_attr(feature = "serde", serde(skip))]
    int_value: Option<i32>,
  },

  /// A [`<percentage-token>`](https://drafts.csswg.org/css-syntax/#percentage-token-diagram)
  Percentage {
    /// Whether the number had a `+` or `-` sign.
    #[cfg_attr(feature = "serde", serde(skip))]
    has_sign: bool,

    /// The value as a float, divided by 100 so that the nominal range is 0.0 to 1.0.
    #[cfg_attr(feature = "serde", serde(rename = "value"))]
    unit_value: f32,

    /// If the origin source did not include a fractional part, the value as an integer.
    /// It is **not** divided by 100.
    #[cfg_attr(feature = "serde", serde(skip))]
    int_value: Option<i32>,
  },

  /// A [`<dimension-token>`](https://drafts.csswg.org/css-syntax/#dimension-token-diagram)
  Dimension {
    /// Whether the number had a `+` or `-` sign.
    ///
    /// This is used is some cases like the <An+B> micro syntax. (See the `parse_nth` function.)
    #[cfg_attr(feature = "serde", serde(skip))]
    has_sign: bool,

    /// The value as a float
    value: f32,

    /// If the origin source did not include a fractional part, the value as an integer.
    #[cfg_attr(feature = "serde", serde(skip))]
    int_value: Option<i32>,

    /// The unit, e.g. "px" in `12px`
    unit: CowArcStr<'a>,
  },

  /// A [`<whitespace-token>`](https://drafts.csswg.org/css-syntax/#whitespace-token-diagram)
  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<CowArcStr>"))]
  WhiteSpace(CowArcStr<'a>),

  /// A comment.
  ///
  /// The CSS Syntax spec does not generate tokens for comments,
  /// But we do, because we can (borrowed &str makes it cheap).
  ///
  /// The value does not include the `/*` `*/` markers.
  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<CowArcStr>"))]
  Comment(CowArcStr<'a>),

  /// A `:` `<colon-token>`
  Colon, // :

  /// A `;` `<semicolon-token>`
  Semicolon, // ;

  /// A `,` `<comma-token>`
  Comma, // ,

  /// A `~=` [`<include-match-token>`](https://drafts.csswg.org/css-syntax/#include-match-token-diagram)
  IncludeMatch,

  /// A `|=` [`<dash-match-token>`](https://drafts.csswg.org/css-syntax/#dash-match-token-diagram)
  DashMatch,

  /// A `^=` [`<prefix-match-token>`](https://drafts.csswg.org/css-syntax/#prefix-match-token-diagram)
  PrefixMatch,

  /// A `$=` [`<suffix-match-token>`](https://drafts.csswg.org/css-syntax/#suffix-match-token-diagram)
  SuffixMatch,

  /// A `*=` [`<substring-match-token>`](https://drafts.csswg.org/css-syntax/#substring-match-token-diagram)
  SubstringMatch,

  /// A `<!--` [`<CDO-token>`](https://drafts.csswg.org/css-syntax/#CDO-token-diagram)
  #[cfg_attr(feature = "serde", serde(rename = "cdo"))]
  CDO,

  /// A `-->` [`<CDC-token>`](https://drafts.csswg.org/css-syntax/#CDC-token-diagram)
  #[cfg_attr(feature = "serde", serde(rename = "cdc"))]
  CDC,

  /// A [`<function-token>`](https://drafts.csswg.org/css-syntax/#function-token-diagram)
  ///
  /// The value (name) does not include the `(` marker.
  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<CowArcStr>"))]
  Function(CowArcStr<'a>),

  /// A `<(-token>`
  ParenthesisBlock,

  /// A `<[-token>`
  SquareBracketBlock,

  /// A `<{-token>`
  CurlyBracketBlock,

  /// A `<bad-url-token>`
  ///
  /// This token always indicates a parse error.
  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<CowArcStr>"))]
  BadUrl(CowArcStr<'a>),

  /// A `<bad-string-token>`
  ///
  /// This token always indicates a parse error.
  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<CowArcStr>"))]
  BadString(CowArcStr<'a>),

  /// A `<)-token>`
  ///
  /// When obtained from one of the `Parser::next*` methods,
  /// this token is always unmatched and indicates a parse error.
  CloseParenthesis,

  /// A `<]-token>`
  ///
  /// When obtained from one of the `Parser::next*` methods,
  /// this token is always unmatched and indicates a parse error.
  CloseSquareBracket,

  /// A `<}-token>`
  ///
  /// When obtained from one of the `Parser::next*` methods,
  /// this token is always unmatched and indicates a parse error.
  CloseCurlyBracket,
}

impl<'a> From<&cssparser::Token<'a>> for Token<'a> {
  #[inline]
  fn from(t: &cssparser::Token<'a>) -> Token<'a> {
    match t {
      cssparser::Token::Ident(x) => Token::Ident(x.into()),
      cssparser::Token::AtKeyword(x) => Token::AtKeyword(x.into()),
      cssparser::Token::Hash(x) => Token::Hash(x.into()),
      cssparser::Token::IDHash(x) => Token::IDHash(x.into()),
      cssparser::Token::QuotedString(x) => Token::String(x.into()),
      cssparser::Token::UnquotedUrl(x) => Token::UnquotedUrl(x.into()),
      cssparser::Token::Function(x) => Token::Function(x.into()),
      cssparser::Token::BadUrl(x) => Token::BadUrl(x.into()),
      cssparser::Token::BadString(x) => Token::BadString(x.into()),
      cssparser::Token::Delim(c) => Token::Delim(*c),
      cssparser::Token::Number {
        has_sign,
        value,
        int_value,
      } => Token::Number {
        has_sign: *has_sign,
        value: *value,
        int_value: *int_value,
      },
      cssparser::Token::Dimension {
        has_sign,
        value,
        int_value,
        unit,
      } => Token::Dimension {
        has_sign: *has_sign,
        value: *value,
        int_value: *int_value,
        unit: unit.into(),
      },
      cssparser::Token::Percentage {
        has_sign,
        unit_value,
        int_value,
      } => Token::Percentage {
        has_sign: *has_sign,
        unit_value: *unit_value,
        int_value: *int_value,
      },
      cssparser::Token::WhiteSpace(w) => Token::WhiteSpace((*w).into()),
      cssparser::Token::Comment(c) => Token::Comment((*c).into()),
      cssparser::Token::Colon => Token::Colon,
      cssparser::Token::Semicolon => Token::Semicolon,
      cssparser::Token::Comma => Token::Comma,
      cssparser::Token::IncludeMatch => Token::IncludeMatch,
      cssparser::Token::DashMatch => Token::DashMatch,
      cssparser::Token::PrefixMatch => Token::PrefixMatch,
      cssparser::Token::SuffixMatch => Token::SuffixMatch,
      cssparser::Token::SubstringMatch => Token::SubstringMatch,
      cssparser::Token::CDO => Token::CDO,
      cssparser::Token::CDC => Token::CDC,
      cssparser::Token::ParenthesisBlock => Token::ParenthesisBlock,
      cssparser::Token::SquareBracketBlock => Token::SquareBracketBlock,
      cssparser::Token::CurlyBracketBlock => Token::CurlyBracketBlock,
      cssparser::Token::CloseParenthesis => Token::CloseParenthesis,
      cssparser::Token::CloseSquareBracket => Token::CloseSquareBracket,
      cssparser::Token::CloseCurlyBracket => Token::CloseCurlyBracket,
    }
  }
}

impl<'a> ToCss for Token<'a> {
  #[inline]
  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
  where
    W: std::fmt::Write,
  {
    use cssparser::ToCss;
    match self {
      Token::Ident(x) => cssparser::Token::Ident(x.as_ref().into()).to_css(dest)?,
      Token::AtKeyword(x) => cssparser::Token::AtKeyword(x.as_ref().into()).to_css(dest)?,
      Token::Hash(x) => cssparser::Token::Hash(x.as_ref().into()).to_css(dest)?,
      Token::IDHash(x) => cssparser::Token::IDHash(x.as_ref().into()).to_css(dest)?,
      Token::String(x) => cssparser::Token::QuotedString(x.as_ref().into()).to_css(dest)?,
      Token::UnquotedUrl(x) => cssparser::Token::UnquotedUrl(x.as_ref().into()).to_css(dest)?,
      Token::Function(x) => cssparser::Token::Function(x.as_ref().into()).to_css(dest)?,
      Token::BadUrl(x) => cssparser::Token::BadUrl(x.as_ref().into()).to_css(dest)?,
      Token::BadString(x) => cssparser::Token::BadString(x.as_ref().into()).to_css(dest)?,
      Token::Delim(c) => cssparser::Token::Delim(*c).to_css(dest)?,
      Token::Number {
        has_sign,
        value,
        int_value,
      } => cssparser::Token::Number {
        has_sign: *has_sign,
        value: *value,
        int_value: *int_value,
      }
      .to_css(dest)?,
      Token::Dimension {
        has_sign,
        value,
        int_value,
        unit,
      } => cssparser::Token::Dimension {
        has_sign: *has_sign,
        value: *value,
        int_value: *int_value,
        unit: unit.as_ref().into(),
      }
      .to_css(dest)?,
      Token::Percentage {
        has_sign,
        unit_value,
        int_value,
      } => cssparser::Token::Percentage {
        has_sign: *has_sign,
        unit_value: *unit_value,
        int_value: *int_value,
      }
      .to_css(dest)?,
      Token::WhiteSpace(w) => cssparser::Token::WhiteSpace(w).to_css(dest)?,
      Token::Comment(c) => cssparser::Token::Comment(c).to_css(dest)?,
      Token::Colon => cssparser::Token::Colon.to_css(dest)?,
      Token::Semicolon => cssparser::Token::Semicolon.to_css(dest)?,
      Token::Comma => cssparser::Token::Comma.to_css(dest)?,
      Token::IncludeMatch => cssparser::Token::IncludeMatch.to_css(dest)?,
      Token::DashMatch => cssparser::Token::DashMatch.to_css(dest)?,
      Token::PrefixMatch => cssparser::Token::PrefixMatch.to_css(dest)?,
      Token::SuffixMatch => cssparser::Token::SuffixMatch.to_css(dest)?,
      Token::SubstringMatch => cssparser::Token::SubstringMatch.to_css(dest)?,
      Token::CDO => cssparser::Token::CDO.to_css(dest)?,
      Token::CDC => cssparser::Token::CDC.to_css(dest)?,
      Token::ParenthesisBlock => cssparser::Token::ParenthesisBlock.to_css(dest)?,
      Token::SquareBracketBlock => cssparser::Token::SquareBracketBlock.to_css(dest)?,
      Token::CurlyBracketBlock => cssparser::Token::CurlyBracketBlock.to_css(dest)?,
      Token::CloseParenthesis => cssparser::Token::CloseParenthesis.to_css(dest)?,
      Token::CloseSquareBracket => cssparser::Token::CloseSquareBracket.to_css(dest)?,
      Token::CloseCurlyBracket => cssparser::Token::CloseCurlyBracket.to_css(dest)?,
    }

    Ok(())
  }
}

impl<'a> Eq for Token<'a> {}

impl<'a> std::hash::Hash for Token<'a> {
  fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
    let tag = std::mem::discriminant(self);
    tag.hash(state);
    match self {
      Token::Ident(x) => x.hash(state),
      Token::AtKeyword(x) => x.hash(state),
      Token::Hash(x) => x.hash(state),
      Token::IDHash(x) => x.hash(state),
      Token::String(x) => x.hash(state),
      Token::UnquotedUrl(x) => x.hash(state),
      Token::Function(x) => x.hash(state),
      Token::BadUrl(x) => x.hash(state),
      Token::BadString(x) => x.hash(state),
      Token::Delim(x) => x.hash(state),
      Token::Number {
        has_sign,
        value,
        int_value,
      } => {
        has_sign.hash(state);
        integer_decode(*value).hash(state);
        int_value.hash(state);
      }
      Token::Dimension {
        has_sign,
        value,
        int_value,
        unit,
      } => {
        has_sign.hash(state);
        integer_decode(*value).hash(state);
        int_value.hash(state);
        unit.hash(state);
      }
      Token::Percentage {
        has_sign,
        unit_value,
        int_value,
      } => {
        has_sign.hash(state);
        integer_decode(*unit_value).hash(state);
        int_value.hash(state);
      }
      Token::WhiteSpace(w) => w.hash(state),
      Token::Comment(c) => c.hash(state),
      Token::Colon
      | Token::Semicolon
      | Token::Comma
      | Token::IncludeMatch
      | Token::DashMatch
      | Token::PrefixMatch
      | Token::SuffixMatch
      | Token::SubstringMatch
      | Token::CDO
      | Token::CDC
      | Token::ParenthesisBlock
      | Token::SquareBracketBlock
      | Token::CurlyBracketBlock
      | Token::CloseParenthesis
      | Token::CloseSquareBracket
      | Token::CloseCurlyBracket => {}
    }
  }
}

/// Converts a floating point value into its mantissa, exponent,
/// and sign components so that it can be hashed.
fn integer_decode(v: f32) -> (u32, i16, i8) {
  let bits: u32 = unsafe { std::mem::transmute(v) };
  let sign: i8 = if bits >> 31 == 0 { 1 } else { -1 };
  let mut exponent: i16 = ((bits >> 23) & 0xff) as i16;
  let mantissa = if exponent == 0 {
    (bits & 0x7fffff) << 1
  } else {
    (bits & 0x7fffff) | 0x800000
  };
  // Exponent bias + mantissa shift
  exponent -= 127 + 23;
  (mantissa, exponent, sign)
}

impl<'i> TokenList<'i> {
  pub(crate) fn get_necessary_fallbacks(&self, targets: Targets) -> ColorFallbackKind {
    let mut fallbacks = ColorFallbackKind::empty();
    for token in &self.0 {
      match token {
        TokenOrValue::Color(color) => {
          fallbacks |= color.get_possible_fallbacks(targets);
        }
        TokenOrValue::Function(f) => {
          fallbacks |= f.arguments.get_necessary_fallbacks(targets);
        }
        TokenOrValue::Var(v) => {
          if let Some(fallback) = &v.fallback {
            fallbacks |= fallback.get_necessary_fallbacks(targets);
          }
        }
        TokenOrValue::Env(v) => {
          if let Some(fallback) = &v.fallback {
            fallbacks |= fallback.get_necessary_fallbacks(targets);
          }
        }
        _ => {}
      }
    }

    fallbacks
  }

  pub(crate) fn get_fallback(&self, kind: ColorFallbackKind) -> Self {
    let tokens = self
      .0
      .iter()
      .map(|token| match token {
        TokenOrValue::Color(color) => TokenOrValue::Color(color.get_fallback(kind)),
        TokenOrValue::Function(f) => TokenOrValue::Function(f.get_fallback(kind)),
        TokenOrValue::Var(v) => TokenOrValue::Var(v.get_fallback(kind)),
        TokenOrValue::Env(e) => TokenOrValue::Env(e.get_fallback(kind)),
        _ => token.clone(),
      })
      .collect();
    TokenList(tokens)
  }

  pub(crate) fn get_fallbacks(&mut self, targets: Targets) -> Vec<(SupportsCondition<'i>, Self)> {
    // Get the full list of possible fallbacks, and remove the lowest one, which will replace
    // the original declaration. The remaining fallbacks need to be added as @supports rules.
    let mut fallbacks = self.get_necessary_fallbacks(targets);
    let lowest_fallback = fallbacks.lowest();
    fallbacks.remove(lowest_fallback);

    let mut res = Vec::new();
    if fallbacks.contains(ColorFallbackKind::P3) {
      res.push((
        ColorFallbackKind::P3.supports_condition(),
        self.get_fallback(ColorFallbackKind::P3),
      ));
    }

    if fallbacks.contains(ColorFallbackKind::LAB) {
      res.push((
        ColorFallbackKind::LAB.supports_condition(),
        self.get_fallback(ColorFallbackKind::LAB),
      ));
    }

    if !lowest_fallback.is_empty() {
      for token in self.0.iter_mut() {
        match token {
          TokenOrValue::Color(color) => {
            *color = color.get_fallback(lowest_fallback);
          }
          TokenOrValue::Function(f) => *f = f.get_fallback(lowest_fallback),
          TokenOrValue::Var(v) if v.fallback.is_some() => *v = v.get_fallback(lowest_fallback),
          TokenOrValue::Env(v) if v.fallback.is_some() => *v = v.get_fallback(lowest_fallback),
          _ => {}
        }
      }
    }

    res
  }

  pub(crate) fn get_features(&self) -> Features {
    let mut features = Features::empty();
    for token in &self.0 {
      match token {
        TokenOrValue::Color(color) => {
          features |= color.get_features();
        }
        TokenOrValue::UnresolvedColor(unresolved_color) => {
          features |= Features::SpaceSeparatedColorNotation;
          match unresolved_color {
            UnresolvedColor::LightDark { light, dark } => {
              features |= Features::LightDark;
              features |= light.get_features();
              features |= dark.get_features();
            }
            _ => {}
          }
        }
        TokenOrValue::Function(f) => {
          features |= f.arguments.get_features();
        }
        TokenOrValue::Var(v) => {
          if let Some(fallback) = &v.fallback {
            features |= fallback.get_features();
          }
        }
        TokenOrValue::Env(v) => {
          if let Some(fallback) = &v.fallback {
            features |= fallback.get_features();
          }
        }
        _ => {}
      }
    }

    features
  }

  /// Substitutes variables with the provided values.
  #[cfg(feature = "substitute_variables")]
  #[cfg_attr(docsrs, doc(cfg(feature = "substitute_variables")))]
  pub fn substitute_variables(&mut self, vars: &std::collections::HashMap<&str, TokenList<'i>>) {
    self.visit(&mut VarInliner { vars }).unwrap()
  }
}

#[cfg(feature = "substitute_variables")]
struct VarInliner<'a, 'i> {
  vars: &'a std::collections::HashMap<&'a str, TokenList<'i>>,
}

#[cfg(feature = "substitute_variables")]
impl<'a, 'i> crate::visitor::Visitor<'i> for VarInliner<'a, 'i> {
  type Error = std::convert::Infallible;

  fn visit_types(&self) -> crate::visitor::VisitTypes {
    crate::visit_types!(TOKENS | VARIABLES)
  }

  fn visit_token_list(&mut self, tokens: &mut TokenList<'i>) -> Result<(), Self::Error> {
    let mut i = 0;
    let mut seen = std::collections::HashSet::new();
    while i < tokens.0.len() {
      let token = &mut tokens.0[i];
      token.visit(self).unwrap();
      if let TokenOrValue::Var(var) = token {
        if let Some(value) = self.vars.get(var.name.ident.0.as_ref()) {
          // Ignore circular references.
          if seen.insert(var.name.ident.0.clone()) {
            tokens.0.splice(i..i + 1, value.0.iter().cloned());
            // Don't advance. We need to replace any variables in the value.
            continue;
          }
        } else if let Some(fallback) = &var.fallback {
          let fallback = fallback.0.clone();
          if seen.insert(var.name.ident.0.clone()) {
            tokens.0.splice(i..i + 1, fallback.into_iter());
            continue;
          }
        }
      }
      seen.clear();
      i += 1;
    }
    Ok(())
  }
}

/// A CSS variable reference.
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "visitor", derive(Visit))]
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
#[cfg_attr(feature = "visitor", visit(visit_variable, VARIABLES))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub struct Variable<'i> {
  /// The variable name.
  #[cfg_attr(feature = "serde", serde(borrow))]
  pub name: DashedIdentReference<'i>,
  /// A fallback value in case the variable is not defined.
  pub fallback: Option<TokenList<'i>>,
}

impl<'i> Variable<'i> {
  fn parse<'t>(
    input: &mut Parser<'i, 't>,
    options: &ParserOptions<'_, 'i>,
    depth: usize,
  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
    let name = DashedIdentReference::parse_with_options(input, options)?;

    let fallback = if input.try_parse(|input| input.expect_comma()).is_ok() {
      Some(TokenList::parse(input, options, depth)?)
    } else {
      None
    };

    Ok(Variable { name, fallback })
  }

  fn to_css<W>(&self, dest: &mut Printer<W>, is_custom_property: bool) -> Result<(), PrinterError>
  where
    W: std::fmt::Write,
  {
    dest.write_str("var(")?;
    self.name.to_css(dest)?;
    if let Some(fallback) = &self.fallback {
      dest.delim(',', false)?;
      fallback.to_css(dest, is_custom_property)?;
    }
    dest.write_char(')')
  }

  fn get_fallback(&self, kind: ColorFallbackKind) -> Self {
    Variable {
      name: self.name.clone(),
      fallback: self.fallback.as_ref().map(|fallback| fallback.get_fallback(kind)),
    }
  }
}

/// A CSS environment variable reference.
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(
  feature = "visitor",
  derive(Visit),
  visit(visit_environment_variable, ENVIRONMENT_VARIABLES)
)]
#[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 EnvironmentVariable<'i> {
  /// The environment variable name.
  #[cfg_attr(feature = "serde", serde(borrow))]
  pub name: EnvironmentVariableName<'i>,
  /// Optional indices into the dimensions of the environment variable.
  #[cfg_attr(feature = "serde", serde(default))]
  pub indices: Vec<CSSInteger>,
  /// A fallback value in case the variable is not defined.
  pub fallback: Option<TokenList<'i>>,
}

/// A CSS environment variable name.
#[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", rename_all = "lowercase")
)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub enum EnvironmentVariableName<'i> {
  /// A UA-defined environment variable.
  #[cfg_attr(
    feature = "serde",
    serde(with = "crate::serialization::ValueWrapper::<UAEnvironmentVariable>")
  )]
  UA(UAEnvironmentVariable),
  /// A custom author-defined environment variable.
  #[cfg_attr(feature = "serde", serde(borrow))]
  Custom(DashedIdentReference<'i>),
  /// An unknown environment variable.
  #[cfg_attr(feature = "serde", serde(with = "crate::serialization::ValueWrapper::<CustomIdent>"))]
  Unknown(CustomIdent<'i>),
}

enum_property! {
  /// A UA-defined environment variable name.
  pub enum UAEnvironmentVariable {
    /// The safe area inset from the top of the viewport.
    SafeAreaInsetTop,
    /// The safe area inset from the right of the viewport.
    SafeAreaInsetRight,
    /// The safe area inset from the bottom of the viewport.
    SafeAreaInsetBottom,
    /// The safe area inset from the left of the viewport.
    SafeAreaInsetLeft,
    /// The viewport segment width.
    ViewportSegmentWidth,
    /// The viewport segment height.
    ViewportSegmentHeight,
    /// The viewport segment top position.
    ViewportSegmentTop,
    /// The viewport segment left position.
    ViewportSegmentLeft,
    /// The viewport segment bottom position.
    ViewportSegmentBottom,
    /// The viewport segment right position.
    ViewportSegmentRight,
  }
}

impl<'i> EnvironmentVariableName<'i> {
  /// Returns the name of the environment variable as a string.
  pub fn name(&self) -> &str {
    match self {
      EnvironmentVariableName::UA(ua) => ua.as_str(),
      EnvironmentVariableName::Custom(c) => c.ident.as_ref(),
      EnvironmentVariableName::Unknown(u) => u.0.as_ref(),
    }
  }
}

impl<'i> Parse<'i> for EnvironmentVariableName<'i> {
  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
    if let Ok(ua) = input.try_parse(UAEnvironmentVariable::parse) {
      return Ok(EnvironmentVariableName::UA(ua));
    }

    if let Ok(dashed) =
      input.try_parse(|input| DashedIdentReference::parse_with_options(input, &ParserOptions::default()))
    {
      return Ok(EnvironmentVariableName::Custom(dashed));
    }

    let ident = CustomIdent::parse(input)?;
    return Ok(EnvironmentVariableName::Unknown(ident));
  }
}

impl<'i> ToCss for EnvironmentVariableName<'i> {
  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
  where
    W: std::fmt::Write,
  {
    match self {
      EnvironmentVariableName::UA(ua) => ua.to_css(dest),
      EnvironmentVariableName::Custom(custom) => custom.to_css(dest),
      EnvironmentVariableName::Unknown(unknown) => unknown.to_css(dest),
    }
  }
}

impl<'i> EnvironmentVariable<'i> {
  pub(crate) fn parse<'t>(
    input: &mut Parser<'i, 't>,
    options: &ParserOptions<'_, 'i>,
    depth: usize,
  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
    input.expect_function_matching("env")?;
    input.parse_nested_block(|input| Self::parse_nested(input, options, depth))
  }

  pub(crate) fn parse_nested<'t>(
    input: &mut Parser<'i, 't>,
    options: &ParserOptions<'_, 'i>,
    depth: usize,
  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
    let name = EnvironmentVariableName::parse(input)?;
    let mut indices = Vec::new();
    while let Ok(index) = input.try_parse(CSSInteger::parse) {
      indices.push(index);
    }

    let fallback = if input.try_parse(|input| input.expect_comma()).is_ok() {
      Some(TokenList::parse(input, options, depth + 1)?)
    } else {
      None
    };

    Ok(EnvironmentVariable {
      name,
      indices,
      fallback,
    })
  }

  pub(crate) fn to_css<W>(&self, dest: &mut Printer<W>, is_custom_property: bool) -> Result<(), PrinterError>
  where
    W: std::fmt::Write,
  {
    dest.write_str("env(")?;
    self.name.to_css(dest)?;

    for item in &self.indices {
      dest.write_char(' ')?;
      item.to_css(dest)?;
    }

    if let Some(fallback) = &self.fallback {
      dest.delim(',', false)?;
      fallback.to_css(dest, is_custom_property)?;
    }
    dest.write_char(')')
  }

  fn get_fallback(&self, kind: ColorFallbackKind) -> Self {
    EnvironmentVariable {
      name: self.name.clone(),
      indices: self.indices.clone(),
      fallback: self.fallback.as_ref().map(|fallback| fallback.get_fallback(kind)),
    }
  }
}

/// A custom CSS function.
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "visitor", derive(Visit))]
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
#[cfg_attr(feature = "visitor", visit(visit_function, FUNCTIONS))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub struct Function<'i> {
  /// The function name.
  #[cfg_attr(feature = "serde", serde(borrow))]
  pub name: Ident<'i>,
  /// The function arguments.
  pub arguments: TokenList<'i>,
}

impl<'i> Function<'i> {
  fn to_css<W>(&self, dest: &mut Printer<W>, is_custom_property: bool) -> Result<(), PrinterError>
  where
    W: std::fmt::Write,
  {
    self.name.to_css(dest)?;
    dest.write_char('(')?;
    self.arguments.to_css(dest, is_custom_property)?;
    dest.write_char(')')
  }

  fn get_fallback(&self, kind: ColorFallbackKind) -> Self {
    Function {
      name: self.name.clone(),
      arguments: self.arguments.get_fallback(kind),
    }
  }
}

/// A color value with an unresolved alpha value (e.g. a variable).
/// These can be converted from the modern slash syntax to older comma syntax.
/// This can only be done when the only unresolved component is the alpha
/// since variables can resolve to multiple tokens.
#[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", rename_all = "lowercase")
)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub enum UnresolvedColor<'i> {
  /// An rgb() color.
  RGB {
    /// The red component.
    r: f32,
    /// The green component.
    g: f32,
    /// The blue component.
    b: f32,
    /// The unresolved alpha component.
    #[cfg_attr(feature = "serde", serde(borrow))]
    alpha: TokenList<'i>,
  },
  /// An hsl() color.
  HSL {
    /// The hue component.
    h: f32,
    /// The saturation component.
    s: f32,
    /// The lightness component.
    l: f32,
    /// The unresolved alpha component.
    #[cfg_attr(feature = "serde", serde(borrow))]
    alpha: TokenList<'i>,
  },
  /// The light-dark() function.
  #[cfg_attr(feature = "serde", serde(rename = "light-dark"))]
  LightDark {
    /// The light value.
    light: TokenList<'i>,
    /// The dark value.
    dark: TokenList<'i>,
  },
}

impl<'i> LightDarkColor for UnresolvedColor<'i> {
  #[inline]
  fn light_dark(light: Self, dark: Self) -> Self {
    UnresolvedColor::LightDark {
      light: TokenList(vec![TokenOrValue::UnresolvedColor(light)]),
      dark: TokenList(vec![TokenOrValue::UnresolvedColor(dark)]),
    }
  }
}

impl<'i> UnresolvedColor<'i> {
  fn parse<'t>(
    f: &CowArcStr<'i>,
    input: &mut Parser<'i, 't>,
    options: &ParserOptions<'_, 'i>,
  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
    let mut parser = ComponentParser::new(false);
    match_ignore_ascii_case! { &*f,
      "rgb" => {
        input.parse_nested_block(|input| {
          parser.parse_relative::<RGB, _, _>(input, |input, parser| {
            let (r, g, b, is_legacy) = parse_rgb_components(input, parser)?;
            if is_legacy {
              return Err(input.new_custom_error(ParserError::InvalidValue))
            }
            input.expect_delim('/')?;
            let alpha = TokenList::parse(input, options, 0)?;
            Ok(UnresolvedColor::RGB { r, g, b, alpha })
          })
        })
      },
      "hsl" => {
        input.parse_nested_block(|input| {
          parser.parse_relative::<HSL, _, _>(input, |input, parser| {
            let (h, s, l, is_legacy) = parse_hsl_hwb_components::<HSL>(input, parser, false)?;
            if is_legacy {
              return Err(input.new_custom_error(ParserError::InvalidValue))
            }
            input.expect_delim('/')?;
            let alpha = TokenList::parse(input, options, 0)?;
            Ok(UnresolvedColor::HSL { h, s, l, alpha })
          })
        })
      },
      "light-dark" => {
        input.parse_nested_block(|input| {
          let light = input.parse_until_before(Delimiter::Comma, |input|
            TokenList::parse(input, options, 0)
          )?;
          input.expect_comma()?;
          let dark = TokenList::parse(input, options, 0)?;
          Ok(UnresolvedColor::LightDark { light, dark })
        })
      },
      _ => Err(input.new_custom_error(ParserError::InvalidValue))
    }
  }

  fn to_css<W>(&self, dest: &mut Printer<W>, is_custom_property: bool) -> Result<(), PrinterError>
  where
    W: std::fmt::Write,
  {
    match self {
      UnresolvedColor::RGB { r, g, b, alpha } => {
        if should_compile!(dest.targets.current, SpaceSeparatedColorNotation) {
          dest.write_str("rgba(")?;
          r.to_css(dest)?;
          dest.delim(',', false)?;
          g.to_css(dest)?;
          dest.delim(',', false)?;
          b.to_css(dest)?;
          dest.delim(',', false)?;
          alpha.to_css(dest, is_custom_property)?;
          dest.write_char(')')?;
          return Ok(());
        }

        dest.write_str("rgb(")?;
        r.to_css(dest)?;
        dest.write_char(' ')?;
        g.to_css(dest)?;
        dest.write_char(' ')?;
        b.to_css(dest)?;
        dest.delim('/', true)?;
        alpha.to_css(dest, is_custom_property)?;
        dest.write_char(')')
      }
      UnresolvedColor::HSL { h, s, l, alpha } => {
        if should_compile!(dest.targets.current, SpaceSeparatedColorNotation) {
          dest.write_str("hsla(")?;
          h.to_css(dest)?;
          dest.delim(',', false)?;
          Percentage(*s / 100.0).to_css(dest)?;
          dest.delim(',', false)?;
          Percentage(*l / 100.0).to_css(dest)?;
          dest.delim(',', false)?;
          alpha.to_css(dest, is_custom_property)?;
          dest.write_char(')')?;
          return Ok(());
        }

        dest.write_str("hsl(")?;
        h.to_css(dest)?;
        dest.write_char(' ')?;
        Percentage(*s / 100.0).to_css(dest)?;
        dest.write_char(' ')?;
        Percentage(*l / 100.0).to_css(dest)?;
        dest.delim('/', true)?;
        alpha.to_css(dest, is_custom_property)?;
        dest.write_char(')')
      }
      UnresolvedColor::LightDark { light, dark } => {
        if should_compile!(dest.targets.current, LightDark) {
          dest.write_str("var(--lightningcss-light")?;
          dest.delim(',', false)?;
          light.to_css(dest, is_custom_property)?;
          dest.write_char(')')?;
          dest.whitespace()?;
          dest.write_str("var(--lightningcss-dark")?;
          dest.delim(',', false)?;
          dark.to_css(dest, is_custom_property)?;
          return dest.write_char(')');
        }

        dest.write_str("light-dark(")?;
        light.to_css(dest, is_custom_property)?;
        dest.delim(',', false)?;
        dark.to_css(dest, is_custom_property)?;
        dest.write_char(')')
      }
    }
  }
}
