use crate::format_element::{Document, FormatElement, InternedDocument, LineMode, TextMetrics};

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum IndentStyle {
    #[default]
    Tab,
    Space,
}

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum LineEnding {
    #[default]
    Lf,
    CrLf,
}

impl LineEnding {
    const fn as_str(self) -> &'static str {
        match self {
            Self::Lf => "\n",
            Self::CrLf => "\r\n",
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PrinterOptions {
    pub indent_style: IndentStyle,
    pub indent_width: u8,
    pub line_width: u16,
    pub line_ending: LineEnding,
}

impl Default for PrinterOptions {
    fn default() -> Self {
        Self {
            indent_style: IndentStyle::Tab,
            indent_width: 4,
            line_width: 80,
            line_ending: LineEnding::Lf,
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Printed {
    code: String,
}

impl Printed {
    #[must_use]
    pub fn as_code(&self) -> &str {
        &self.code
    }

    #[must_use]
    pub fn into_code(self) -> String {
        self.code
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PrintError {
    message: String,
}

impl std::fmt::Display for PrintError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(&self.message)
    }
}

impl std::error::Error for PrintError {}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum PrintMode {
    Flat,
    Expanded,
}

#[derive(Debug, Clone)]
pub struct Printer {
    options: PrinterOptions,
}

impl Printer {
    #[must_use]
    pub fn new(options: PrinterOptions) -> Self {
        Self { options }
    }

    pub fn print(&self, document: &Document) -> Result<Printed, PrintError> {
        self.print_with_capacity(document, 0)
    }

    pub fn print_with_capacity(
        &self,
        document: &Document,
        output_capacity: usize,
    ) -> Result<Printed, PrintError> {
        let mut output = String::with_capacity(output_capacity);
        let mut state = PrinterState::new(self.options);
        let mut queue = PrintQueue::new(document.as_slice());
        let mut mode_stack = Vec::new();

        while let Some(element) = queue.next(&mut state, &mut mode_stack) {
            match element {
                FormatElement::Token(text) => state.push_token(text, &mut output),
                FormatElement::Text(text) => {
                    state.push_text(text.as_str(), text.metrics(), &mut output);
                }
                FormatElement::Space => state.push_token(" ", &mut output),
                FormatElement::Line(line_mode) => {
                    let mode = mode_stack.last().copied().unwrap_or(PrintMode::Expanded);
                    match (mode, line_mode) {
                        (PrintMode::Flat, LineMode::Soft) => {}
                        (PrintMode::Flat, LineMode::SoftOrSpace) => {
                            state.push_token(" ", &mut output);
                        }
                        (_, LineMode::Hard) | (PrintMode::Expanded, _) => {
                            state.push_newline(&mut output);
                        }
                    }
                }
                FormatElement::Indent(document) => {
                    queue.push(document.as_slice(), None, 1, &mut state, &mut mode_stack);
                }
                FormatElement::Group(document) => {
                    let mode = if self.fits(document.as_slice(), PrintMode::Flat, state.column) {
                        PrintMode::Flat
                    } else {
                        PrintMode::Expanded
                    };
                    queue.push(
                        document.as_slice(),
                        Some(mode),
                        0,
                        &mut state,
                        &mut mode_stack,
                    );
                }
                FormatElement::BestFit { flat, expanded } => {
                    let (variant, mode) =
                        if self.fits(flat.as_slice(), PrintMode::Flat, state.column) {
                            (flat.as_slice(), PrintMode::Flat)
                        } else {
                            (expanded.as_slice(), PrintMode::Expanded)
                        };
                    queue.push(variant, Some(mode), 0, &mut state, &mut mode_stack);
                }
                FormatElement::Verbatim(text) => {
                    state.push_text(text.as_str(), text.metrics(), &mut output);
                }
            }
        }

        Ok(Printed { code: output })
    }

    fn fits(&self, document: &[FormatElement], mode: PrintMode, column: usize) -> bool {
        let mut width = column;
        let mut queue = MeasureQueue::new(document, mode);
        let line_width = usize::from(self.options.line_width);

        while let Some((element, mode)) = queue.next() {
            match element {
                FormatElement::Token(text) => width += text.len(),
                FormatElement::Text(text) => {
                    if let Some(single_line_width) = text.metrics().single_line_width() {
                        width += single_line_width;
                    } else {
                        width += text.metrics().first_line_width();
                        return width <= line_width;
                    }
                }
                FormatElement::Space => width += 1,
                FormatElement::Line(line_mode) => match (mode, line_mode) {
                    (PrintMode::Flat, LineMode::Soft) => {}
                    (PrintMode::Flat, LineMode::SoftOrSpace) => width += 1,
                    (_, LineMode::Hard) | (PrintMode::Expanded, _) => return width <= line_width,
                },
                FormatElement::Indent(document) => queue.push(document, None),
                FormatElement::Group(document) => queue.push(document, Some(PrintMode::Flat)),
                FormatElement::BestFit { flat, .. } => queue.push(flat, Some(PrintMode::Flat)),
                FormatElement::Verbatim(text) => {
                    if let Some(single_line_width) = text.metrics().single_line_width() {
                        width += single_line_width;
                    } else {
                        width += text.metrics().first_line_width();
                        return width <= line_width;
                    }
                }
            }

            if width > line_width {
                return false;
            }
        }

        true
    }
}

#[derive(Debug, Clone)]
struct PrintFrame<'a> {
    elements: &'a [FormatElement],
    index: usize,
    mode: Option<PrintMode>,
    indent_delta: usize,
}

#[derive(Debug, Clone)]
struct PrintQueue<'a> {
    frames: Vec<PrintFrame<'a>>,
}

impl<'a> PrintQueue<'a> {
    fn new(elements: &'a [FormatElement]) -> Self {
        Self {
            frames: vec![PrintFrame {
                elements,
                index: 0,
                mode: None,
                indent_delta: 0,
            }],
        }
    }

    fn push(
        &mut self,
        elements: &'a [FormatElement],
        mode: Option<PrintMode>,
        indent_delta: usize,
        state: &mut PrinterState,
        mode_stack: &mut Vec<PrintMode>,
    ) {
        if indent_delta > 0 {
            state.indent_level += indent_delta;
        }
        if let Some(mode) = mode {
            mode_stack.push(mode);
        }
        self.frames.push(PrintFrame {
            elements,
            index: 0,
            mode,
            indent_delta,
        });
    }

    fn next(
        &mut self,
        state: &mut PrinterState,
        mode_stack: &mut Vec<PrintMode>,
    ) -> Option<&'a FormatElement> {
        loop {
            let frame = self.frames.last_mut()?;
            if frame.index < frame.elements.len() {
                let element = &frame.elements[frame.index];
                frame.index += 1;
                return Some(element);
            }

            let frame = self.frames.pop()?;
            state.indent_level = state.indent_level.saturating_sub(frame.indent_delta);
            if frame.mode.is_some() {
                mode_stack.pop();
            }
        }
    }
}

#[derive(Debug, Clone)]
struct MeasureFrame<'a> {
    elements: &'a [FormatElement],
    index: usize,
    mode: Option<PrintMode>,
}

#[derive(Debug, Clone)]
struct MeasureQueue<'a> {
    frames: Vec<MeasureFrame<'a>>,
    mode_stack: Vec<PrintMode>,
}

impl<'a> MeasureQueue<'a> {
    fn new(elements: &'a [FormatElement], mode: PrintMode) -> Self {
        Self {
            frames: vec![MeasureFrame {
                elements,
                index: 0,
                mode: Some(mode),
            }],
            mode_stack: vec![mode],
        }
    }

    fn push(&mut self, document: &'a InternedDocument, mode: Option<PrintMode>) {
        if let Some(mode) = mode {
            self.mode_stack.push(mode);
        }
        self.frames.push(MeasureFrame {
            elements: document.as_slice(),
            index: 0,
            mode,
        });
    }

    fn next(&mut self) -> Option<(&'a FormatElement, PrintMode)> {
        loop {
            let frame = self.frames.last_mut()?;
            if frame.index < frame.elements.len() {
                let element = &frame.elements[frame.index];
                frame.index += 1;
                let mode = self
                    .mode_stack
                    .last()
                    .copied()
                    .unwrap_or(PrintMode::Expanded);
                return Some((element, mode));
            }

            let frame = self.frames.pop()?;
            if frame.mode.is_some() {
                self.mode_stack.pop();
            }
        }
    }
}

#[derive(Debug, Clone, Copy)]
struct PrinterState {
    options: PrinterOptions,
    indent_level: usize,
    column: usize,
    line_has_content: bool,
}

impl PrinterState {
    fn new(options: PrinterOptions) -> Self {
        Self {
            options,
            indent_level: 0,
            column: 0,
            line_has_content: false,
        }
    }

    fn push_token(&mut self, text: &str, output: &mut String) {
        if !self.line_has_content {
            self.push_indent(output);
        }
        output.push_str(text);
        self.column += text.len();
        self.line_has_content = true;
    }

    fn push_text(&mut self, text: &str, metrics: TextMetrics, output: &mut String) {
        if !self.line_has_content {
            self.push_indent(output);
        }

        output.push_str(text);
        if let Some(width) = metrics.single_line_width() {
            self.column += width;
            self.line_has_content = true;
        } else {
            self.column = if metrics.ends_with_newline() {
                0
            } else {
                metrics.last_line_width()
            };
            self.line_has_content = !metrics.ends_with_newline();
        }
    }

    fn push_newline(&mut self, output: &mut String) {
        output.push_str(self.options.line_ending.as_str());
        self.column = 0;
        self.line_has_content = false;
    }

    fn push_indent(&mut self, output: &mut String) {
        if self.line_has_content || self.indent_level == 0 {
            return;
        }

        match self.options.indent_style {
            IndentStyle::Tab => {
                for _ in 0..self.indent_level {
                    output.push('\t');
                }
            }
            IndentStyle::Space => {
                for _ in 0..(self.indent_level * usize::from(self.options.indent_width)) {
                    output.push(' ');
                }
            }
        }

        self.column += self.indent_width();
        self.line_has_content = true;
    }

    fn indent_width(&self) -> usize {
        self.indent_level * usize::from(self.options.indent_width)
    }
}
