//! The `@supports` rule.

use std::collections::HashMap;

use super::Location;
use super::{CssRuleList, MinifyContext};
use crate::error::{MinifyError, ParserError, PrinterError};
use crate::parser::DefaultAtRule;
use crate::printer::Printer;
use crate::properties::custom::TokenList;
use crate::properties::PropertyId;
use crate::targets::{Features, FeaturesIterator, Targets};
use crate::traits::{Parse, ToCss};
use crate::values::string::CowArcStr;
use crate::vendor_prefix::VendorPrefix;
#[cfg(feature = "visitor")]
use crate::visitor::Visit;
use cssparser::*;

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

/// A [@supports](https://drafts.csswg.org/css-conditional-3/#at-supports) rule.
#[derive(Debug, PartialEq, Clone)]
#[cfg_attr(feature = "visitor", derive(Visit))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
pub struct SupportsRule<'i, R = DefaultAtRule> {
  /// The supports condition.
  #[cfg_attr(feature = "serde", serde(borrow))]
  pub condition: SupportsCondition<'i>,
  /// The rules within the `@supports` rule.
  pub rules: CssRuleList<'i, R>,
  /// The location of the rule in the source file.
  #[cfg_attr(feature = "visitor", skip_visit)]
  pub loc: Location,
}

impl<'i, T: Clone> SupportsRule<'i, T> {
  pub(crate) fn minify(
    &mut self,
    context: &mut MinifyContext<'_, 'i>,
    parent_is_unused: bool,
  ) -> Result<(), MinifyError> {
    let inserted = context.targets.enter_supports(self.condition.get_supported_features());
    if inserted {
      context.handler_context.targets = context.targets.current;
    }

    self.condition.set_prefixes_for_targets(&context.targets.current);
    let result = self.rules.minify(context, parent_is_unused);

    if inserted {
      context.targets.exit_supports();
      context.handler_context.targets = context.targets.current;
    }
    result
  }
}

impl<'a, 'i, T: ToCss> ToCss for SupportsRule<'i, T> {
  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
  where
    W: std::fmt::Write,
  {
    #[cfg(feature = "sourcemap")]
    dest.add_mapping(self.loc);
    dest.write_str("@supports ")?;
    self.condition.to_css(dest)?;
    dest.whitespace()?;
    dest.write_char('{')?;
    dest.indent();
    dest.newline()?;

    let inserted = dest.targets.enter_supports(self.condition.get_supported_features());
    self.rules.to_css(dest)?;
    if inserted {
      dest.targets.exit_supports();
    }

    dest.dedent();
    dest.newline()?;
    dest.write_char('}')
  }
}

/// A [`<supports-condition>`](https://drafts.csswg.org/css-conditional-3/#typedef-supports-condition),
/// as used in the `@supports` and `@import` rules.
#[derive(Debug, PartialEq, Clone)]
#[cfg_attr(feature = "visitor", derive(Visit))]
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
#[cfg_attr(feature = "visitor", visit(visit_supports_condition, SUPPORTS_CONDITIONS))]
#[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 SupportsCondition<'i> {
  /// A `not` expression.
  #[cfg_attr(feature = "visitor", skip_type)]
  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<Box<SupportsCondition>>"))]
  Not(Box<SupportsCondition<'i>>),
  /// An `and` expression.
  #[cfg_attr(feature = "visitor", skip_type)]
  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<Vec<SupportsCondition>>"))]
  And(Vec<SupportsCondition<'i>>),
  /// An `or` expression.
  #[cfg_attr(feature = "visitor", skip_type)]
  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<Vec<SupportsCondition>>"))]
  Or(Vec<SupportsCondition<'i>>),
  /// A declaration to evaluate.
  Declaration {
    /// The property id for the declaration.
    #[cfg_attr(feature = "serde", serde(borrow, rename = "propertyId"))]
    property_id: PropertyId<'i>,
    /// The raw value of the declaration.
    value: CowArcStr<'i>,
  },
  /// A selector to evaluate.
  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<CowArcStr>"))]
  Selector(CowArcStr<'i>),
  // FontTechnology()
  /// An unknown condition.
  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<CowArcStr>"))]
  Unknown(CowArcStr<'i>),
}

impl<'i> SupportsCondition<'i> {
  /// Combines the given supports condition into this one with an `and` expression.
  pub fn and(&mut self, b: &SupportsCondition<'i>) {
    if let SupportsCondition::And(a) = self {
      if !a.contains(&b) {
        a.push(b.clone());
      }
    } else if self != b {
      *self = SupportsCondition::And(vec![self.clone(), b.clone()])
    }
  }

  /// Combines the given supports condition into this one with an `or` expression.
  pub fn or(&mut self, b: &SupportsCondition<'i>) {
    if let SupportsCondition::Or(a) = self {
      if !a.contains(&b) {
        a.push(b.clone());
      }
    } else if self != b {
      *self = SupportsCondition::Or(vec![self.clone(), b.clone()])
    }
  }

  fn set_prefixes_for_targets(&mut self, targets: &Targets) {
    match self {
      SupportsCondition::Not(cond) => cond.set_prefixes_for_targets(targets),
      SupportsCondition::And(items) | SupportsCondition::Or(items) => {
        for item in items {
          item.set_prefixes_for_targets(targets);
        }
      }
      SupportsCondition::Declaration { property_id, .. } => {
        let prefix = property_id.prefix();
        if prefix.is_empty() || prefix.contains(VendorPrefix::None) {
          property_id.set_prefixes_for_targets(*targets);
        }
      }
      _ => {}
    }
  }

  fn get_supported_features(&self) -> Features {
    fn get_supported_features_internal(value: &SupportsCondition) -> Option<Features> {
      match value {
        SupportsCondition::And(list) => list.iter().map(|c| get_supported_features_internal(c)).try_union_all(),
        SupportsCondition::Declaration { value, .. } => {
          let mut input = ParserInput::new(&value);
          let mut parser = Parser::new(&mut input);
          if let Ok(tokens) = TokenList::parse(&mut parser, &Default::default(), 0) {
            Some(tokens.get_features())
          } else {
            Some(Features::empty())
          }
        }
        // bail out if "not" or "or" exists for now
        SupportsCondition::Not(_) | SupportsCondition::Or(_) => None,
        SupportsCondition::Selector(_) | SupportsCondition::Unknown(_) => Some(Features::empty()),
      }
    }

    get_supported_features_internal(self).unwrap_or(Features::empty())
  }
}

impl<'i> Parse<'i> for SupportsCondition<'i> {
  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
    if input.try_parse(|input| input.expect_ident_matching("not")).is_ok() {
      let in_parens = Self::parse_in_parens(input)?;
      return Ok(SupportsCondition::Not(Box::new(in_parens)));
    }

    let in_parens = Self::parse_in_parens(input)?;
    let mut expected_type = None;
    let mut conditions = Vec::new();
    let mut seen_declarations = HashMap::new();

    loop {
      let condition = input.try_parse(|input| {
        let location = input.current_source_location();
        let s = input.expect_ident()?;
        let found_type = match_ignore_ascii_case! { &s,
          "and" => 1,
          "or" => 2,
          _ => return Err(location.new_unexpected_token_error(
            cssparser::Token::Ident(s.clone())
          ))
        };

        if let Some(expected) = expected_type {
          if found_type != expected {
            return Err(location.new_unexpected_token_error(cssparser::Token::Ident(s.clone())));
          }
        } else {
          expected_type = Some(found_type);
        }

        Self::parse_in_parens(input)
      });

      if let Ok(condition) = condition {
        if conditions.is_empty() {
          conditions.push(in_parens.clone());
          if let SupportsCondition::Declaration { property_id, value } = &in_parens {
            seen_declarations.insert((property_id.with_prefix(VendorPrefix::None), value.clone()), 0);
          }
        }

        if let SupportsCondition::Declaration { property_id, value } = condition {
          // Merge multiple declarations with the same property id (minus prefix) and value together.
          let property_id = property_id.with_prefix(VendorPrefix::None);
          let key = (property_id.clone(), value.clone());
          if let Some(index) = seen_declarations.get(&key) {
            if let SupportsCondition::Declaration {
              property_id: cur_property,
              ..
            } = &mut conditions[*index]
            {
              cur_property.add_prefix(property_id.prefix());
            }
          } else {
            seen_declarations.insert(key, conditions.len());
            conditions.push(SupportsCondition::Declaration { property_id, value });
          }
        } else {
          conditions.push(condition);
        }
      } else {
        break;
      }
    }

    if conditions.len() == 1 {
      return Ok(conditions.pop().unwrap());
    }

    match expected_type {
      Some(1) => Ok(SupportsCondition::And(conditions)),
      Some(2) => Ok(SupportsCondition::Or(conditions)),
      _ => Ok(in_parens),
    }
  }
}

impl<'i> SupportsCondition<'i> {
  fn parse_in_parens<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
    input.skip_whitespace();
    let location = input.current_source_location();
    let pos = input.position();
    match input.next()? {
      Token::Function(ref f) => {
        match_ignore_ascii_case! { &*f,
          "selector" => {
            let res = input.try_parse(|input| {
              input.parse_nested_block(|input| {
                let pos = input.position();
                input.expect_no_error_token()?;
                Ok(SupportsCondition::Selector(input.slice_from(pos).into()))
              })
            });
            if res.is_ok() {
              return res
            }
          },
          _ => {}
        }
      }
      Token::ParenthesisBlock => {
        let res = input.try_parse(|input| {
          input.parse_nested_block(|input| {
            if let Ok(condition) = input.try_parse(SupportsCondition::parse) {
              return Ok(condition);
            }

            Self::parse_declaration(input)
          })
        });
        if res.is_ok() {
          return res;
        }
      }
      t => return Err(location.new_unexpected_token_error(t.clone())),
    };

    input.parse_nested_block(|input| input.expect_no_error_token().map_err(|err| err.into()))?;
    Ok(SupportsCondition::Unknown(input.slice_from(pos).into()))
  }

  pub(crate) fn parse_declaration<'t>(
    input: &mut Parser<'i, 't>,
  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
    let property_id = PropertyId::parse(input)?;
    input.expect_colon()?;
    input.skip_whitespace();
    let pos = input.position();
    input.expect_no_error_token()?;
    Ok(SupportsCondition::Declaration {
      property_id,
      value: input.slice_from(pos).into(),
    })
  }

  fn needs_parens(&self, parent: &SupportsCondition) -> bool {
    match self {
      SupportsCondition::Not(_) => true,
      SupportsCondition::And(_) => !matches!(parent, SupportsCondition::And(_)),
      SupportsCondition::Or(_) => !matches!(parent, SupportsCondition::Or(_)),
      _ => false,
    }
  }

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

impl<'i> ToCss for SupportsCondition<'i> {
  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
  where
    W: std::fmt::Write,
  {
    match self {
      SupportsCondition::Not(condition) => {
        dest.write_str("not ")?;
        condition.to_css_with_parens_if_needed(dest, condition.needs_parens(self))
      }
      SupportsCondition::And(conditions) => {
        let mut first = true;
        for condition in conditions {
          if first {
            first = false;
          } else {
            dest.write_str(" and ")?;
          }
          condition.to_css_with_parens_if_needed(dest, condition.needs_parens(self))?;
        }
        Ok(())
      }
      SupportsCondition::Or(conditions) => {
        let mut first = true;
        for condition in conditions {
          if first {
            first = false;
          } else {
            dest.write_str(" or ")?;
          }
          condition.to_css_with_parens_if_needed(dest, condition.needs_parens(self))?;
        }
        Ok(())
      }
      SupportsCondition::Declaration { property_id, value } => {
        dest.write_char('(')?;

        let prefix = property_id.prefix().or_none();
        if prefix != VendorPrefix::None {
          dest.write_char('(')?;
        }

        let name = property_id.name();
        let mut first = true;
        for p in prefix {
          if first {
            first = false;
          } else {
            dest.write_str(") or (")?;
          }

          p.to_css(dest)?;
          serialize_name(name, dest)?;
          dest.delim(':', false)?;
          dest.write_str(value)?;
        }

        if prefix != VendorPrefix::None {
          dest.write_char(')')?;
        }

        dest.write_char(')')
      }
      SupportsCondition::Selector(sel) => {
        dest.write_str("selector(")?;
        dest.write_str(sel)?;
        dest.write_char(')')
      }
      SupportsCondition::Unknown(unknown) => dest.write_str(&unknown),
    }
  }
}
