use ruff_formatter::write;
use ruff_python_ast::{AnyStringKind, FString};
use ruff_source_file::Locator;

use crate::prelude::*;
use crate::preview::is_f_string_formatting_enabled;
use crate::string::{Quoting, StringNormalizer, StringQuotes};

use super::f_string_element::FormatFStringElement;

/// Formats an f-string which is part of a larger f-string expression.
///
/// For example, this would be used to format the f-string part in `"foo" f"bar {x}"`
/// or the standalone f-string in `f"foo {x} bar"`.
pub(crate) struct FormatFString<'a> {
    value: &'a FString,
    /// The quoting of an f-string. This is determined by the parent node
    /// (f-string expression) and is required to format an f-string correctly.
    quoting: Quoting,
}

impl<'a> FormatFString<'a> {
    pub(crate) fn new(value: &'a FString, quoting: Quoting) -> Self {
        Self { value, quoting }
    }
}

impl Format<PyFormatContext<'_>> for FormatFString<'_> {
    fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> {
        let locator = f.context().locator();

        let normalizer = StringNormalizer::from_context(f.context())
            .with_quoting(self.quoting)
            .with_preferred_quote_style(f.options().quote_style());

        // If f-string formatting is disabled (not in preview), then we will
        // fall back to the previous behavior of normalizing the f-string.
        if !is_f_string_formatting_enabled(f.context()) {
            let result = normalizer.normalize(self.value.into(), &locator).fmt(f);
            let comments = f.context().comments();
            self.value.elements.iter().for_each(|value| {
                comments.mark_verbatim_node_comments_formatted(value.into());
                // Above method doesn't mark the trailing comments of the f-string elements
                // as formatted, so we need to do it manually. For example,
                //
                // ```python
                // f"""foo {
                //     x:.3f
                //     # comment
                // }"""
                // ```
                for trailing_comment in comments.trailing(value) {
                    trailing_comment.mark_formatted();
                }
            });
            return result;
        }

        let string_kind = normalizer.choose_quotes(self.value.into(), &locator).kind();

        let context = FStringContext::new(
            string_kind,
            FStringLayout::from_f_string(self.value, &locator),
        );

        // Starting prefix and quote
        let quotes = StringQuotes::from(string_kind);
        write!(f, [string_kind.prefix(), quotes])?;

        f.join()
            .entries(
                self.value
                    .elements
                    .iter()
                    .map(|element| FormatFStringElement::new(element, context)),
            )
            .finish()?;

        // Ending quote
        quotes.fmt(f)
    }
}

#[derive(Clone, Copy, Debug)]
pub(crate) struct FStringContext {
    kind: AnyStringKind,
    layout: FStringLayout,
}

impl FStringContext {
    const fn new(kind: AnyStringKind, layout: FStringLayout) -> Self {
        Self { kind, layout }
    }

    pub(crate) fn kind(self) -> AnyStringKind {
        self.kind
    }

    pub(crate) const fn layout(self) -> FStringLayout {
        self.layout
    }
}

#[derive(Copy, Clone, Debug)]
pub(crate) enum FStringLayout {
    /// Original f-string is flat.
    /// Don't break expressions to keep the string flat.
    Flat,
    /// Original f-string has multiline expressions in the replacement fields.
    /// Allow breaking expressions across multiple lines.
    Multiline,
}

impl FStringLayout {
    fn from_f_string(f_string: &FString, locator: &Locator) -> Self {
        // Heuristic: Allow breaking the f-string expressions across multiple lines
        // only if there already is at least one multiline expression. This puts the
        // control in the hands of the user to decide if they want to break the
        // f-string expressions across multiple lines or not. This is similar to
        // how Prettier does it for template literals in JavaScript.
        //
        // If it's single quoted f-string and it contains a multiline expression, then we
        // assume that the target version of Python supports it (3.12+). If there are comments
        // used in any of the expression of the f-string, then it's always going to be multiline
        // and we assume that the target version of Python supports it (3.12+).
        //
        // Reference: https://prettier.io/docs/en/next/rationale.html#template-literals
        if f_string
            .expressions()
            .any(|expr| memchr::memchr2(b'\n', b'\r', locator.slice(expr).as_bytes()).is_some())
        {
            Self::Multiline
        } else {
            Self::Flat
        }
    }

    pub(crate) const fn is_flat(self) -> bool {
        matches!(self, Self::Flat)
    }
}
