use std::{cell::Cell, num::NonZeroU8};

use crate::GroupId;

use super::PrintMode;

/// A Tag marking the start and end of some content to which some special formatting should be applied.
///
/// Tags always come in pairs of a start and an end tag and the styling defined by this tag
/// will be applied to all elements in between the start/end tags.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Tag {
    /// Indents the content one level deeper, see [crate::builders::indent] for documentation and examples.
    StartIndent,
    EndIndent,

    /// Variant of `Indent` that indents content by a number of spaces. For example, `Align(2)`
    /// indents any content following a line break by an additional two spaces.
    ///
    /// Nesting Aligns has the effect that all except the most inner align are handled as `Indent`.
    StartAlign(Align),
    EndAlign,

    /// Reduces the indention of the specified content either by one level or to the root, depending on the mode.
    /// Reverse operation of `Indent` and can be used to *undo* an `Align` for nested content.
    StartDedent(DedentMode),
    EndDedent(DedentMode),

    /// Creates a logical group where its content is either consistently printed:
    /// * on a single line: Omitting `LineMode::Soft` line breaks and printing spaces for `LineMode::SoftOrSpace`
    /// * on multiple lines: Printing all line breaks
    ///
    /// See [crate::builders::group] for documentation and examples.
    StartGroup(Group),
    EndGroup,

    /// Allows to specify content that gets printed depending on whatever the enclosing group
    /// is printed on a single line or multiple lines. See [crate::builders::if_group_breaks] for examples.
    StartConditionalContent(Condition),
    EndConditionalContent,

    /// Optimized version of [Tag::StartConditionalContent] for the case where some content
    /// should be indented if the specified group breaks.
    StartIndentIfGroupBreaks(GroupId),
    EndIndentIfGroupBreaks(GroupId),

    /// Concatenates multiple elements together with a given separator printed in either
    /// flat or expanded mode to fill the print width. Expect that the content is a list of alternating
    /// [element, separator] See [crate::Formatter::fill].
    StartFill,
    EndFill,

    /// Entry inside of a [Tag::StartFill]
    StartEntry,
    EndEntry,

    /// Delay the printing of its content until the next line break
    StartLineSuffix,
    EndLineSuffix,

    /// Special semantic element marking the content with a label.
    /// This does not directly influence how the content will be printed.
    ///
    /// See [crate::builders::labelled] for documentation.
    StartLabelled(LabelId),
    EndLabelled,

    /// Marks the current indention as the root that [LineMode::Literal](super::LineMode::Literal)
    /// line breaks and [DedentMode::Root] dedents return to.
    /// See [crate::builders::mark_as_root] for documentation.
    StartMarkAsRoot,
    EndMarkAsRoot,
}

impl Tag {
    /// Returns `true` if `self` is any start tag.
    pub const fn is_start(&self) -> bool {
        matches!(
            self,
            Tag::StartIndent
                | Tag::StartAlign(_)
                | Tag::StartDedent(_)
                | Tag::StartGroup { .. }
                | Tag::StartConditionalContent(_)
                | Tag::StartIndentIfGroupBreaks(_)
                | Tag::StartFill
                | Tag::StartEntry
                | Tag::StartLineSuffix
                | Tag::StartLabelled(_)
                | Tag::StartMarkAsRoot
        )
    }

    /// Returns `true` if `self` is any end tag.
    pub const fn is_end(&self) -> bool {
        !self.is_start()
    }

    pub const fn kind(&self) -> TagKind {
        use Tag::{
            EndAlign, EndConditionalContent, EndDedent, EndEntry, EndFill, EndGroup, EndIndent,
            EndIndentIfGroupBreaks, EndLabelled, EndLineSuffix, EndMarkAsRoot, StartAlign,
            StartConditionalContent, StartDedent, StartEntry, StartFill, StartGroup, StartIndent,
            StartIndentIfGroupBreaks, StartLabelled, StartLineSuffix, StartMarkAsRoot,
        };

        match self {
            StartIndent | EndIndent => TagKind::Indent,
            StartAlign(_) | EndAlign => TagKind::Align,
            StartDedent(_) | EndDedent(_) => TagKind::Dedent,
            StartGroup(_) | EndGroup => TagKind::Group,
            StartConditionalContent(_) | EndConditionalContent => TagKind::ConditionalContent,
            StartIndentIfGroupBreaks(_) | EndIndentIfGroupBreaks(_) => TagKind::IndentIfGroupBreaks,
            StartFill | EndFill => TagKind::Fill,
            StartEntry | EndEntry => TagKind::Entry,
            StartLineSuffix | EndLineSuffix => TagKind::LineSuffix,
            StartLabelled(_) | EndLabelled => TagKind::Labelled,
            StartMarkAsRoot | EndMarkAsRoot => TagKind::MarkAsRoot,
        }
    }
}

/// The kind of a [Tag].
///
/// Each start end tag pair has its own [tag kind](TagKind).
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum TagKind {
    Indent,
    Align,
    Dedent,
    Group,
    ConditionalContent,
    IndentIfGroupBreaks,
    Fill,
    Entry,
    LineSuffix,
    Labelled,
    TailwindClass,
    MarkAsRoot,
}

#[derive(Debug, Copy, Default, Clone, Eq, PartialEq)]
pub enum GroupMode {
    /// Print group in flat mode.
    #[default]
    Flat,

    /// The group should be printed in expanded mode
    Expand,

    /// Expand mode has been propagated from an enclosing group to this group.
    Propagated,
}

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

#[derive(Debug, Clone, Eq, PartialEq, Default)]
pub struct Group {
    id: Option<GroupId>,
    mode: Cell<GroupMode>,
}

impl Group {
    pub fn new() -> Self {
        Self { id: None, mode: Cell::new(GroupMode::Flat) }
    }

    #[must_use]
    pub fn with_id(mut self, id: Option<GroupId>) -> Self {
        self.id = id;
        self
    }

    #[must_use]
    pub fn with_mode(mut self, mode: GroupMode) -> Self {
        self.mode = Cell::new(mode);
        self
    }

    pub fn mode(&self) -> GroupMode {
        self.mode.get()
    }

    pub fn propagate_expand(&self) {
        if self.mode.get() == GroupMode::Flat {
            self.mode.set(GroupMode::Propagated);
        }
    }

    pub fn id(&self) -> Option<GroupId> {
        self.id
    }
}

#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum DedentMode {
    /// Reduces the indent by a level (if the current indent is > 0)
    Level,

    /// Reduces the indent to the root
    Root,
}

#[derive(Debug, Clone, Eq, PartialEq)]
pub struct Condition {
    /// * Flat -> Omitted if the enclosing group is a multiline group, printed for groups fitting on a single line
    /// * Multiline -> Omitted if the enclosing group fits on a single line, printed if the group breaks over multiple lines.
    pub(crate) mode: PrintMode,

    /// The id of the group for which it should check if it breaks or not. The group must appear in the document
    /// before the conditional group (but doesn't have to be in the ancestor chain).
    pub(crate) group_id: Option<GroupId>,
}

impl Condition {
    pub fn new(mode: PrintMode) -> Self {
        Self { mode, group_id: None }
    }

    #[must_use]
    pub fn with_group_id(mut self, id: Option<GroupId>) -> Self {
        self.group_id = id;
        self
    }

    pub fn mode(&self) -> PrintMode {
        self.mode
    }

    pub fn group_id(&self) -> Option<GroupId> {
        self.group_id
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Align(pub(crate) NonZeroU8);

impl Align {
    pub fn new(count: NonZeroU8) -> Self {
        Self(count)
    }

    pub fn count(&self) -> NonZeroU8 {
        self.0
    }
}

#[derive(Debug, Eq, Copy, Clone)]
pub struct LabelId {
    value: u64,
    #[cfg(debug_assertions)]
    name: &'static str,
}

impl PartialEq for LabelId {
    fn eq(&self, other: &Self) -> bool {
        let is_equal = self.value == other.value;

        #[cfg(debug_assertions)]
        {
            if is_equal {
                assert_eq!(
                    self.name, other.name,
                    "Two `LabelId`s with different names have the same `value`. Are you mixing labels of two different `LabelDefinition` or are the values returned by the `LabelDefinition` not unique?"
                );
            }
        }

        is_equal
    }
}

impl LabelId {
    #[expect(clippy::needless_pass_by_value)] // The `Label` trait is unnecessary, would refactor it later.
    pub fn of<T: Label>(label: T) -> Self {
        Self {
            value: label.id(),
            #[cfg(debug_assertions)]
            name: label.debug_name(),
        }
    }
}

/// Defines the valid labels of a language. You want to have at most one implementation per formatter
/// project.
pub trait Label {
    /// Returns the `u64` uniquely identifying this specific label.
    fn id(&self) -> u64;

    /// Returns the name of the label that is shown in debug builds.
    fn debug_name(&self) -> &'static str;
}
