use std::env;
use std::rc::Rc;

use ruin_reactivity::effect_in;
use ruin_reactivity_parsing::{
    ReactiveRope, ReactiveTokenTree, RopeEdit, TextRange, TokenTreeNode, TokenTreeParse,
    TokenTreeUpdate,
};
use ruin_runtime::{async_main, fs, stdio};

enum Command {
    Insert {
        offset: usize,
        text: String,
    },
    Replace {
        start: usize,
        end: usize,
        text: String,
    },
    ReplaceAll {
        text: String,
    },
    Delete {
        start: usize,
        end: usize,
    },
    Print,
    Help,
    Quit,
}

#[async_main]
async fn main() {
    let path = env::args()
        .nth(1)
        .expect("usage: cargo run -p ruin-reactivity-parsing --example tokentree_reactive_parser -- <path/to/file>");
    let initial_text = fs::read_to_string(&path)
        .await
        .expect("source file should be readable");

    let reactor = ruin_reactivity::Reactor::new();
    let rope = ReactiveRope::from_text(&reactor, &initial_text);
    let parser = Rc::new(ReactiveTokenTree::new(&rope));

    effect_in(&reactor, {
        let parser = Rc::clone(&parser);
        let rope = rope.clone();
        move || {
            parser.with_snapshot(|snapshot| {
                let last_update = parser.last_update();
                print_snapshot(snapshot, &rope, last_update.as_ref());
            });
        }
    })
    .leak();

    println!("Commands:");
    println!("  insert <byte_offset> <text>");
    println!("  replace <start> <end> <text>");
    println!("  replace_all <text>");
    println!("  delete <start> <end>");
    println!("  print");
    println!("  help");
    println!("  quit");

    let mut input = stdio::stdin().expect("stdin should be available");
    while let Some(line) = input.read_line().await.expect("stdin read should succeed") {
        let trimmed = line.trim_end();
        if trimmed.is_empty() {
            continue;
        }

        match parse_command(trimmed) {
            Ok(Command::Insert { offset, text }) => {
                let _ = rope.edit(RopeEdit::insert(offset, text));
            }
            Ok(Command::Replace { start, end, text }) => {
                let _ = rope.edit(RopeEdit::replace(TextRange::new(start, end), text));
            }
            Ok(Command::ReplaceAll { text }) => {
                let _ = rope.edit(RopeEdit::replace(TextRange::new(0, rope.len_bytes()), text));
            }
            Ok(Command::Delete { start, end }) => {
                let _ = rope.edit(RopeEdit::delete(TextRange::new(start, end)));
            }
            Ok(Command::Print) => {
                let snapshot = parser.snapshot();
                let last_update = parser.last_update();
                print_snapshot(&snapshot, &rope, last_update.as_ref());
            }
            Ok(Command::Help) => {
                println!("Commands:");
                println!("  insert <byte_offset> <text>");
                println!("  replace <start> <end> <text>");
                println!("  replace_all <text>");
                println!("  delete <start> <end>");
                println!("  print");
                println!("  help");
                println!("  quit");
            }
            Ok(Command::Quit) => break,
            Err(error) => eprintln!("{error}"),
        }
    }
}

fn parse_command(input: &str) -> Result<Command, String> {
    if input == "quit" {
        return Ok(Command::Quit);
    }
    if input == "print" {
        return Ok(Command::Print);
    }
    if input == "help" {
        return Ok(Command::Help);
    }
    if let Some(rest) = input.strip_prefix("insert ") {
        let (offset, text) = rest
            .split_once(' ')
            .ok_or_else(|| "insert expects: insert <offset> <text>".to_string())?;
        return Ok(Command::Insert {
            offset: offset
                .parse()
                .map_err(|_| "invalid insert offset".to_string())?,
            text: text.to_string(),
        });
    }
    if let Some(rest) = input.strip_prefix("replace ") {
        let mut parts = rest.splitn(3, ' ');
        let start = parts
            .next()
            .ok_or_else(|| "replace expects: replace <start> <end> <text>".to_string())?;
        let end = parts
            .next()
            .ok_or_else(|| "replace expects: replace <start> <end> <text>".to_string())?;
        let text = parts
            .next()
            .ok_or_else(|| "replace expects: replace <start> <end> <text>".to_string())?;
        return Ok(Command::Replace {
            start: start
                .parse()
                .map_err(|_| "invalid replace start".to_string())?,
            end: end.parse().map_err(|_| "invalid replace end".to_string())?,
            text: text.to_string(),
        });
    }
    if let Some(text) = input.strip_prefix("replace_all ") {
        return Ok(Command::ReplaceAll {
            text: text.to_string(),
        });
    }
    if let Some(rest) = input.strip_prefix("delete ") {
        let (start, end) = rest
            .split_once(' ')
            .ok_or_else(|| "delete expects: delete <start> <end>".to_string())?;
        return Ok(Command::Delete {
            start: start
                .parse()
                .map_err(|_| "invalid delete start".to_string())?,
            end: end.parse().map_err(|_| "invalid delete end".to_string())?,
        });
    }
    Err("unknown command; type `help`".to_string())
}

fn print_snapshot(
    snapshot: &TokenTreeParse,
    rope: &ReactiveRope,
    update: Option<&TokenTreeUpdate>,
) {
    let metrics = rope.snapshot_metrics();
    println!();
    println!("Document revision {}", metrics.revision);
    println!(
        "Text bytes={}, leaves={}, nodes={}, reused_on_last_edit={}",
        metrics.text.bytes, metrics.leaves, snapshot.stats.total_nodes, snapshot.stats.reused_nodes
    );
    if let Some(update) = update {
        println!(
            "Last edit: [{}..{}) -> {:?}",
            update.delta.range.start, update.delta.range.end, update.delta.inserted_text
        );
        println!(
            "Diagnostics changed={}, trivia changed={}",
            update.diagnostics_changed, update.trivia_changed
        );
    }
    println!("Parse tree:");
    print_tree(snapshot, rope, &snapshot.root, 0);
    if !snapshot.diagnostics.is_empty() {
        println!("Diagnostics:");
        for diagnostic in &snapshot.diagnostics {
            println!(
                "  {:?} [{}..{}): {}",
                diagnostic.kind, diagnostic.range.start, diagnostic.range.end, diagnostic.message
            );
        }
    }
    if !snapshot.trivia.comments().is_empty() {
        println!("Comments:");
        for comment in snapshot.trivia.comments() {
            println!(
                "  {:?} [{}..{}): {}",
                comment.kind, comment.range.start, comment.range.end, comment.text
            );
        }
    }
}

fn print_tree(snapshot: &TokenTreeParse, rope: &ReactiveRope, node: &TokenTreeNode, depth: usize) {
    let indent = "  ".repeat(depth);
    let (start, end) = snapshot
        .absolute_span(rope, node.id)
        .expect("node span should be available");
    println!(
        "{}- id={} kind={} span=({}:{})..({}:{})",
        indent, node.id, node.kind, start.line, start.column, end.line, end.column
    );
    for child in &node.children {
        print_tree(snapshot, rope, child, depth + 1);
    }
}
