use crate::{config::Config, LANGUAGE};

use std::{
    collections::HashMap,
    fmt::Write,
    path::{Path, PathBuf},
};

use console::{style, Style, Term};
use tree_sitter::{Node, Parser, Query, QueryCursor, Range, Tree};

pub struct App {
    config: Config,
    path: PathBuf,
    query: Option<Query>,
    query_path: Option<PathBuf>,
    src: Vec<u8>,
    tree: Tree,
}

impl App {
    pub fn new<'a, P: AsRef<Path>>(src: &'a [u8], path: P, query_path: Option<P>) -> Self {
        let path = path.as_ref().to_owned();

        let mut parser = Parser::new();
        parser.set_language(*LANGUAGE).unwrap();

        let tree = parser.parse(&src, None).unwrap();
        let query_path = query_path.map(|q| q.as_ref().to_owned());
        let query = query_path.as_ref().map(|p| {
            let query_src = std::fs::read_to_string(&p).expect("unable to read query");
            Query::new(*LANGUAGE, &query_src).expect("query parse error")
        });

        Self {
            config: Default::default(),
            path,
            query,
            query_path,
            src: src.to_owned(),
            tree,
        }
    }

    pub fn draw(&self) {
        let term = Term::stdout();
        term.clear_screen().unwrap();
        let mut done = false;
        let mut depth = 0;
        let mut in_capture: Option<Range> = None;
        let mut cursor = self.tree.walk();

        let capture_names = self
            .query
            .as_ref()
            .map(|q| q.capture_names())
            .unwrap_or_default();
        let capture_map = self
            .query
            .as_ref()
            .map(|query| {
                QueryCursor::new()
                    .matches(&query, self.tree.root_node(), self.src.as_slice())
                    .flat_map(|match_| match_.captures)
                    .fold(
                        HashMap::new(),
                        |mut map: HashMap<Node, Vec<u32>>, capture| {
                            map.entry(capture.node)
                                .and_modify(|idxs| idxs.push(capture.index))
                                .or_insert_with(|| vec![capture.index]);
                            map
                        },
                    )
            })
            .unwrap_or_default();

        while !done {
            let node = cursor.node();
            let mut tree_string = String::new();
            in_capture = match in_capture {
                Some(range)
                    if !contains(&range, &node.range()) && capture_map.contains_key(&node) =>
                {
                    Some(node.range())
                }
                Some(range) if !contains(&range, &node.range()) => None,
                None if capture_map.contains_key(&node) => Some(node.range()),
                i => i,
            };

            write!(
                tree_string,
                "{}",
                (if in_capture.is_some() {
                    Style::new().on_yellow().on_bright()
                } else {
                    Style::new()
                })
                .bright()
                .black()
                .apply_to(
                    format!("{}{}", "|", " ".repeat(self.config.indent_level))
                        .repeat(depth as usize)
                )
            )
            .unwrap();

            write!(
                tree_string,
                "{} ",
                if node.is_error() {
                    Style::new().red()
                } else if in_capture.is_some() {
                    Style::new().on_yellow().on_bright()
                } else {
                    Style::new()
                }
                .apply_to(node.kind()),
            )
            .unwrap();

            if let Some(idxs) = capture_map.get(&node) {
                for index in idxs {
                    write!(
                        tree_string,
                        "@{} ",
                        style(capture_names[*index as usize].as_str()).magenta()
                    )
                    .unwrap();
                }
            }

            if self.config.show_ranges {
                let range = node.range();
                write!(
                    tree_string,
                    " {}",
                    style(format!("{:?}..{:?}", range.start_byte, range.end_byte,))
                        .bright()
                        .black()
                )
                .unwrap();
            }

            if self.config.show_src {
                write!(
                    tree_string,
                    " {:.?}",
                    style(node.utf8_text(&self.src).unwrap()).cyan()
                )
                .unwrap();
            }

            term.write_line(&tree_string).unwrap();
            term.clear_to_end_of_screen().unwrap();

            if cursor.goto_first_child() {
                depth += 1;
                continue;
            }
            if cursor.goto_next_sibling() {
                continue;
            }

            loop {
                if !cursor.goto_parent() {
                    done = true;
                    break;
                } else {
                    depth -= 1;
                }

                if cursor.goto_next_sibling() {
                    break;
                }
            }
        }

        // see https://github.com/console-rs/console/issues/36#issuecomment-624731432
        // for the reasoning behing this hackjob

        term.write_line("\n(>) increase indent").unwrap();
        term.clear_to_end_of_screen().unwrap();

        term.write_line("(<) decrease indent ").unwrap();
        term.clear_to_end_of_screen().unwrap();

        term.write_line("(n) toggle ranges").unwrap();
        term.clear_to_end_of_screen().unwrap();

        term.write_line("(s) toggle source text").unwrap();
        term.clear_to_end_of_screen().unwrap();

        term.write_line("(r) reload from disk").unwrap();
        term.clear_to_end_of_screen().unwrap();

        term.write_line("(C-c) quit").unwrap();
        term.clear_to_end_of_screen().unwrap();
    }

    pub fn increase_indent(&mut self) {
        self.config.indent_level = self.config.indent_level.saturating_add(1);
    }

    pub fn decrease_indent(&mut self) {
        self.config.indent_level = self.config.indent_level.saturating_sub(1);
    }

    pub fn toggle_ranges(&mut self) {
        self.config.show_ranges = !self.config.show_ranges;
    }

    pub fn toggle_source(&mut self) {
        self.config.show_src = !self.config.show_src;
    }

    pub fn reload(&mut self) {
        let src = std::fs::read_to_string(&self.path).unwrap();
        let new = Self::new(src.as_bytes(), &self.path, self.query_path.as_ref());
        *self = Self {
            config: self.config,
            ..new
        };
    }
}

// does a encompass b
fn contains(a: &Range, b: &Range) -> bool {
    a.start_byte <= b.start_byte && a.end_byte >= b.end_byte
}