//! Media queries.
use crate::error::{ErrorWithLocation, MinifyError, MinifyErrorKind, ParserError, PrinterError};
use crate::macros::enum_property;
use crate::parser::starts_with_ignore_ascii_case;
use crate::printer::Printer;
use crate::properties::custom::EnvironmentVariable;
#[cfg(feature = "visitor")]
use crate::rules::container::ContainerSizeFeatureId;
use crate::rules::custom_media::CustomMediaRule;
use crate::rules::Location;
use crate::stylesheet::ParserOptions;
use crate::targets::{should_compile, Targets};
use crate::traits::{Parse, ParseWithOptions, ToCss};
use crate::values::ident::{DashedIdent, Ident};
use crate::values::number::{CSSInteger, CSSNumber};
use crate::values::string::CowArcStr;
use crate::values::{length::Length, ratio::Ratio, resolution::Resolution};
use crate::vendor_prefix::VendorPrefix;
#[cfg(feature = "visitor")]
use crate::visitor::Visit;
use bitflags::bitflags;
use cssparser::*;
#[cfg(feature = "into_owned")]
use static_self::IntoOwned;
use std::borrow::Cow;
use std::collections::{HashMap, HashSet};

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

/// A [media query list](https://drafts.csswg.org/mediaqueries/#mq-list).
#[derive(Clone, Debug, PartialEq, Default)]
#[cfg_attr(feature = "visitor", derive(Visit), visit(visit_media_list, MEDIA_QUERIES))]
#[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 MediaList<'i> {
  /// The list of media queries.
  #[cfg_attr(feature = "serde", serde(borrow))]
  pub media_queries: Vec<MediaQuery<'i>>,
}

impl<'i> MediaList<'i> {
  /// Creates an empty media query list.
  pub fn new() -> Self {
    MediaList { media_queries: vec![] }
  }

  /// Parse a media query list from CSS.
  pub fn parse<'t>(
    input: &mut Parser<'i, 't>,
    options: &ParserOptions<'_, 'i>,
  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
    let mut media_queries = vec![];
    loop {
      match input.parse_until_before(Delimiter::Comma, |i| MediaQuery::parse_with_options(i, options)) {
        Ok(mq) => {
          media_queries.push(mq);
        }
        Err(err) => match err.kind {
          ParseErrorKind::Basic(BasicParseErrorKind::EndOfInput) => break,
          _ => return Err(err),
        },
      }

      match input.next() {
        Ok(&Token::Comma) => {}
        Ok(_) => unreachable!(),
        Err(_) => break,
      }
    }

    Ok(MediaList { media_queries })
  }

  pub(crate) fn transform_custom_media(
    &mut self,
    loc: Location,
    custom_media: &HashMap<CowArcStr<'i>, CustomMediaRule<'i>>,
  ) -> Result<(), MinifyError> {
    for query in self.media_queries.iter_mut() {
      query.transform_custom_media(loc, custom_media)?;
    }
    Ok(())
  }

  pub(crate) fn transform_resolution(&mut self, targets: Targets) {
    let mut i = 0;
    while i < self.media_queries.len() {
      let query = &self.media_queries[i];
      let mut prefixes = query.get_necessary_prefixes(targets);
      prefixes.remove(VendorPrefix::None);
      if !prefixes.is_empty() {
        let query = query.clone();
        for prefix in prefixes {
          let mut transformed = query.clone();
          transformed.transform_resolution(prefix);
          if !self.media_queries.contains(&transformed) {
            self.media_queries.insert(i, transformed);
          }
          i += 1;
        }
      }

      i += 1;
    }
  }

  /// Returns whether the media query list always matches.
  pub fn always_matches(&self) -> bool {
    // If the media list is empty, it always matches.
    self.media_queries.is_empty() || self.media_queries.iter().all(|mq| mq.always_matches())
  }

  /// Returns whether the media query list never matches.
  pub fn never_matches(&self) -> bool {
    !self.media_queries.is_empty() && self.media_queries.iter().all(|mq| mq.never_matches())
  }

  /// Attempts to combine the given media query list into this one. The resulting media query
  /// list matches if both the original media query lists would have matched.
  ///
  /// Returns an error if the boolean logic is not possible.
  pub fn and(&mut self, b: &MediaList<'i>) -> Result<(), ()> {
    if self.media_queries.is_empty() {
      self.media_queries.extend(b.media_queries.iter().cloned());
      return Ok(());
    }

    for b in &b.media_queries {
      if self.media_queries.contains(&b) {
        continue;
      }

      for a in &mut self.media_queries {
        a.and(&b)?;
      }
    }

    Ok(())
  }

  /// Combines the given media query list into this one. The resulting media query list
  /// matches if either of the original media query lists would have matched.
  pub fn or(&mut self, b: &MediaList<'i>) {
    for mq in &b.media_queries {
      if !self.media_queries.contains(&mq) {
        self.media_queries.push(mq.clone())
      }
    }
  }
}

impl<'i> ToCss for MediaList<'i> {
  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
  where
    W: std::fmt::Write,
  {
    if self.media_queries.is_empty() {
      dest.write_str("not all")?;
      return Ok(());
    }

    let mut first = true;
    for query in &self.media_queries {
      if !first {
        dest.delim(',', false)?;
      }
      first = false;
      query.to_css(dest)?;
    }
    Ok(())
  }
}

enum_property! {
  /// A [media query qualifier](https://drafts.csswg.org/mediaqueries/#mq-prefix).
  pub enum Qualifier {
    /// Prevents older browsers from matching the media query.
    Only,
    /// Negates a media query.
    Not,
  }
}

/// A [media type](https://drafts.csswg.org/mediaqueries/#media-types) within a media query.
#[derive(Clone, Debug, 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 = "kebab-case", into = "CowArcStr", from = "CowArcStr")
)]
pub enum MediaType<'i> {
  /// Matches all devices.
  All,
  /// Matches printers, and devices intended to reproduce a printed
  /// display, such as a web browser showing a document in “Print Preview”.
  Print,
  /// Matches all devices that aren’t matched by print.
  Screen,
  /// An unknown media type.
  #[cfg_attr(feature = "serde", serde(borrow))]
  Custom(CowArcStr<'i>),
}

impl<'i> From<CowArcStr<'i>> for MediaType<'i> {
  fn from(name: CowArcStr<'i>) -> Self {
    match_ignore_ascii_case! { &*name,
      "all" => MediaType::All,
      "print" => MediaType::Print,
      "screen" => MediaType::Screen,
      _ => MediaType::Custom(name)
    }
  }
}

impl<'i> Into<CowArcStr<'i>> for MediaType<'i> {
  fn into(self) -> CowArcStr<'i> {
    match self {
      MediaType::All => "all".into(),
      MediaType::Print => "print".into(),
      MediaType::Screen => "screen".into(),
      MediaType::Custom(desc) => desc,
    }
  }
}

impl<'i> Parse<'i> for MediaType<'i> {
  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
    let name: CowArcStr = input.expect_ident()?.into();
    Ok(Self::from(name))
  }
}

#[cfg(feature = "jsonschema")]
#[cfg_attr(docsrs, doc(cfg(feature = "jsonschema")))]
impl<'a> schemars::JsonSchema for MediaType<'a> {
  fn is_referenceable() -> bool {
    true
  }

  fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
    str::json_schema(gen)
  }

  fn schema_name() -> String {
    "MediaType".into()
  }
}

/// A [media query](https://drafts.csswg.org/mediaqueries/#media).
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "visitor", derive(Visit))]
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
#[cfg_attr(feature = "visitor", visit(visit_media_query, MEDIA_QUERIES))]
#[cfg_attr(feature = "serde", derive(serde::Serialize), serde(rename_all = "camelCase"))]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub struct MediaQuery<'i> {
  /// The qualifier for this query.
  pub qualifier: Option<Qualifier>,
  /// The media type for this query, that can be known, unknown, or "all".
  #[cfg_attr(feature = "serde", serde(borrow))]
  pub media_type: MediaType<'i>,
  /// The condition that this media query contains. This cannot have `or`
  /// in the first level.
  pub condition: Option<MediaCondition<'i>>,
}

impl<'i> ParseWithOptions<'i> for MediaQuery<'i> {
  fn parse_with_options<'t>(
    input: &mut Parser<'i, 't>,
    options: &ParserOptions<'_, 'i>,
  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
    let (qualifier, explicit_media_type) = input
      .try_parse(|input| -> Result<_, ParseError<'i, ParserError<'i>>> {
        let qualifier = input.try_parse(Qualifier::parse).ok();
        let media_type = MediaType::parse(input)?;
        Ok((qualifier, Some(media_type)))
      })
      .unwrap_or_default();

    let condition = if explicit_media_type.is_none() {
      Some(MediaCondition::parse_with_flags(
        input,
        QueryConditionFlags::ALLOW_OR,
        options,
      )?)
    } else if input.try_parse(|i| i.expect_ident_matching("and")).is_ok() {
      Some(MediaCondition::parse_with_flags(
        input,
        QueryConditionFlags::empty(),
        options,
      )?)
    } else {
      None
    };

    let media_type = explicit_media_type.unwrap_or(MediaType::All);
    Ok(Self {
      qualifier,
      media_type,
      condition,
    })
  }
}

impl<'i> MediaQuery<'i> {
  fn transform_custom_media(
    &mut self,
    loc: Location,
    custom_media: &HashMap<CowArcStr<'i>, CustomMediaRule<'i>>,
  ) -> Result<(), MinifyError> {
    if let Some(condition) = &mut self.condition {
      let used = process_condition(
        loc,
        custom_media,
        &mut self.media_type,
        &mut self.qualifier,
        condition,
        &mut HashSet::new(),
      )?;
      if !used {
        self.condition = None;
      }
    }
    Ok(())
  }

  fn get_necessary_prefixes(&self, targets: Targets) -> VendorPrefix {
    if let Some(condition) = &self.condition {
      condition.get_necessary_prefixes(targets)
    } else {
      VendorPrefix::empty()
    }
  }

  fn transform_resolution(&mut self, prefix: VendorPrefix) {
    if let Some(condition) = &mut self.condition {
      condition.transform_resolution(prefix)
    }
  }

  /// Returns whether the media query is guaranteed to always match.
  pub fn always_matches(&self) -> bool {
    self.qualifier == None && self.media_type == MediaType::All && self.condition == None
  }

  /// Returns whether the media query is guaranteed to never match.
  pub fn never_matches(&self) -> bool {
    self.qualifier == Some(Qualifier::Not) && self.media_type == MediaType::All && self.condition == None
  }

  /// Attempts to combine the given media query into this one. The resulting media query
  /// matches if both of the original media queries would have matched.
  ///
  /// Returns an error if the boolean logic is not possible.
  pub fn and<'a>(&mut self, b: &MediaQuery<'i>) -> Result<(), ()> {
    let at = (&self.qualifier, &self.media_type);
    let bt = (&b.qualifier, &b.media_type);
    let (qualifier, media_type) = match (at, bt) {
      // `not all and screen` => not all
      // `screen and not all` => not all
      ((&Some(Qualifier::Not), &MediaType::All), _) |
      (_, (&Some(Qualifier::Not), &MediaType::All)) => (Some(Qualifier::Not), MediaType::All),
      // `not screen and not print` => ERROR
      // `not screen and not screen` => not screen
      ((&Some(Qualifier::Not), a), (&Some(Qualifier::Not), b)) => {
        if a == b {
          (Some(Qualifier::Not), a.clone())
        } else {
          return Err(())
        }
      },
      // `all and print` => print
      // `print and all` => print
      // `all and not print` => not print
      ((_, MediaType::All), (q, t)) |
      ((q, t), (_, MediaType::All)) |
      // `not screen and print` => print
      // `print and not screen` => print
      ((&Some(Qualifier::Not), _), (q, t)) |
      ((q, t), (&Some(Qualifier::Not), _)) => (q.clone(), t.clone()),
      // `print and screen` => not all
      ((_, a), (_, b)) if a != b => (Some(Qualifier::Not), MediaType::All),
      ((_, a), _) => (None, a.clone())
    };

    self.qualifier = qualifier;
    self.media_type = media_type;

    if let Some(cond) = &b.condition {
      self.condition = if let Some(condition) = &self.condition {
        if condition != cond {
          Some(MediaCondition::Operation {
            conditions: vec![condition.clone(), cond.clone()],
            operator: Operator::And,
          })
        } else {
          Some(condition.clone())
        }
      } else {
        Some(cond.clone())
      }
    }

    Ok(())
  }
}

impl<'i> ToCss for MediaQuery<'i> {
  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
  where
    W: std::fmt::Write,
  {
    if let Some(qual) = self.qualifier {
      qual.to_css(dest)?;
      dest.write_char(' ')?;
    }

    match self.media_type {
      MediaType::All => {
        // We need to print "all" if there's a qualifier, or there's
        // just an empty list of expressions.
        //
        // Otherwise, we'd serialize media queries like "(min-width:
        // 40px)" in "all (min-width: 40px)", which is unexpected.
        if self.qualifier.is_some() || self.condition.is_none() {
          dest.write_str("all")?;
        }
      }
      MediaType::Print => dest.write_str("print")?,
      MediaType::Screen => dest.write_str("screen")?,
      MediaType::Custom(ref desc) => dest.write_str(desc)?,
    }

    let condition = match self.condition {
      Some(ref c) => c,
      None => return Ok(()),
    };

    let needs_parens = if self.media_type != MediaType::All || self.qualifier.is_some() {
      dest.write_str(" and ")?;
      matches!(condition, MediaCondition::Operation { operator, .. } if *operator != Operator::And)
    } else {
      false
    };

    to_css_with_parens_if_needed(condition, dest, needs_parens)
  }
}

#[cfg(feature = "serde")]
#[derive(serde::Deserialize)]
#[serde(untagged)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
enum MediaQueryOrRaw<'i> {
  #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
  MediaQuery {
    qualifier: Option<Qualifier>,
    #[cfg_attr(feature = "serde", serde(borrow))]
    media_type: MediaType<'i>,
    condition: Option<MediaCondition<'i>>,
  },
  Raw {
    raw: CowArcStr<'i>,
  },
}

#[cfg(feature = "serde")]
impl<'i, 'de: 'i> serde::Deserialize<'de> for MediaQuery<'i> {
  fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
  where
    D: serde::Deserializer<'de>,
  {
    let mq = MediaQueryOrRaw::deserialize(deserializer)?;
    match mq {
      MediaQueryOrRaw::MediaQuery {
        qualifier,
        media_type,
        condition,
      } => Ok(MediaQuery {
        qualifier,
        media_type,
        condition,
      }),
      MediaQueryOrRaw::Raw { raw } => {
        let res = MediaQuery::parse_string_with_options(raw.as_ref(), ParserOptions::default())
          .map_err(|_| serde::de::Error::custom("Could not parse value"))?;
        Ok(res.into_owned())
      }
    }
  }
}

enum_property! {
  /// A binary `and` or `or` operator.
  pub enum Operator {
    /// The `and` operator.
    And,
    /// The `or` operator.
    Or,
  }
}

/// Represents a media condition.
#[derive(Clone, Debug, 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 MediaCondition<'i> {
  /// A media feature, implicitly parenthesized.
  #[cfg_attr(feature = "serde", serde(borrow, with = "ValueWrapper::<MediaFeature>"))]
  Feature(MediaFeature<'i>),
  /// A negation of a condition.
  #[cfg_attr(feature = "visitor", skip_type)]
  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<Box<MediaCondition>>"))]
  Not(Box<MediaCondition<'i>>),
  /// A set of joint operations.
  #[cfg_attr(feature = "visitor", skip_type)]
  Operation {
    /// The operator for the conditions.
    operator: Operator,
    /// The conditions for the operator.
    conditions: Vec<MediaCondition<'i>>,
  },
}

/// A trait for conditions such as media queries and container queries.
pub(crate) trait QueryCondition<'i>: Sized {
  fn parse_feature<'t>(
    input: &mut Parser<'i, 't>,
    options: &ParserOptions<'_, 'i>,
  ) -> Result<Self, ParseError<'i, ParserError<'i>>>;
  fn create_negation(condition: Box<Self>) -> Self;
  fn create_operation(operator: Operator, conditions: Vec<Self>) -> Self;
  fn parse_style_query<'t>(
    input: &mut Parser<'i, 't>,
    _options: &ParserOptions<'_, 'i>,
  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
    Err(input.new_error_for_next_token())
  }

  fn needs_parens(&self, parent_operator: Option<Operator>, targets: &Targets) -> bool;
}

impl<'i> QueryCondition<'i> for MediaCondition<'i> {
  #[inline]
  fn parse_feature<'t>(
    input: &mut Parser<'i, 't>,
    options: &ParserOptions<'_, 'i>,
  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
    let feature = MediaFeature::parse_with_options(input, options)?;
    Ok(Self::Feature(feature))
  }

  #[inline]
  fn create_negation(condition: Box<MediaCondition<'i>>) -> Self {
    Self::Not(condition)
  }

  #[inline]
  fn create_operation(operator: Operator, conditions: Vec<MediaCondition<'i>>) -> Self {
    Self::Operation { operator, conditions }
  }

  fn needs_parens(&self, parent_operator: Option<Operator>, targets: &Targets) -> bool {
    match self {
      MediaCondition::Not(_) => true,
      MediaCondition::Operation { operator, .. } => Some(*operator) != parent_operator,
      MediaCondition::Feature(f) => f.needs_parens(parent_operator, targets),
    }
  }
}

bitflags! {
  /// Flags for `parse_query_condition`.
  #[derive(PartialEq, Eq, Clone, Copy)]
  pub(crate) struct QueryConditionFlags: u8 {
    /// Whether to allow top-level "or" boolean logic.
    const ALLOW_OR = 1 << 0;
    /// Whether to allow style container queries.
    const ALLOW_STYLE = 1 << 1;
  }
}

impl<'i> MediaCondition<'i> {
  /// Parse a single media condition.
  fn parse_with_flags<'t>(
    input: &mut Parser<'i, 't>,
    flags: QueryConditionFlags,
    options: &ParserOptions<'_, 'i>,
  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
    parse_query_condition(input, flags, options)
  }

  fn get_necessary_prefixes(&self, targets: Targets) -> VendorPrefix {
    match self {
      MediaCondition::Feature(MediaFeature::Range {
        name: MediaFeatureName::Standard(MediaFeatureId::Resolution),
        ..
      }) => targets.prefixes(VendorPrefix::None, crate::prefixes::Feature::AtResolution),
      MediaCondition::Not(not) => not.get_necessary_prefixes(targets),
      MediaCondition::Operation { conditions, .. } => {
        let mut prefixes = VendorPrefix::empty();
        for condition in conditions {
          prefixes |= condition.get_necessary_prefixes(targets);
        }
        prefixes
      }
      _ => VendorPrefix::empty(),
    }
  }

  fn transform_resolution(&mut self, prefix: VendorPrefix) {
    match self {
      MediaCondition::Feature(MediaFeature::Range {
        name: MediaFeatureName::Standard(MediaFeatureId::Resolution),
        operator,
        value: MediaFeatureValue::Resolution(value),
      }) => match prefix {
        VendorPrefix::WebKit | VendorPrefix::Moz => {
          *self = MediaCondition::Feature(MediaFeature::Range {
            name: MediaFeatureName::Standard(match prefix {
              VendorPrefix::WebKit => MediaFeatureId::WebKitDevicePixelRatio,
              VendorPrefix::Moz => MediaFeatureId::MozDevicePixelRatio,
              _ => unreachable!(),
            }),
            operator: *operator,
            value: MediaFeatureValue::Number(match value {
              Resolution::Dpi(dpi) => *dpi / 96.0,
              Resolution::Dpcm(dpcm) => *dpcm * 2.54 / 96.0,
              Resolution::Dppx(dppx) => *dppx,
            }),
          });
        }
        _ => {}
      },
      MediaCondition::Not(not) => not.transform_resolution(prefix),
      MediaCondition::Operation { conditions, .. } => {
        for condition in conditions {
          condition.transform_resolution(prefix);
        }
      }
      _ => {}
    }
  }
}

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

/// Parse a single query condition.
pub(crate) fn parse_query_condition<'t, 'i, P: QueryCondition<'i>>(
  input: &mut Parser<'i, 't>,
  flags: QueryConditionFlags,
  options: &ParserOptions<'_, 'i>,
) -> Result<P, ParseError<'i, ParserError<'i>>> {
  let location = input.current_source_location();
  let (is_negation, is_style) = match *input.next()? {
    Token::ParenthesisBlock => (false, false),
    Token::Ident(ref ident) if ident.eq_ignore_ascii_case("not") => (true, false),
    Token::Function(ref f)
      if flags.contains(QueryConditionFlags::ALLOW_STYLE) && f.eq_ignore_ascii_case("style") =>
    {
      (false, true)
    }
    ref t => return Err(location.new_unexpected_token_error(t.clone())),
  };

  let first_condition = match (is_negation, is_style) {
    (true, false) => {
      let inner_condition = parse_parens_or_function(input, flags, options)?;
      return Ok(P::create_negation(Box::new(inner_condition)));
    }
    (true, true) => {
      let inner_condition = P::parse_style_query(input, options)?;
      return Ok(P::create_negation(Box::new(inner_condition)));
    }
    (false, false) => parse_paren_block(input, flags, options)?,
    (false, true) => P::parse_style_query(input, options)?,
  };

  let operator = match input.try_parse(Operator::parse) {
    Ok(op) => op,
    Err(..) => return Ok(first_condition),
  };

  if !flags.contains(QueryConditionFlags::ALLOW_OR) && operator == Operator::Or {
    return Err(location.new_unexpected_token_error(Token::Ident("or".into())));
  }

  let mut conditions = vec![];
  conditions.push(first_condition);
  conditions.push(parse_parens_or_function(input, flags, options)?);

  let delim = match operator {
    Operator::And => "and",
    Operator::Or => "or",
  };

  loop {
    if input.try_parse(|i| i.expect_ident_matching(delim)).is_err() {
      return Ok(P::create_operation(operator, conditions));
    }

    conditions.push(parse_parens_or_function(input, flags, options)?);
  }
}

/// Parse a media condition in parentheses, or a style() function.
fn parse_parens_or_function<'t, 'i, P: QueryCondition<'i>>(
  input: &mut Parser<'i, 't>,
  flags: QueryConditionFlags,
  options: &ParserOptions<'_, 'i>,
) -> Result<P, ParseError<'i, ParserError<'i>>> {
  let location = input.current_source_location();
  match *input.next()? {
    Token::ParenthesisBlock => parse_paren_block(input, flags, options),
    Token::Function(ref f)
      if flags.contains(QueryConditionFlags::ALLOW_STYLE) && f.eq_ignore_ascii_case("style") =>
    {
      P::parse_style_query(input, options)
    }
    ref t => return Err(location.new_unexpected_token_error(t.clone())),
  }
}

fn parse_paren_block<'t, 'i, P: QueryCondition<'i>>(
  input: &mut Parser<'i, 't>,
  flags: QueryConditionFlags,
  options: &ParserOptions<'_, 'i>,
) -> Result<P, ParseError<'i, ParserError<'i>>> {
  input.parse_nested_block(|input| {
    if let Ok(inner) =
      input.try_parse(|i| parse_query_condition(i, flags | QueryConditionFlags::ALLOW_OR, options))
    {
      return Ok(inner);
    }

    P::parse_feature(input, options)
  })
}

pub(crate) fn to_css_with_parens_if_needed<V: ToCss, W>(
  value: V,
  dest: &mut Printer<W>,
  needs_parens: bool,
) -> Result<(), PrinterError>
where
  W: std::fmt::Write,
{
  if needs_parens {
    dest.write_char('(')?;
  }
  value.to_css(dest)?;
  if needs_parens {
    dest.write_char(')')?;
  }
  Ok(())
}

pub(crate) fn operation_to_css<'i, V: ToCss + QueryCondition<'i>, W>(
  operator: Operator,
  conditions: &Vec<V>,
  dest: &mut Printer<W>,
) -> Result<(), PrinterError>
where
  W: std::fmt::Write,
{
  let mut iter = conditions.iter();
  let first = iter.next().unwrap();
  to_css_with_parens_if_needed(first, dest, first.needs_parens(Some(operator), &dest.targets.current))?;
  for item in iter {
    dest.write_char(' ')?;
    operator.to_css(dest)?;
    dest.write_char(' ')?;
    to_css_with_parens_if_needed(item, dest, item.needs_parens(Some(operator), &dest.targets.current))?;
  }

  Ok(())
}

impl<'i> MediaCondition<'i> {
  fn negate(&self) -> Option<MediaCondition<'i>> {
    match self {
      MediaCondition::Not(not) => Some((**not).clone()),
      MediaCondition::Feature(f) => f.negate().map(MediaCondition::Feature),
      _ => None,
    }
  }
}

impl<'i> ToCss for MediaCondition<'i> {
  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
  where
    W: std::fmt::Write,
  {
    match *self {
      MediaCondition::Feature(ref f) => f.to_css(dest),
      MediaCondition::Not(ref c) => {
        if let Some(negated) = c.negate() {
          negated.to_css(dest)
        } else {
          dest.write_str("not ")?;
          to_css_with_parens_if_needed(&**c, dest, c.needs_parens(None, &dest.targets.current))
        }
      }
      MediaCondition::Operation {
        ref conditions,
        operator,
      } => operation_to_css(operator, conditions, dest),
    }
  }
}

/// A [comparator](https://drafts.csswg.org/mediaqueries/#typedef-mf-comparison) within a media query.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "visitor", derive(Visit))]
#[cfg_attr(
  feature = "serde",
  derive(serde::Serialize, serde::Deserialize),
  serde(rename_all = "kebab-case")
)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
pub enum MediaFeatureComparison {
  /// `=`
  Equal,
  /// `>`
  GreaterThan,
  /// `>=`
  GreaterThanEqual,
  /// `<`
  LessThan,
  /// `<=`
  LessThanEqual,
}

impl ToCss for MediaFeatureComparison {
  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
  where
    W: std::fmt::Write,
  {
    use MediaFeatureComparison::*;
    match self {
      Equal => dest.delim('=', true),
      GreaterThan => dest.delim('>', true),
      GreaterThanEqual => {
        dest.whitespace()?;
        dest.write_str(">=")?;
        dest.whitespace()
      }
      LessThan => dest.delim('<', true),
      LessThanEqual => {
        dest.whitespace()?;
        dest.write_str("<=")?;
        dest.whitespace()
      }
    }
  }
}

impl MediaFeatureComparison {
  fn opposite(&self) -> MediaFeatureComparison {
    match self {
      MediaFeatureComparison::GreaterThan => MediaFeatureComparison::LessThan,
      MediaFeatureComparison::GreaterThanEqual => MediaFeatureComparison::LessThanEqual,
      MediaFeatureComparison::LessThan => MediaFeatureComparison::GreaterThan,
      MediaFeatureComparison::LessThanEqual => MediaFeatureComparison::GreaterThanEqual,
      MediaFeatureComparison::Equal => MediaFeatureComparison::Equal,
    }
  }

  fn negate(&self) -> MediaFeatureComparison {
    match self {
      MediaFeatureComparison::GreaterThan => MediaFeatureComparison::LessThanEqual,
      MediaFeatureComparison::GreaterThanEqual => MediaFeatureComparison::LessThan,
      MediaFeatureComparison::LessThan => MediaFeatureComparison::GreaterThanEqual,
      MediaFeatureComparison::LessThanEqual => MediaFeatureComparison::GreaterThan,
      MediaFeatureComparison::Equal => MediaFeatureComparison::Equal,
    }
  }
}

/// A generic media feature or container feature.
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(
  feature = "visitor",
  derive(Visit),
  visit(visit_media_feature, MEDIA_QUERIES, <'i, MediaFeatureId>),
  visit(<'i, ContainerSizeFeatureId>)
)]
#[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 QueryFeature<'i, FeatureId> {
  /// A plain media feature, e.g. `(min-width: 240px)`.
  Plain {
    /// The name of the feature.
    #[cfg_attr(feature = "serde", serde(borrow))]
    name: MediaFeatureName<'i, FeatureId>,
    /// The feature value.
    value: MediaFeatureValue<'i>,
  },
  /// A boolean feature, e.g. `(hover)`.
  Boolean {
    /// The name of the feature.
    name: MediaFeatureName<'i, FeatureId>,
  },
  /// A range, e.g. `(width > 240px)`.
  Range {
    /// The name of the feature.
    name: MediaFeatureName<'i, FeatureId>,
    /// A comparator.
    operator: MediaFeatureComparison,
    /// The feature value.
    value: MediaFeatureValue<'i>,
  },
  /// An interval, e.g. `(120px < width < 240px)`.
  #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
  Interval {
    /// The name of the feature.
    name: MediaFeatureName<'i, FeatureId>,
    /// A start value.
    start: MediaFeatureValue<'i>,
    /// A comparator for the start value.
    start_operator: MediaFeatureComparison,
    /// The end value.
    end: MediaFeatureValue<'i>,
    /// A comparator for the end value.
    end_operator: MediaFeatureComparison,
  },
}

/// A [media feature](https://drafts.csswg.org/mediaqueries/#typedef-media-feature)
pub type MediaFeature<'i> = QueryFeature<'i, MediaFeatureId>;

impl<'i, FeatureId> ParseWithOptions<'i> for QueryFeature<'i, FeatureId>
where
  FeatureId: for<'x> Parse<'x> + std::fmt::Debug + PartialEq + ValueType + Clone,
{
  fn parse_with_options<'t>(
    input: &mut Parser<'i, 't>,
    options: &ParserOptions<'_, 'i>,
  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
    match input.try_parse(|input| Self::parse_name_first(input, options)) {
      Ok(res) => Ok(res),
      Err(
        err @ ParseError {
          kind: ParseErrorKind::Custom(ParserError::InvalidMediaQuery),
          ..
        },
      ) => Err(err),
      _ => Self::parse_value_first(input),
    }
  }
}

impl<'i, FeatureId> QueryFeature<'i, FeatureId>
where
  FeatureId: for<'x> Parse<'x> + std::fmt::Debug + PartialEq + ValueType + Clone,
{
  fn parse_name_first<'t>(
    input: &mut Parser<'i, 't>,
    options: &ParserOptions<'_, 'i>,
  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
    let (name, legacy_op) = MediaFeatureName::parse(input)?;

    let operator = input.try_parse(|input| consume_operation_or_colon(input, true));
    let operator = match operator {
      Err(..) => return Ok(QueryFeature::Boolean { name }),
      Ok(operator) => operator,
    };

    if operator.is_some() && legacy_op.is_some() {
      return Err(input.new_custom_error(ParserError::InvalidMediaQuery));
    }

    let value = MediaFeatureValue::parse(input, name.value_type())?;
    if !value.check_type(name.value_type()) {
      if options.error_recovery {
        options.warn(ParseError {
          kind: ParseErrorKind::Custom(ParserError::InvalidMediaQuery),
          location: input.current_source_location(),
        });
      } else {
        return Err(input.new_custom_error(ParserError::InvalidMediaQuery));
      }
    }

    if let Some(operator) = operator.or(legacy_op) {
      if !name.value_type().allows_ranges() {
        return Err(input.new_custom_error(ParserError::InvalidMediaQuery));
      }

      Ok(QueryFeature::Range { name, operator, value })
    } else {
      Ok(QueryFeature::Plain { name, value })
    }
  }

  fn parse_value_first<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
    // We need to find the feature name first so we know the type.
    let start = input.state();
    let name = loop {
      if let Ok((name, legacy_op)) = MediaFeatureName::parse(input) {
        if legacy_op.is_some() {
          return Err(input.new_custom_error(ParserError::InvalidMediaQuery));
        }
        break name;
      }
      if input.is_exhausted() {
        return Err(input.new_custom_error(ParserError::InvalidMediaQuery));
      }
    };

    input.reset(&start);

    // Now we can parse the first value.
    let value = MediaFeatureValue::parse(input, name.value_type())?;
    let operator = consume_operation_or_colon(input, false)?;

    // Skip over the feature name again.
    {
      let (feature_name, _) = MediaFeatureName::parse(input)?;
      debug_assert_eq!(name, feature_name);
    }

    if !name.value_type().allows_ranges() || !value.check_type(name.value_type()) {
      return Err(input.new_custom_error(ParserError::InvalidMediaQuery));
    }

    if let Ok(end_operator) = input.try_parse(|input| consume_operation_or_colon(input, false)) {
      let start_operator = operator.unwrap();
      let end_operator = end_operator.unwrap();
      // Start and end operators must be matching.
      match (start_operator, end_operator) {
        (MediaFeatureComparison::GreaterThan, MediaFeatureComparison::GreaterThan)
        | (MediaFeatureComparison::GreaterThan, MediaFeatureComparison::GreaterThanEqual)
        | (MediaFeatureComparison::GreaterThanEqual, MediaFeatureComparison::GreaterThanEqual)
        | (MediaFeatureComparison::GreaterThanEqual, MediaFeatureComparison::GreaterThan)
        | (MediaFeatureComparison::LessThan, MediaFeatureComparison::LessThan)
        | (MediaFeatureComparison::LessThan, MediaFeatureComparison::LessThanEqual)
        | (MediaFeatureComparison::LessThanEqual, MediaFeatureComparison::LessThanEqual)
        | (MediaFeatureComparison::LessThanEqual, MediaFeatureComparison::LessThan) => {}
        _ => return Err(input.new_custom_error(ParserError::InvalidMediaQuery)),
      };

      let end_value = MediaFeatureValue::parse(input, name.value_type())?;
      if !end_value.check_type(name.value_type()) {
        return Err(input.new_custom_error(ParserError::InvalidMediaQuery));
      }

      Ok(QueryFeature::Interval {
        name,
        start: value,
        start_operator,
        end: end_value,
        end_operator,
      })
    } else {
      let operator = operator.unwrap().opposite();
      Ok(QueryFeature::Range { name, operator, value })
    }
  }

  pub(crate) fn needs_parens(&self, parent_operator: Option<Operator>, targets: &Targets) -> bool {
    if !should_compile!(targets, MediaIntervalSyntax) {
      return false;
    }

    match self {
      QueryFeature::Interval { .. } => parent_operator != Some(Operator::And),
      QueryFeature::Range { operator, .. } => {
        matches!(
          operator,
          MediaFeatureComparison::GreaterThan | MediaFeatureComparison::LessThan
        )
      }
      _ => false,
    }
  }

  fn negate(&self) -> Option<QueryFeature<'i, FeatureId>> {
    match self {
      QueryFeature::Range { name, operator, value } => Some(QueryFeature::Range {
        name: (*name).clone(),
        operator: operator.negate(),
        value: value.clone(),
      }),
      _ => None,
    }
  }
}

impl<'i, FeatureId: FeatureToCss> ToCss for QueryFeature<'i, FeatureId> {
  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
  where
    W: std::fmt::Write,
  {
    match self {
      QueryFeature::Boolean { name } => {
        dest.write_char('(')?;
        name.to_css(dest)?;
      }
      QueryFeature::Plain { name, value } => {
        dest.write_char('(')?;
        name.to_css(dest)?;
        dest.delim(':', false)?;
        value.to_css(dest)?;
      }
      QueryFeature::Range { name, operator, value } => {
        // If range syntax is unsupported, use min/max prefix if possible.
        if should_compile!(dest.targets.current, MediaRangeSyntax) {
          return write_min_max(operator, name, value, dest, false);
        }

        dest.write_char('(')?;
        name.to_css(dest)?;
        operator.to_css(dest)?;
        value.to_css(dest)?;
      }
      QueryFeature::Interval {
        name,
        start,
        start_operator,
        end,
        end_operator,
      } => {
        if should_compile!(dest.targets.current, MediaIntervalSyntax) {
          write_min_max(&start_operator.opposite(), name, start, dest, true)?;
          dest.write_str(" and ")?;
          return write_min_max(end_operator, name, end, dest, true);
        }

        dest.write_char('(')?;
        start.to_css(dest)?;
        start_operator.to_css(dest)?;
        name.to_css(dest)?;
        end_operator.to_css(dest)?;
        end.to_css(dest)?;
      }
    }

    dest.write_char(')')
  }
}

/// A media feature 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(untagged))]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub enum MediaFeatureName<'i, FeatureId> {
  /// A standard media query feature identifier.
  Standard(FeatureId),
  /// A custom author-defined environment variable.
  #[cfg_attr(feature = "serde", serde(borrow))]
  Custom(DashedIdent<'i>),
  /// An unknown environment variable.
  Unknown(Ident<'i>),
}

impl<'i, FeatureId: for<'x> Parse<'x>> MediaFeatureName<'i, FeatureId> {
  /// Parses a media feature name.
  pub fn parse<'t>(
    input: &mut Parser<'i, 't>,
  ) -> Result<(Self, Option<MediaFeatureComparison>), ParseError<'i, ParserError<'i>>> {
    let ident = input.expect_ident()?;

    if ident.starts_with("--") {
      return Ok((MediaFeatureName::Custom(DashedIdent(ident.into())), None));
    }

    let mut name = ident.as_ref();

    // Webkit places its prefixes before "min" and "max". Remove it first, and
    // re-add after removing min/max.
    let is_webkit = starts_with_ignore_ascii_case(&name, "-webkit-");
    if is_webkit {
      name = &name[8..];
    }

    let comparator = if starts_with_ignore_ascii_case(&name, "min-") {
      name = &name[4..];
      Some(MediaFeatureComparison::GreaterThanEqual)
    } else if starts_with_ignore_ascii_case(&name, "max-") {
      name = &name[4..];
      Some(MediaFeatureComparison::LessThanEqual)
    } else {
      None
    };

    let name = if is_webkit {
      Cow::Owned(format!("-webkit-{}", name))
    } else {
      Cow::Borrowed(name)
    };

    if let Ok(standard) = FeatureId::parse_string(&name) {
      return Ok((MediaFeatureName::Standard(standard), comparator));
    }

    Ok((MediaFeatureName::Unknown(Ident(ident.into())), None))
  }
}

mod private {
  use super::*;

  /// A trait for feature ids which can get a value type.
  pub trait ValueType {
    /// Returns the value type for this feature id.
    fn value_type(&self) -> MediaFeatureType;
  }
}

pub(crate) use private::ValueType;

impl<'i, FeatureId: ValueType> ValueType for MediaFeatureName<'i, FeatureId> {
  fn value_type(&self) -> MediaFeatureType {
    match self {
      Self::Standard(standard) => standard.value_type(),
      _ => MediaFeatureType::Unknown,
    }
  }
}

impl<'i, FeatureId: FeatureToCss> ToCss for MediaFeatureName<'i, FeatureId> {
  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
  where
    W: std::fmt::Write,
  {
    match self {
      Self::Standard(v) => v.to_css(dest),
      Self::Custom(v) => v.to_css(dest),
      Self::Unknown(v) => v.to_css(dest),
    }
  }
}

impl<'i, FeatureId: FeatureToCss> FeatureToCss for MediaFeatureName<'i, FeatureId> {
  fn to_css_with_prefix<W>(&self, prefix: &str, dest: &mut Printer<W>) -> Result<(), PrinterError>
  where
    W: std::fmt::Write,
  {
    match self {
      Self::Standard(v) => v.to_css_with_prefix(prefix, dest),
      Self::Custom(v) => {
        dest.write_str(prefix)?;
        v.to_css(dest)
      }
      Self::Unknown(v) => {
        dest.write_str(prefix)?;
        v.to_css(dest)
      }
    }
  }
}

/// The type of a media feature.
#[derive(PartialEq)]
pub enum MediaFeatureType {
  /// A length value.
  Length,
  /// A number value.
  Number,
  /// An integer value.
  Integer,
  /// A boolean value, either 0 or 1.
  Boolean,
  /// A resolution.
  Resolution,
  /// A ratio.
  Ratio,
  /// An identifier.
  Ident,
  /// An unknown type.
  Unknown,
}

impl MediaFeatureType {
  fn allows_ranges(&self) -> bool {
    use MediaFeatureType::*;
    match self {
      Length => true,
      Number => true,
      Integer => true,
      Boolean => false,
      Resolution => true,
      Ratio => true,
      Ident => false,
      Unknown => true,
    }
  }
}

macro_rules! define_query_features {
  (
    $(#[$outer:meta])*
    $vis:vis enum $name:ident {
      $(
        $(#[$meta: meta])*
        $str: literal: $id: ident = $ty: ident,
      )+
    }
  ) => {
    crate::macros::enum_property! {
      $(#[$outer])*
      $vis enum $name {
        $(
          $(#[$meta])*
          $str: $id,
        )+
      }
    }

    impl ValueType for $name {
      fn value_type(&self) -> MediaFeatureType {
        match self {
          $(
            Self::$id => MediaFeatureType::$ty,
          )+
        }
      }
    }
  }
}

pub(crate) use define_query_features;

define_query_features! {
  /// A media query feature identifier.
  pub enum MediaFeatureId {
    /// The [width](https://w3c.github.io/csswg-drafts/mediaqueries-5/#width) media feature.
    "width": Width = Length,
    /// The [height](https://w3c.github.io/csswg-drafts/mediaqueries-5/#height) media feature.
    "height": Height = Length,
    /// The [aspect-ratio](https://w3c.github.io/csswg-drafts/mediaqueries-5/#aspect-ratio) media feature.
    "aspect-ratio": AspectRatio = Ratio,
    /// The [orientation](https://w3c.github.io/csswg-drafts/mediaqueries-5/#orientation) media feature.
    "orientation": Orientation = Ident,
    /// The [overflow-block](https://w3c.github.io/csswg-drafts/mediaqueries-5/#overflow-block) media feature.
    "overflow-block": OverflowBlock = Ident,
    /// The [overflow-inline](https://w3c.github.io/csswg-drafts/mediaqueries-5/#overflow-inline) media feature.
    "overflow-inline": OverflowInline = Ident,
    /// The [horizontal-viewport-segments](https://w3c.github.io/csswg-drafts/mediaqueries-5/#horizontal-viewport-segments) media feature.
    "horizontal-viewport-segments": HorizontalViewportSegments = Integer,
    /// The [vertical-viewport-segments](https://w3c.github.io/csswg-drafts/mediaqueries-5/#vertical-viewport-segments) media feature.
    "vertical-viewport-segments": VerticalViewportSegments = Integer,
    /// The [display-mode](https://w3c.github.io/csswg-drafts/mediaqueries-5/#display-mode) media feature.
    "display-mode": DisplayMode = Ident,
    /// The [resolution](https://w3c.github.io/csswg-drafts/mediaqueries-5/#resolution) media feature.
    "resolution": Resolution = Resolution, // | infinite??
    /// The [scan](https://w3c.github.io/csswg-drafts/mediaqueries-5/#scan) media feature.
    "scan": Scan = Ident,
    /// The [grid](https://w3c.github.io/csswg-drafts/mediaqueries-5/#grid) media feature.
    "grid": Grid = Boolean,
    /// The [update](https://w3c.github.io/csswg-drafts/mediaqueries-5/#update) media feature.
    "update": Update = Ident,
    /// The [environment-blending](https://w3c.github.io/csswg-drafts/mediaqueries-5/#environment-blending) media feature.
    "environment-blending": EnvironmentBlending = Ident,
    /// The [color](https://w3c.github.io/csswg-drafts/mediaqueries-5/#color) media feature.
    "color": Color = Integer,
    /// The [color-index](https://w3c.github.io/csswg-drafts/mediaqueries-5/#color-index) media feature.
    "color-index": ColorIndex = Integer,
    /// The [monochrome](https://w3c.github.io/csswg-drafts/mediaqueries-5/#monochrome) media feature.
    "monochrome": Monochrome = Integer,
    /// The [color-gamut](https://w3c.github.io/csswg-drafts/mediaqueries-5/#color-gamut) media feature.
    "color-gamut": ColorGamut = Ident,
    /// The [dynamic-range](https://w3c.github.io/csswg-drafts/mediaqueries-5/#dynamic-range) media feature.
    "dynamic-range": DynamicRange = Ident,
    /// The [inverted-colors](https://w3c.github.io/csswg-drafts/mediaqueries-5/#inverted-colors) media feature.
    "inverted-colors": InvertedColors = Ident,
    /// The [pointer](https://w3c.github.io/csswg-drafts/mediaqueries-5/#pointer) media feature.
    "pointer": Pointer = Ident,
    /// The [hover](https://w3c.github.io/csswg-drafts/mediaqueries-5/#hover) media feature.
    "hover": Hover = Ident,
    /// The [any-pointer](https://w3c.github.io/csswg-drafts/mediaqueries-5/#any-pointer) media feature.
    "any-pointer": AnyPointer = Ident,
    /// The [any-hover](https://w3c.github.io/csswg-drafts/mediaqueries-5/#any-hover) media feature.
    "any-hover": AnyHover = Ident,
    /// The [nav-controls](https://w3c.github.io/csswg-drafts/mediaqueries-5/#nav-controls) media feature.
    "nav-controls": NavControls = Ident,
    /// The [video-color-gamut](https://w3c.github.io/csswg-drafts/mediaqueries-5/#video-color-gamut) media feature.
    "video-color-gamut": VideoColorGamut = Ident,
    /// The [video-dynamic-range](https://w3c.github.io/csswg-drafts/mediaqueries-5/#video-dynamic-range) media feature.
    "video-dynamic-range": VideoDynamicRange = Ident,
    /// The [scripting](https://w3c.github.io/csswg-drafts/mediaqueries-5/#scripting) media feature.
    "scripting": Scripting = Ident,
    /// The [prefers-reduced-motion](https://w3c.github.io/csswg-drafts/mediaqueries-5/#prefers-reduced-motion) media feature.
    "prefers-reduced-motion": PrefersReducedMotion = Ident,
    /// The [prefers-reduced-transparency](https://w3c.github.io/csswg-drafts/mediaqueries-5/#prefers-reduced-transparency) media feature.
    "prefers-reduced-transparency": PrefersReducedTransparency = Ident,
    /// The [prefers-contrast](https://w3c.github.io/csswg-drafts/mediaqueries-5/#prefers-contrast) media feature.
    "prefers-contrast": PrefersContrast = Ident,
    /// The [forced-colors](https://w3c.github.io/csswg-drafts/mediaqueries-5/#forced-colors) media feature.
    "forced-colors": ForcedColors = Ident,
    /// The [prefers-color-scheme](https://w3c.github.io/csswg-drafts/mediaqueries-5/#prefers-color-scheme) media feature.
    "prefers-color-scheme": PrefersColorScheme = Ident,
    /// The [prefers-reduced-data](https://w3c.github.io/csswg-drafts/mediaqueries-5/#prefers-reduced-data) media feature.
    "prefers-reduced-data": PrefersReducedData = Ident,
    /// The [device-width](https://w3c.github.io/csswg-drafts/mediaqueries-5/#device-width) media feature.
    "device-width": DeviceWidth = Length,
    /// The [device-height](https://w3c.github.io/csswg-drafts/mediaqueries-5/#device-height) media feature.
    "device-height": DeviceHeight = Length,
    /// The [device-aspect-ratio](https://w3c.github.io/csswg-drafts/mediaqueries-5/#device-aspect-ratio) media feature.
    "device-aspect-ratio": DeviceAspectRatio = Ratio,

    /// The non-standard -webkit-device-pixel-ratio media feature.
    "-webkit-device-pixel-ratio": WebKitDevicePixelRatio = Number,
    /// The non-standard -moz-device-pixel-ratio media feature.
    "-moz-device-pixel-ratio": MozDevicePixelRatio = Number,

    // TODO: parse non-standard media queries?
    // -moz-device-orientation
    // -webkit-transform-3d
  }
}

pub(crate) trait FeatureToCss: ToCss {
  fn to_css_with_prefix<W>(&self, prefix: &str, dest: &mut Printer<W>) -> Result<(), PrinterError>
  where
    W: std::fmt::Write;
}

impl FeatureToCss for MediaFeatureId {
  fn to_css_with_prefix<W>(&self, prefix: &str, dest: &mut Printer<W>) -> Result<(), PrinterError>
  where
    W: std::fmt::Write,
  {
    match self {
      MediaFeatureId::WebKitDevicePixelRatio => {
        dest.write_str("-webkit-")?;
        dest.write_str(prefix)?;
        dest.write_str("device-pixel-ratio")
      }
      _ => {
        dest.write_str(prefix)?;
        self.to_css(dest)
      }
    }
  }
}

#[inline]
fn write_min_max<W, FeatureId: FeatureToCss>(
  operator: &MediaFeatureComparison,
  name: &MediaFeatureName<FeatureId>,
  value: &MediaFeatureValue,
  dest: &mut Printer<W>,
  is_range: bool,
) -> Result<(), PrinterError>
where
  W: std::fmt::Write,
{
  let prefix = match operator {
    MediaFeatureComparison::GreaterThan => {
      if is_range {
        dest.write_char('(')?;
      }
      dest.write_str("not ")?;
      Some("max-")
    }
    MediaFeatureComparison::GreaterThanEqual => Some("min-"),
    MediaFeatureComparison::LessThan => {
      if is_range {
        dest.write_char('(')?;
      }
      dest.write_str("not ")?;
      Some("min-")
    }
    MediaFeatureComparison::LessThanEqual => Some("max-"),
    MediaFeatureComparison::Equal => None,
  };

  dest.write_char('(')?;
  if let Some(prefix) = prefix {
    name.to_css_with_prefix(prefix, dest)?;
  } else {
    name.to_css(dest)?;
  }

  dest.delim(':', false)?;
  value.to_css(dest)?;

  if is_range
    && matches!(
      operator,
      MediaFeatureComparison::GreaterThan | MediaFeatureComparison::LessThan
    )
  {
    dest.write_char(')')?;
  }

  dest.write_char(')')?;
  Ok(())
}

/// [media feature value](https://drafts.csswg.org/mediaqueries/#typedef-mf-value) within a media query.
///
/// See [MediaFeature](MediaFeature).
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "visitor", derive(Visit), visit(visit_media_feature_value, MEDIA_QUERIES))]
#[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 MediaFeatureValue<'i> {
  /// A length value.
  Length(Length),
  /// A number value.
  Number(CSSNumber),
  /// An integer value.
  Integer(CSSInteger),
  /// A boolean value.
  Boolean(bool),
  /// A resolution.
  Resolution(Resolution),
  /// A ratio.
  Ratio(Ratio),
  /// An identifier.
  #[cfg_attr(feature = "serde", serde(borrow))]
  Ident(Ident<'i>),
  /// An environment variable reference.
  Env(EnvironmentVariable<'i>),
}

impl<'i> MediaFeatureValue<'i> {
  fn value_type(&self) -> MediaFeatureType {
    use MediaFeatureValue::*;
    match self {
      Length(..) => MediaFeatureType::Length,
      Number(..) => MediaFeatureType::Number,
      Integer(..) => MediaFeatureType::Integer,
      Boolean(..) => MediaFeatureType::Boolean,
      Resolution(..) => MediaFeatureType::Resolution,
      Ratio(..) => MediaFeatureType::Ratio,
      Ident(..) => MediaFeatureType::Ident,
      Env(..) => MediaFeatureType::Unknown,
    }
  }

  fn check_type(&self, expected_type: MediaFeatureType) -> bool {
    match (expected_type, self.value_type()) {
      (_, MediaFeatureType::Unknown) | (MediaFeatureType::Unknown, _) => true,
      (a, b) => a == b,
    }
  }
}

impl<'i> MediaFeatureValue<'i> {
  /// Parses a single media query feature value, with an expected type.
  /// If the type is unknown, pass MediaFeatureType::Unknown instead.
  pub fn parse<'t>(
    input: &mut Parser<'i, 't>,
    expected_type: MediaFeatureType,
  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
    if let Ok(value) = input.try_parse(|input| Self::parse_known(input, expected_type)) {
      return Ok(value);
    }

    Self::parse_unknown(input)
  }

  fn parse_known<'t>(
    input: &mut Parser<'i, 't>,
    expected_type: MediaFeatureType,
  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
    match expected_type {
      MediaFeatureType::Boolean => {
        let value = CSSInteger::parse(input)?;
        if value != 0 && value != 1 {
          return Err(input.new_custom_error(ParserError::InvalidValue));
        }
        Ok(MediaFeatureValue::Boolean(value == 1))
      }
      MediaFeatureType::Number => Ok(MediaFeatureValue::Number(CSSNumber::parse(input)?)),
      MediaFeatureType::Integer => Ok(MediaFeatureValue::Integer(CSSInteger::parse(input)?)),
      MediaFeatureType::Length => Ok(MediaFeatureValue::Length(Length::parse(input)?)),
      MediaFeatureType::Resolution => Ok(MediaFeatureValue::Resolution(Resolution::parse(input)?)),
      MediaFeatureType::Ratio => Ok(MediaFeatureValue::Ratio(Ratio::parse(input)?)),
      MediaFeatureType::Ident => Ok(MediaFeatureValue::Ident(Ident::parse(input)?)),
      MediaFeatureType::Unknown => Err(input.new_custom_error(ParserError::InvalidValue)),
    }
  }

  fn parse_unknown<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
    // Ratios are ambiguous with numbers because the second param is optional (e.g. 2/1 == 2).
    // We require the / delimiter when parsing ratios so that 2/1 ends up as a ratio and 2 is
    // parsed as a number.
    if let Ok(ratio) = input.try_parse(Ratio::parse_required) {
      return Ok(MediaFeatureValue::Ratio(ratio));
    }

    // Parse number next so that unitless values are not parsed as lengths.
    if let Ok(num) = input.try_parse(CSSNumber::parse) {
      return Ok(MediaFeatureValue::Number(num));
    }

    if let Ok(length) = input.try_parse(Length::parse) {
      return Ok(MediaFeatureValue::Length(length));
    }

    if let Ok(res) = input.try_parse(Resolution::parse) {
      return Ok(MediaFeatureValue::Resolution(res));
    }

    if let Ok(env) = input.try_parse(|input| EnvironmentVariable::parse(input, &ParserOptions::default(), 0)) {
      return Ok(MediaFeatureValue::Env(env));
    }

    let ident = Ident::parse(input)?;
    Ok(MediaFeatureValue::Ident(ident))
  }
}

impl<'i> ToCss for MediaFeatureValue<'i> {
  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
  where
    W: std::fmt::Write,
  {
    match self {
      MediaFeatureValue::Length(len) => len.to_css(dest),
      MediaFeatureValue::Number(num) => num.to_css(dest),
      MediaFeatureValue::Integer(num) => num.to_css(dest),
      MediaFeatureValue::Boolean(b) => {
        if *b {
          dest.write_char('1')
        } else {
          dest.write_char('0')
        }
      }
      MediaFeatureValue::Resolution(res) => res.to_css(dest),
      MediaFeatureValue::Ratio(ratio) => ratio.to_css(dest),
      MediaFeatureValue::Ident(id) => {
        id.to_css(dest)?;
        Ok(())
      }
      MediaFeatureValue::Env(env) => env.to_css(dest, false),
    }
  }
}

/// Consumes an operation or a colon, or returns an error.
fn consume_operation_or_colon<'i, 't>(
  input: &mut Parser<'i, 't>,
  allow_colon: bool,
) -> Result<Option<MediaFeatureComparison>, ParseError<'i, ParserError<'i>>> {
  let location = input.current_source_location();
  let first_delim = {
    let location = input.current_source_location();
    let next_token = input.next()?;
    match next_token {
      Token::Colon if allow_colon => return Ok(None),
      Token::Delim(oper) => oper,
      t => return Err(location.new_unexpected_token_error(t.clone())),
    }
  };
  Ok(Some(match first_delim {
    '=' => MediaFeatureComparison::Equal,
    '>' => {
      if input.try_parse(|i| i.expect_delim('=')).is_ok() {
        MediaFeatureComparison::GreaterThanEqual
      } else {
        MediaFeatureComparison::GreaterThan
      }
    }
    '<' => {
      if input.try_parse(|i| i.expect_delim('=')).is_ok() {
        MediaFeatureComparison::LessThanEqual
      } else {
        MediaFeatureComparison::LessThan
      }
    }
    d => return Err(location.new_unexpected_token_error(Token::Delim(*d))),
  }))
}

fn process_condition<'i>(
  loc: Location,
  custom_media: &HashMap<CowArcStr<'i>, CustomMediaRule<'i>>,
  media_type: &mut MediaType<'i>,
  qualifier: &mut Option<Qualifier>,
  condition: &mut MediaCondition<'i>,
  seen: &mut HashSet<DashedIdent<'i>>,
) -> Result<bool, MinifyError> {
  match condition {
    MediaCondition::Not(cond) => {
      let used = process_condition(loc, custom_media, media_type, qualifier, &mut *cond, seen)?;
      if !used {
        // If unused, only a media type remains so apply a not qualifier.
        // If it is already not, then it cancels out.
        *qualifier = if *qualifier == Some(Qualifier::Not) {
          None
        } else {
          Some(Qualifier::Not)
        };
        return Ok(false);
      }

      // Unwrap nested nots
      match &**cond {
        MediaCondition::Not(cond) => {
          *condition = (**cond).clone();
        }
        _ => {}
      }
    }
    MediaCondition::Operation { conditions, .. } => {
      let mut res = Ok(true);
      conditions.retain_mut(|condition| {
        let r = process_condition(loc, custom_media, media_type, qualifier, condition, seen);
        if let Ok(used) = r {
          used
        } else {
          res = r;
          false
        }
      });
      return res;
    }
    MediaCondition::Feature(QueryFeature::Boolean { name }) => {
      let name = match name {
        MediaFeatureName::Custom(name) => name,
        _ => return Ok(true),
      };

      if seen.contains(name) {
        return Err(ErrorWithLocation {
          kind: MinifyErrorKind::CircularCustomMedia { name: name.to_string() },
          loc,
        });
      }

      let rule = custom_media.get(&name.0).ok_or_else(|| ErrorWithLocation {
        kind: MinifyErrorKind::CustomMediaNotDefined { name: name.to_string() },
        loc,
      })?;

      seen.insert(name.clone());

      let mut res = Ok(true);
      let mut conditions: Vec<MediaCondition> = rule
        .query
        .media_queries
        .iter()
        .filter_map(|query| {
          if query.media_type != MediaType::All || query.qualifier != None {
            if *media_type == MediaType::All {
              // `not all` will never match.
              if *qualifier == Some(Qualifier::Not) {
                res = Ok(false);
                return None;
              }

              // Propagate media type and qualifier to @media rule.
              *media_type = query.media_type.clone();
              *qualifier = query.qualifier.clone();
            } else if query.media_type != *media_type || query.qualifier != *qualifier {
              // Boolean logic with media types is hard to emulate, so we error for now.
              res = Err(ErrorWithLocation {
                kind: MinifyErrorKind::UnsupportedCustomMediaBooleanLogic {
                  custom_media_loc: rule.loc,
                },
                loc,
              });
              return None;
            }
          }

          if let Some(condition) = &query.condition {
            let mut condition = condition.clone();
            let r = process_condition(loc, custom_media, media_type, qualifier, &mut condition, seen);
            if r.is_err() {
              res = r;
            }
            // Parentheses are required around the condition unless there is a single media feature.
            match condition {
              MediaCondition::Feature(..) => Some(condition),
              _ => Some(condition),
            }
          } else {
            None
          }
        })
        .collect();

      seen.remove(name);

      if res.is_err() {
        return res;
      }

      if conditions.is_empty() {
        return Ok(false);
      }

      if conditions.len() == 1 {
        *condition = conditions.pop().unwrap();
      } else {
        *condition = MediaCondition::Operation {
          conditions,
          operator: Operator::Or,
        };
      }
    }
    _ => {}
  }

  Ok(true)
}

#[cfg(test)]
mod tests {
  use super::*;
  use crate::{
    stylesheet::PrinterOptions,
    targets::{Browsers, Targets},
  };

  fn parse(s: &str) -> MediaQuery {
    let mut input = ParserInput::new(&s);
    let mut parser = Parser::new(&mut input);
    MediaQuery::parse_with_options(&mut parser, &ParserOptions::default()).unwrap()
  }

  fn and(a: &str, b: &str) -> String {
    let mut a = parse(a);
    let b = parse(b);
    a.and(&b).unwrap();
    a.to_css_string(PrinterOptions::default()).unwrap()
  }

  #[test]
  fn test_and() {
    assert_eq!(and("(min-width: 250px)", "(color)"), "(width >= 250px) and (color)");
    assert_eq!(
      and("(min-width: 250px) or (color)", "(orientation: landscape)"),
      "((width >= 250px) or (color)) and (orientation: landscape)"
    );
    assert_eq!(
      and("(min-width: 250px) and (color)", "(orientation: landscape)"),
      "(width >= 250px) and (color) and (orientation: landscape)"
    );
    assert_eq!(and("all", "print"), "print");
    assert_eq!(and("print", "all"), "print");
    assert_eq!(and("all", "not print"), "not print");
    assert_eq!(and("not print", "all"), "not print");
    assert_eq!(and("not all", "print"), "not all");
    assert_eq!(and("print", "not all"), "not all");
    assert_eq!(and("print", "screen"), "not all");
    assert_eq!(and("not print", "screen"), "screen");
    assert_eq!(and("print", "not screen"), "print");
    assert_eq!(and("not screen", "print"), "print");
    assert_eq!(and("not screen", "not all"), "not all");
    assert_eq!(and("print", "(min-width: 250px)"), "print and (width >= 250px)");
    assert_eq!(and("(min-width: 250px)", "print"), "print and (width >= 250px)");
    assert_eq!(
      and("print and (min-width: 250px)", "(color)"),
      "print and (width >= 250px) and (color)"
    );
    assert_eq!(and("all", "only screen"), "only screen");
    assert_eq!(and("only screen", "all"), "only screen");
    assert_eq!(and("print", "print"), "print");
  }

  #[test]
  fn test_negated_interval_parens() {
    let media_query = parse("screen and not (200px <= width < 500px)");
    let printer_options = PrinterOptions {
      targets: Targets {
        browsers: Some(Browsers {
          chrome: Some(95 << 16),
          ..Default::default()
        }),
        ..Default::default()
      },
      ..Default::default()
    };
    assert_eq!(
      media_query.to_css_string(printer_options).unwrap(),
      "screen and not ((min-width: 200px) and (not (min-width: 500px)))"
    );
  }
}
