use crate::diagnostics::{Diagnostic, Severity};
use crate::lexer::{TokenSpan, TokenizedFile};

use core::convert::Into;
use crossterm::style::Stylize;
use std::cmp::max;
use std::collections::HashMap;
use std::ops::RangeInclusive;

const INDENT: &str = "  ";
/*
error[P0034]: missing `{` to start `switch` body
  in file: files/P0034_04.uc
   1 |    ╭switch
     |    │------ `switch` starts here
   2 |  ╭ │(
   3 |  │ │    A
   4 |  │ │)
     |  │ ╰─^ expected `{` before end of file
     |  ╰  ^ switch selector is here




     Outmost guideline isn't reaching ^!. And should it be `^` even?
 */
/*
error: expected one of `,`, `:`, or `}`, found `token_to`
  --> rottlib/src/ast/mod.rs:80:13
   |
78 |         Self {
   |         ---- while parsing this struct
79 |             token_from: self.token_from,scd
   |                                         --- while parsing this struct field
80 |             token_to: std::cmp::max(self.token_to, right_most_index),
   |             ^^^^^^^^ expected one of `,`, `:`, or `}`
 */

/*
   |
76 | /             "asdasdas
77 | |             asd1
78 | |             asd2
79 | |             asdasd"
   | |___________________^ expected `()`, found `&str`
 */

/*
   1. Get each span's range and total lines covered by spans as ranges;
   2. We need `+N` more lines for `N` labels;
   3.
*/


// TODO: check if blue guidelines are sometimes red or vice versa
// TODO: tabs needs to be replaced with 1-width character
// These are abstract rendering events, not self-contained draw commands.
// They are emitted in increasing order of "significant lines" (range starts/ends).
// The actual source span for a label is recovered later from its LabelType.
#[derive(PartialEq, Eq, Clone, Copy)]
enum RendererCommands {
    StartRange {
        label_type: LabelType,
        column: usize,
    },
    FinishRange {
        label_type: LabelType,
        column: usize,
    },
    SingleRange {
        label_type: LabelType,
    },
}

enum LineIndexType {
    Normal(usize),
    Missing,
    Ellipsis,
}

// Label ordering is semantic: primary first, then secondaries in diagnostic order.
// That order is also used to break visual ties when multiple labels would otherwise
// start or end on the same source line.
#[derive(PartialEq, Eq, Hash, Clone, Copy)]
enum LabelType {
    Primary,
    Secondary(usize),
}

struct RangeSet {
    primary_range: Option<RangeInclusive<usize>>,
    secondary_ranges: Vec<RangeInclusive<usize>>,
}

impl RangeSet {
    /*fn get(&self, index: usize) -> Option<&RangeInclusive<usize>> {
        if self.primary_range.is_some() {
            if index == 0 {
                return self.primary_range.as_ref();
            } else {
                self.secondary_ranges.get(index - 1)
            }
        } else {
            self.secondary_ranges.get(index)
        }
    }

    fn len(&self) -> usize {
        self.secondary_ranges.len() + if self.primary_range.is_some() { 1 } else { 0 }
    }*/

    fn iter(&self) -> impl Iterator<Item = &RangeInclusive<usize>> {
        self.primary_range
            .iter()
            .chain(self.secondary_ranges.iter())
    }

    fn iter_labeled(&self) -> impl Iterator<Item = (LabelType, &RangeInclusive<usize>)> {
        self.primary_range
            .iter()
            .map(|range| (LabelType::Primary, range))
            .chain(
                self.secondary_ranges
                    .iter()
                    .enumerate()
                    .map(|(index, range)| (LabelType::Secondary(index), range)),
            )
    }

    fn get_first_bound_above(&self, line_number: Option<usize>) -> Option<usize> {
        self.iter()
            .filter_map(|range| {
                let start = *range.start();
                let end = *range.end();

                let start_ok = line_number.is_none_or(|n| start > n).then_some(start);
                let end_ok = line_number.is_none_or(|n| end > n).then_some(end);

                match (start_ok, end_ok) {
                    (Some(a), Some(b)) => Some(a.min(b)),
                    (Some(a), None) => Some(a),
                    (None, Some(b)) => Some(b),
                    (None, None) => None,
                }
            })
            .min()
    }
}

// Converts labeled line ranges into an ordered stream of renderer events.
//
// Important invariants:
//
// 1. Commands are ordered by increasing significant line.
//    A significant line is any line on which some label starts or ends.
//
// 2. If multiple labels would visually terminate on the same source line,
//    the renderer treats them as ending on distinct phantom rows, ordered by
//    diagnostic priority (primary/secondary order). This prevents intersections
//    and means that same-line closings are intentionally linearized rather than
//    treated as a geometric tie.
//
// 3. RendererCommands do not store source line numbers directly.
//    Later rendering recovers the underlying span from LabelType and uses the
//    event order to know when labels become active/inactive.
//
// 4. When a label starts on the same significant line where another label ends,
//    starts are processed first. This is intentional: longer-lived/opening labels
//    must occupy earlier columns so that shorter-lived/closing labels bend around
//    them without intersecting.
fn make_renderer_commands(ranges: RangeSet) -> Vec<(usize, RendererCommands)> {
    // Maps currently-open labels to the index of their StartRange command so that
    // we can patch in the final column once the label closes.
    let mut open_ranges = HashMap::new();
    let mut commands = Vec::new();
    let mut current_line = None;
    while let Some(next_significant_line) = ranges.get_first_bound_above(current_line) {
        current_line = Some(next_significant_line);
        // First process all new ranges because they'll live longer and have
        // to have earlier columns
        for (label, range) in ranges.iter_labeled() {
            if *range.start() == next_significant_line {
                if range.start() != range.end() {
                    commands.push((
                        *range.start(),
                        RendererCommands::StartRange {
                            label_type: label,
                            column: 0,
                        },
                    ));
                    open_ranges.insert(label, commands.len() - 1);
                } else {
                    commands.push((
                        *range.start(),
                        RendererCommands::SingleRange { label_type: label },
                    ));
                }
            }
        }

        // Closing pass.
        // The assigned column is the number of ranges that remain open after removing
        // this label. Because same-line visual ties are already linearized by label
        // priority / phantom rows, processing labels in iter_labeled() order is
        // intentional here.
        for (label, range) in ranges.iter_labeled() {
            if *range.end() == next_significant_line {
                if let Some(index) = open_ranges.remove(&label) {
                    // Column meaning:
                    //   0 = outermost / earliest lane
                    //   larger values = further inward lanes
                    //
                    // We assign the column at close time, not at open time, because the final lane
                    // depends on which other ranges outlive this one.
                    let column = open_ranges.len();
                    if let Some((line_number, RendererCommands::StartRange { .. })) =
                        commands.get(index)
                    {
                        commands[index] = (
                            *line_number,
                            RendererCommands::StartRange {
                                label_type: label,
                                column,
                            },
                        );
                    }
                    commands.push((
                        *range.end(),
                        RendererCommands::FinishRange {
                            label_type: label,
                            column,
                        },
                    ));
                }
            }
        }
    }
    commands
}

fn max_line_number_width(ranges: &RangeSet) -> usize {
    let max_line = ranges.iter().map(|range| *range.end()).max().unwrap_or(0);

    if max_line == 0 {
        1
    } else {
        max_line.ilog10() as usize + 1
    }
}

fn span_to_range<'src>(
    span: TokenSpan,
    file: &TokenizedFile<'src>,
) -> Option<RangeInclusive<usize>> {
    let start_line = file.token_line(span.start)?;
    let end_line = file.token_line(span.end)?;

    if start_line <= end_line {
        Some(start_line..=end_line)
    } else {
        None
    }
}

fn make_ranges<'src>(file: &TokenizedFile<'src>, diagnostic: &Diagnostic) -> RangeSet {
    let mut result = RangeSet {
        primary_range: None,
        secondary_ranges: Vec::new(),
    };
    result.primary_range = diagnostic
        .primary_label()
        .and_then(|label| span_to_range(label.span, file));
    for secondary in diagnostic.secondary_labels() {
        if let Some(range) = span_to_range(secondary.span, file) {
            result.secondary_ranges.push(range);
        }
    }
    result
}

impl Diagnostic {
    pub fn render<'src>(&self, file: &TokenizedFile<'src>, file_path: impl Into<String>) {
        self.render_header();
        println!("{INDENT}{}: {}", "in file".blue().bold(), file_path.into());
        self.render_lines(file);
        self.render_help_and_notes(file);
    }
    /*StartRange {
        label_type: LabelType,
        column: usize,
    },
    FinishRange {
        label_type: LabelType,
    },
    SingleRange {
        label_type: LabelType,
    }, */
    fn label_data(&self, label_type: LabelType) -> Option<(TokenSpan, String)> {
        match label_type {
            LabelType::Primary => self
                .primary_label()
                .map(|label| (label.span, label.message.clone())),
            LabelType::Secondary(id) => Some((
                self.secondary_labels()[id].span,
                self.secondary_labels()[id].message.clone(),
            )),
        }
    }
    fn render_lines<'src>(&self, file: &TokenizedFile<'src>) {
        let ranges = make_ranges(file, &self);
        let max_line_number_width = max(max_line_number_width(&ranges), 3);
        let commands = make_renderer_commands(ranges);
        let mut max_column = 0;
        for command in &commands {
            if let (_, RendererCommands::StartRange { column, .. }) = command {
                max_column = max(max_column, *column);
            }
        }
        let mut vertical_stack = Vec::new();
        vertical_stack.resize(max_column + 1, None);

        let mut i = 0;
        while i < commands.len() {
            let mut current_line = commands[i].0;
            let mut single_commands = Vec::new();
            let mut start_commands = Vec::new();
            let mut finish_commands = Vec::new();
            while i < commands.len() && current_line == commands[i].0 {
                match commands[i].1 {
                    RendererCommands::SingleRange { label_type } => {
                        single_commands.push(label_type)
                    }
                    RendererCommands::StartRange { label_type, column } => {
                        start_commands.push((label_type, column));
                    }
                    RendererCommands::FinishRange { label_type, column } => {
                        finish_commands.push((label_type, column))
                    }
                }
                i += 1;
            }
            // !!!!!!!!!!!!!!!!
            // First - update line drawing stack
            // First - update line drawing stack
            for &(label_type, column) in &start_commands {
                vertical_stack[column] = Some(label_type);
            }
            // Next - draw the line
            self.draw_line_with_starts(
                current_line,
                max_line_number_width,
                file,
                &vertical_stack,
                &start_commands,
            );
            for label_type in single_commands {
                self.render_single_command(
                    label_type,
                    max_line_number_width,
                    file,
                    &vertical_stack,
                );
            }
            // Next - render finish commands (drop for now)
            for (label_type, column) in finish_commands {
                self.render_finish_command(
                    label_type,
                    max_line_number_width,
                    file,
                    &vertical_stack,
                );
                vertical_stack[column] = None;
            }
            // !!!!!!!!!!!!!!!!
            // Render some more lines
            let mut countdown = 3;
            current_line += 1;
            while i < commands.len() && current_line < commands[i].0 {
                if countdown == 0 {
                    if current_line + 1 == commands[i].0 {
                        self.draw_line(current_line, max_line_number_width, file, &vertical_stack);
                    } else {
                        println!(
                            "{}",
                            self.make_line_prefix(
                                LineIndexType::Ellipsis,
                                max_line_number_width,
                                &vertical_stack
                            )
                        );
                    }
                    break;
                } else {
                    self.draw_line(current_line, max_line_number_width, file, &vertical_stack);
                }
                current_line += 1;
                countdown -= 1;
            }
        }
    }

    fn render_single_command<'src>(
        &self,
        label_type: LabelType,
        max_line_number_width: usize,
        file: &TokenizedFile<'src>,
        vertical_stack: &[Option<LabelType>],
    ) {
        let Some((span, message)) = self.label_data(label_type) else {
            return;
        };
        let Some(visible) = file.span_visible_on_line(span) else {
            return;
        };

        let mut builder = self.make_line_prefix(
            LineIndexType::Missing,
            max_line_number_width,
            vertical_stack,
        );

        builder.push_str(&" ".repeat(visible.columns.start));

        let underline_width = (visible.columns.end - visible.columns.start).max(1);
        let mut underline_label = if label_type == LabelType::Primary {
            "^".repeat(underline_width)
        } else {
            "-".repeat(underline_width)
        };
        underline_label.push_str(&format!(" {}", message));

        match label_type {
            LabelType::Primary => {
                if self.severity == Severity::Error {
                    builder.push_str(&underline_label.red().bold().to_string());
                } else {
                    builder.push_str(&underline_label.yellow().bold().to_string());
                }
            }
            LabelType::Secondary(_) => {
                builder.push_str(&underline_label.blue().bold().to_string());
            }
        }

        println!("{builder}");
    }

    fn render_finish_command<'src>(
        &self,
        label_type: LabelType,
        max_line_number_width: usize,
        file: &TokenizedFile<'src>,
        vertical_stack: &[Option<LabelType>],
    ) {
        let Some((span, message)) = self.label_data(label_type) else {
            return;
        };

        let Some(visible) = file
            .token_visible_spans(span.end)
            .and_then(|spans| spans.into_iter().last())
        else {
            return;
        };

        let mut builder =
            self.make_finish_prefix(max_line_number_width, vertical_stack, label_type);

        builder.push_str(&"─".repeat(visible.columns.start).red().to_string());

        let underline_width = (visible.columns.end - visible.columns.start).max(1);
        let mut underline_label = "^".repeat(underline_width);
        underline_label.push_str(&format!(" {}", message));

        match label_type {
            LabelType::Primary => {
                if self.severity == Severity::Error {
                    builder.push_str(&underline_label.red().bold().to_string());
                } else {
                    builder.push_str(&underline_label.yellow().bold().to_string());
                }
            }
            LabelType::Secondary(_) => {
                builder.push_str(&underline_label.blue().bold().to_string());
            }
        }

        println!("{builder}");
    }

    fn draw_line<'src>(
        &self,
        current_line: usize,
        max_line_number_width: usize,
        file: &TokenizedFile<'src>,
        vertical_stack: &[Option<LabelType>],
    ) {
        println!(
            "{}{}",
            self.make_line_prefix(
                LineIndexType::Normal(current_line),
                max_line_number_width,
                vertical_stack
            ),
            file.line_text(current_line).unwrap_or_default()
        );
    }

    fn draw_line_with_starts<'src>(
        &self,
        current_line: usize,
        max_line_number_width: usize,
        file: &TokenizedFile<'src>,
        vertical_stack: &[Option<LabelType>],
        start_commands: &[(LabelType, usize)],
    ) {
        println!(
            "{}{}",
            self.make_start_prefix(
                LineIndexType::Normal(current_line),
                max_line_number_width,
                vertical_stack,
                start_commands,
            ),
            file.line_text(current_line).unwrap_or_default()
        );
    }

    fn make_line_prefix<'src>(
        &self,
        current_line: LineIndexType,
        max_line_number_width: usize,
        vertical_stack: &[Option<LabelType>],
    ) -> String {
        let line_text = match current_line {
            LineIndexType::Normal(current_line) => (current_line + 1).to_string(),
            LineIndexType::Missing => "".to_string(),
            LineIndexType::Ellipsis => "...".to_string(),
        };
        let line_padding = " ".repeat(max_line_number_width - line_text.len());
        let mut builder = format!(" {}{} | ", line_padding, line_text)
            .blue()
            .bold()
            .to_string();

        for vertical_line in vertical_stack {
            if let Some(label) = vertical_line {
                let piece = match label {
                    LabelType::Primary => {
                        if self.severity == Severity::Error {
                            " │".red()
                        } else {
                            " │".yellow()
                        }
                    }
                    LabelType::Secondary(_) => " │".blue(),
                }
                .to_string();
                builder.push_str(&piece);
            } else {
                builder.push_str("  ");
            }
        }
        builder
    }

    fn make_start_prefix(
        &self,
        current_line: LineIndexType,
        max_line_number_width: usize,
        vertical_stack: &[Option<LabelType>],
        start_commands: &[(LabelType, usize)],
    ) -> String {
        let line_text = match current_line {
            LineIndexType::Normal(current_line) => (current_line + 1).to_string(),
            LineIndexType::Missing => "".to_string(),
            LineIndexType::Ellipsis => "...".to_string(),
        };
        let line_padding = " ".repeat(max_line_number_width - line_text.len());
        let mut builder = format!(" {}{} | ", line_padding, line_text)
            .blue()
            .bold()
            .to_string();

        for (column, vertical_line) in vertical_stack.iter().enumerate() {
            let piece = match vertical_line {
                Some(label) => {
                    let starts_here = start_commands.iter().any(|(start_label, start_column)| {
                        *start_label == *label && *start_column == column
                    });

                    match label {
                        LabelType::Primary => {
                            if self.severity == Severity::Error {
                                if starts_here {
                                    " ╭".red()
                                } else {
                                    " │".red()
                                }
                            } else {
                                if starts_here {
                                    " ╭".yellow()
                                } else {
                                    " │".yellow()
                                }
                            }
                        }
                        LabelType::Secondary(_) => {
                            if starts_here {
                                " ╭".blue()
                            } else {
                                " │".blue()
                            }
                        }
                    }
                    .to_string()
                }
                None => "  ".to_string(),
            };

            builder.push_str(&piece);
        }

        builder
    }

    fn make_finish_prefix(
        &self,
        max_line_number_width: usize,
        vertical_stack: &[Option<LabelType>],
        finishing_label: LabelType,
    ) -> String {
        let line_text = "";
        let line_padding = " ".repeat(max_line_number_width - line_text.len());
        let mut builder = format!(" {}{} | ", line_padding, line_text)
            .blue()
            .bold()
            .to_string();

        for vertical_line in vertical_stack {
            let piece = match vertical_line {
                Some(label) if *label == finishing_label => match label {
                    LabelType::Primary => {
                        if self.severity == Severity::Error {
                            " ╰".red()
                        } else {
                            " ╰".yellow()
                        }
                    }
                    LabelType::Secondary(_) => " ╰".blue(),
                }
                .to_string(),

                Some(label) => match label {
                    LabelType::Primary => {
                        if self.severity == Severity::Error {
                            " │".red()
                        } else {
                            " │".yellow()
                        }
                    }
                    LabelType::Secondary(_) => " │".blue(),
                }
                .to_string(),

                None => "  ".to_string(),
            };

            builder.push_str(&piece);
        }

        builder
    }

    fn render_header(&self) {
        let severity_label = match self.severity {
            Severity::Error => "error".red(),
            Severity::Warning => "warning".yellow(),
        };
        if let Some(ref code) = self.code {
            println!(
                "{}",
                format!("{}[{}]: {}", severity_label, code, self.headline).bold()
            );
        } else {
            println!(
                "{}",
                format!("{}: {}", severity_label, self.headline).bold()
            );
        }
    }

    fn render_help_and_notes<'src>(&self, file: &TokenizedFile<'src>) {
        if self.help().is_none() && self.notes().is_empty() {
            return;
        }

        let ranges = make_ranges(file, self);
        let max_line_number_width = max(max_line_number_width(&ranges), 3);

        // Blank gutter separator, like rustc's trailing `|`
        println!(
            "{}",
            format!(" {} |", " ".repeat(max_line_number_width))
                .blue()
                .bold()
        );

        if let Some(help) = self.help() {
            self.render_trailer_line("help", help, max_line_number_width);
        }

        for note in self.notes() {
            self.render_trailer_line("note", note, max_line_number_width);
        }
    }

    fn render_trailer_line(&self, kind: &str, message: &str, max_line_number_width: usize) {
        let prefix = format!(" {} = ", " ".repeat(max_line_number_width))
            .blue()
            .bold()
            .to_string();

        let kind = match kind {
            "help" => "help".green().bold().to_string(),
            "note" => "note".blue().bold().to_string(),
            _ => kind.bold().to_string(),
        };

        println!("{prefix}{kind}: {message}");
    }
}
