// SPDX-FileCopyrightText: 2023 CELESTIFYX Team
// SPDX-License-Identifier: GPL-3.0-or-later

use regex::Regex;

use std::cmp::{
    max,
    min
};

use terminal_size::{
    terminal_size,
    Width
};

use lazy_static::lazy_static;
use chromakitx::AnyColor;

use super::{
    info::{
        InfoConfig,
        InfoPosition
    },

    renderer::AnsiRenderer,
    token::TokenType,
    tokenizer::Tokenizer
};

lazy_static! {
    static ref ANSI_ESCAPE: Regex = Regex::new(r"\x1b\[[0-9;]*m").unwrap();
}

pub struct AsciiArt {
    colors: Option<Vec<AnyColor>>,
    bold: bool,
    info: Option<InfoConfig>,
    renderer: AnsiRenderer,
    current_color: Option<usize>,
    lines: Vec<String>,
    start: usize,
    end: usize
}

impl AsciiArt {
    pub fn new(text: Option<String>, colors: Option<Vec<AnyColor>>, bold: bool, info: Option<InfoConfig>) -> Self {
        let lines: Vec<String> = if let Some(text) = text {
            Self::preprocess_lines(&text)
        } else {
            Vec::new()
        };

        let (start, end): (usize, usize) = Self::calculate_boundaries(&lines);

        Self {
            colors,
            bold,
            info,
            renderer: AnsiRenderer::new(),
            current_color: None,
            lines,
            start,
            end
        }
    }

    fn preprocess_lines(text: &str) -> Vec<String> {
        let lines: Vec<&str> = text.lines().collect();

        let mut start_idx: usize = 0;
        let mut end_idx:   usize = lines.len();

        for (i, line) in lines.iter().enumerate() {
            if !line.trim().is_empty() {
                start_idx = i;
                break;
            }
        }

        for i in (0..lines.len()).rev() {
            if !lines[i].trim().is_empty() {
                end_idx = i + 1;
                break;
            }
        }

        lines[start_idx..end_idx].iter().map(|s: &&str| s.to_string()).collect()
    }

    fn calculate_boundaries(lines: &[String]) -> (usize, usize) {
        if lines.is_empty() {
            return (0, 0);
        }

        let mut start: usize = usize::MAX;
        let mut end:   usize = 0;

        for line in lines {
            let tokenizer: Tokenizer = Tokenizer::new(line.clone());

            let line_start: usize = tokenizer.leading_spaces();
            let line_end:   usize = tokenizer.true_length();

            start = min(start, line_start);
            end   = max(end, line_end);
        }

        if start == usize::MAX {
            return (0, 0);
        }

        (start, end)
    }

    fn resolve_color(&self, current_color: Option<usize>) -> Option<AnyColor> {
        current_color.and_then(|idx: usize| {
            self.colors.as_ref().and_then(|colors: &Vec<AnyColor>| colors.get(idx).cloned())
        })
    }

    fn render_line(&mut self, line: &str) -> String {
        let tokenizer: Tokenizer = Tokenizer::new(line.to_string());
        let mut output: Vec<String> = Vec::new();

        let default_color: Option<usize> = if let Some(ref colors) = self.colors {
            if !colors.is_empty() {
                Some(0)
            } else {
                None
            }
        } else {
            None
        };

        let mut current_color: Option<usize> = if let Some(idx) = self.current_color {
            Some(idx)
        } else {
            default_color
        };

        let mut current_segment: Vec<String> = Vec::new();

        for tok in tokenizer.truncate(self.start, self.end) {
            if tok.token_type == TokenType::Color {
                if !current_segment.is_empty() {
                    let segment_text: String = current_segment.join("");

                    let color: Option<AnyColor> = self.resolve_color(current_color);
                    let styled: String = self.renderer.render_segment(&segment_text, color, self.bold);

                    (&mut output).push(styled);
                    (&mut current_segment).clear();
                }

                if let Some(value) = &tok.value && let Ok(color_idx) = value.parse::<usize>() && let Some(colors) = &self.colors && (color_idx < colors.len()) {
                    current_color = Some(color_idx);
                    self.current_color = Some(color_idx);
                }
            } else if (tok.token_type == TokenType::Char) || (tok.token_type == TokenType::Space) {
                (&mut current_segment).push(tok.value.unwrap_or_else(|| " ".to_string()));
            }
        }

        if !current_segment.is_empty() {
            let segment_text: String = current_segment.join("");
            let color: Option<AnyColor> = self.resolve_color(current_color);

            let styled: String = self.renderer.render_segment(&segment_text, color, self.bold);
            (&mut output).push(styled);
        }

        output.join("")
    }

    pub fn render(&mut self) -> String {
        (&mut *self).iter().collect::<Vec<_>>().join("\n")
    }

    fn max_visible_width(lines: &[String]) -> usize {
        lines.iter().map(|line: &String| Self::visible_len(line)).max().unwrap_or(0)
    }

    fn iter(&mut self) -> AsciiArtIterator<'_> {
        let info_lines: Option<Vec<String>> = self.info.as_ref().map(|info: &InfoConfig| info.lines.clone());

        let lines_clone: Vec<String> = self.lines.clone();
        let art_lines_cache = Some(lines_clone.clone().into_iter().map(|l: String| (&mut *self).render_line(&l)).collect::<Vec<_>>());

        let max_art_width:  usize = lines_clone.iter().map(|line: &String| Tokenizer::new(line.clone()).true_length()).max().unwrap_or(0);
        let max_info_width: usize = info_lines.as_ref().map(|lines: &Vec<String>| AsciiArt::max_visible_width(lines)).unwrap_or(0);

        AsciiArtIterator {
            art: self,
            index: 0,
            art_lines_cache,
            max_art_width,
            max_info_width
        }
    }

    pub fn width(&self) -> usize {
        self.end - self.start
    }

    pub fn height(&self) -> usize {
        self.lines.len()
    }

    pub fn lines(&self) -> Vec<String> {
        self.lines.clone()
    }

    pub fn bold(&self) -> bool {
        self.bold
    }

    pub fn set_bold(&mut self, bold: bool) {
        self.bold = bold;
    }

    fn strip_ansi(text: &str) -> String {
        ANSI_ESCAPE.replace_all(text, "").to_string()
    }

    fn truncate_line(line: &str, max_width: usize) -> String {
        let mut visible_len: usize = 0;
        let mut result: String = String::new();
        let mut pos: usize = 0;

        while (pos < line.len()) && (visible_len < max_width) {
            if let Some(mat) = ANSI_ESCAPE.find_at(line, pos) && (mat.start() == pos) {
                (&mut result).push_str(mat.as_str());
                pos = mat.end();

                continue;
            }

            if let Some(ch) = line[pos..].chars().next() {
                (&mut result).push(ch);
                visible_len += 1;

                pos += ch.len_utf8();
                continue;
            }

            break;
        }

        result
    }

    fn get_terminal_width() -> usize {
        terminal_size().map(|(w, _): (Width, _)| w.0 as usize).unwrap_or(80)
    }

    #[inline]
    fn line_or_empty(lines: &[String], index: usize) -> &str {
        lines.get(index).map(|s: &String| s.as_str()).unwrap_or("")
    }

    #[inline]
    fn visible_len(s: &str) -> usize {
        let stripped: String = AsciiArt::strip_ansi(s);
        stripped.chars().count()
    }

    #[inline]
    fn truncate_if_needed(line: String) -> String {
        let term_width: usize = AsciiArt::get_terminal_width();

        if AsciiArt::strip_ansi(&line).len() > term_width {
            return AsciiArt::truncate_line(&line, term_width);
        }

        line
    }

    #[inline]
    fn combine_lines(info_lines: &[String], art_lines: &[String], index: usize, left_max_width: usize, mut margin: usize) -> String {
        let left:  &str = AsciiArt::line_or_empty(info_lines, index);
        let right: &str = AsciiArt::line_or_empty(art_lines, index);

        let left_visible: usize = AsciiArt::visible_len(left);
        let padding:      usize = left_max_width.saturating_sub(left_visible);

        if art_lines.is_empty() || info_lines.is_empty() {
            margin = 0;
        }

        let combined: String = format!("{}{}{}{}", left, " ".repeat(padding), " ".repeat(margin), right);
        AsciiArt::truncate_if_needed(combined)
    }

    #[inline]
    fn combined_line_at(index: usize, first: &[String], second: &[String]) -> String {
        if index < first.len() {
            return first[index].clone();
        }

        second[index - first.len()].clone()
    }
}

struct AsciiArtIterator<'a> {
    art: &'a mut AsciiArt,
    index: usize,
    art_lines_cache: Option<Vec<String>>,
    max_art_width: usize,
    max_info_width: usize
}

impl<'a> Iterator for AsciiArtIterator<'a> {
    type Item = String;

    fn next(&mut self) -> Option<Self::Item> {
        if self.art.info.is_none() {
            if self.index >= self.art.lines.len() {
                return None;
            }

            let line: String = self.art.lines[self.index].clone();
            self.index += 1;

            return Some((&mut *self.art).render_line(&line));
        }

        if self.art_lines_cache.is_none() {
            let mut cache: Vec<String> = Vec::new();
            let lines_clone: Vec<String> = self.art.lines.clone();

            for line in lines_clone {
                (&mut cache).push((&mut *self.art).render_line(&line));
            }

            self.art_lines_cache = Some(cache);
        }

        let info: &InfoConfig = self.art.info.as_ref().unwrap();
        let info_lines: &Vec<String> = &info.lines;
        let art_lines: &Vec<String> = self.art_lines_cache.as_ref().unwrap();
        let position: InfoPosition = info.position;

        match position {
            InfoPosition::Top => {
                let total: usize = info_lines.len() + art_lines.len();

                if self.index >= total {
                    return None;
                }

                let current_index: usize = self.index;
                self.index += 1;

                Some(AsciiArt::combined_line_at(current_index, &info_lines, &art_lines))
            },

            InfoPosition::Bottom => {
                let total: usize = art_lines.len() + info_lines.len();

                if self.index >= total {
                    return None;
                }

                let current_index: usize = self.index;
                self.index += 1;

                Some(AsciiArt::combined_line_at(current_index, &art_lines, &info_lines))
            },

            InfoPosition::Right => {
                let max_art_width: usize = self.max_art_width;
                let max_len:       usize = max(art_lines.len(), info_lines.len());

                if self.index >= max_len {
                    return None;
                }

                let current_index: usize = self.index;
                self.index += 1;

                Some(AsciiArt::combine_lines(art_lines, info_lines, current_index, max_art_width, info.margin))
            },

            InfoPosition::Left => {
                let max_info_width: usize = self.max_info_width;
                let max_len:        usize = max(art_lines.len(), info_lines.len());

                if self.index >= max_len {
                    return None;
                }

                let current_index: usize = self.index;
                self.index += 1;

                Some(AsciiArt::combine_lines(info_lines, art_lines, current_index, max_info_width, info.margin))
            }
        }
    }
}
