Documentation
use std::ops::Range;

use ratatui::text::Text;
use smol_str::SmolStr;

use crate::utils::tui::BoxText;

#[derive(Debug, Default)]
pub struct LogText {
    text: BoxText,
    changes: Vec<JJChange>,
}

#[derive(Debug, Default)]
pub struct JJChange {
    beacon: char,
    pub range_line: Range<usize>,
    pub id: SmolStr,
}

impl LogText {
    pub fn new(log: Vec<u8>) -> Self {
        let mut text: BoxText = log.into();
        Self::align(&mut text);
        let changes = Self::parse_changes(&text);

        Self { text, changes }
    }

    pub fn text(&self) -> &Text<'_> {
        &self.text
    }

    pub fn beacons(&self) -> &[JJChange] {
        &self.changes
    }

    pub fn find_working(&self) -> Option<(usize, &JJChange)> {
        self.beacons()
            .iter()
            .enumerate()
            .find_map(|(i, v)| v.is_working().then_some((i, v)))
    }

    pub fn find_by_id(&self, id: &str) -> Option<(usize, &JJChange)> {
        let i = self.changes.iter().position(|v| v.id == id)?;
        Some((i, &self.changes[i]))
    }

    pub fn find_by_long_id(&self, id: &str) -> Option<(usize, &JJChange)> {
        let i = match id.split_once('/') {
            Some((id, divergent)) => self.changes.iter().position(|v: &JJChange| {
                v.id.split_once('/')
                    .is_some_and(|(short_id, change_divergent)| {
                        id.starts_with(short_id) && change_divergent == divergent
                    })
            }),
            None => self
                .changes
                .iter()
                .position(|v: &JJChange| id.starts_with(v.id.as_str())),
        }?;
        Some((i, &self.changes[i]))
    }

    pub fn find_by_line(&self, index: usize) -> Option<(usize, &JJChange)> {
        self.changes
            .binary_search_by(|v| {
                index
                    .clamp(v.range_line.start, v.range_line.end)
                    .cmp(&index)
            })
            .ok()
            .map(|i| (i, &self.changes[i]))
            .or_else(|| self.changes.last().map(|v| (self.changes.len() - 1, v)))
    }
}

impl LogText {
    fn parse_changes(log: &Text<'_>) -> Vec<JJChange> {
        let mut changes = vec![];

        let mut skip_line = false;

        for (i_line, line) in log.iter().enumerate() {
            if skip_line {
                skip_line = false;
                continue;
            }

            let mut span_iter = line.iter().peekable();

            let mut change = JJChange::default();
            let mut id_parts = ["", "", ""];

            while let Some(span) = span_iter.next() {
                match change.beacon {
                    '\x00' => {
                        match span
                            .content
                            .chars()
                            .find(|v| matches!(v, '@' | '' | '' | '×' | '~'))
                        {
                            Some('~') => {
                                break;
                            }
                            Some(c) => {
                                change.beacon = c;
                                change.range_line.start = i_line;

                                skip_line = true;
                            }
                            _ => (),
                        }
                    }
                    _ => {
                        if !span_is_empty(&span.content) {
                            id_parts[0] = &span.content;

                            match span_iter.peek() {
                                Some(span) if span_is_empty(&span.content) => {
                                    change.id = id_parts[0].into();
                                }
                                Some(_) => {
                                    let part1 = span_iter.next().expect("must be `Some`");
                                    id_parts[1] = &part1.content;

                                    if let Some(part2) = span_iter.peek()
                                        && part2.content.starts_with('/')
                                    {
                                        id_parts[2] = &part2.content;
                                    }

                                    change.id = id_parts.into_iter().collect();
                                }
                                None => {
                                    change.id = id_parts[0].into();
                                }
                            }

                            if id_is_root(&change.id) {
                                change.range_line.end = i_line + 1;
                            } else {
                                change.range_line.end = i_line + 2;
                            }

                            changes.push(change);
                            break;
                        }
                    }
                }
            }
        }

        changes
    }

    fn align(text: &mut Text<'_>) {
        let twidth = text.width();

        for line in text.iter_mut() {
            let complete_n = twidth - line.width();
            if complete_n > 0 {
                line.push_span(" ".repeat(complete_n));
            }
        }
    }
}

impl JJChange {
    pub fn is_working(&self) -> bool {
        self.beacon == '@'
    }
}

fn span_is_empty(span: &str) -> bool {
    span.as_bytes().iter().all(|&v| v == b' ')
}

fn id_is_root(id: &str) -> bool {
    id.as_bytes().iter().all(|&v| v == b'z')
}