mod clipboard;
mod cmd;
mod config;
mod debug;
mod edit;
mod live_reload;
mod parse;
mod tree;
mod ui;

use std::fs;
use std::io;
use std::io::Read;
use std::path::PathBuf;
use std::process;
use std::rc::Rc;

use anyhow::{bail, Context, Result};
use live_reload::FileWatcher;

use crate::cmd::CommandArgs;
use crate::config::Config;
use crate::parse::{ContentType, SyntaxToken};
use crate::tree::Tree;
use crate::ui::{App, HeaderContext};

fn run() -> Result<()> {
    let Some(args) = CommandArgs::parse()? else {
        return Ok(());
    };

    let mut cfg = if args.ignore_config {
        Config::default()
    } else {
        Config::load(args.config.clone())?
    };

    args.update_config(&mut cfg);
    cfg.parse().context("parse config")?;

    let cfg = Rc::new(cfg);

    if args.show_config {
        return cfg.show();
    }

    if let Some(ref debug_file) = args.debug {
        debug::set_file(debug_file.clone());
    }

    // The user can specify the content type manually, or we can determine it based on the
    // file extension.
    // If the content type cannot be determined here, use `ContentType::Any` as default, let
    // parser to detect it.
    let content_type = args.get_content_type();

    let max_data_size = args.max_data_size.unwrap_or(cfg.data.max_data_size) * 1024 * 1024;
    let mut fw = None;
    let data = match args.path.as_ref() {
        Some(path) => {
            let path = PathBuf::from(path);

            // Check file size before reading to prevent OOM on very large files
            let metadata = fs::metadata(&path).context("get file metadata")?;
            let file_size = metadata.len();
            if file_size > max_data_size as u64 {
                bail!("the file size ({}) is too large, we limit the maximum size to {} to ensure TUI performance, you should try to reduce the read size. HINT: You can use command line arg `--max-data-size` or config option `data.max_data_size` to modify this limitation", humansize::format_size(file_size, humansize::BINARY), humansize::format_size(max_data_size, humansize::BINARY));
            }

            if args.live_reload {
                fw = Some(FileWatcher::new(
                    path.clone(),
                    cfg.clone(),
                    content_type,
                    max_data_size,
                ));
            }
            fs::read(path).context("read file")?
        }
        None => {
            let mut data = Vec::new();
            io::stdin().read_to_end(&mut data).context("read stdin")?;

            // Check stdin data size after reading (unavoidable for stdin)
            if data.len() > max_data_size {
                bail!("the data size is too large, we limit the maximum size to {} to ensure TUI performance, you should try to reduce the read size. HINT: You can use command line arg `--max-data-size` or config option `data.max_data_size` to modify this limitation", humansize::format_size(max_data_size, humansize::BINARY));
            }

            data
        }
    };

    if let Some(target_type) = args.to {
        if matches!(target_type, ContentType::Any) {
            bail!("target content type cannot be 'any' when using '--to' option");
        }
        let parser = content_type.new_parser();
        let target_parser = target_type.new_parser();

        let value = parser.parse_root(Some(target_parser.as_ref()), &data)?;
        let tokens = target_parser.syntax_highlight("", &value);
        let text = SyntaxToken::pure_text(&tokens);

        print!("{text}");
        return Ok(());
    }

    let tree = Tree::parse(cfg.clone(), &data, content_type).context("parse data")?;
    let extension = tree.parser.extension();

    let mut app = App::new(cfg.clone(), tree, fw);

    if !cfg.header.disable {
        let header_ctx = HeaderContext::new(args.path, extension, data.len());
        app.set_header(&header_ctx);
    }

    ui::start(app)
}

fn main() {
    if let Err(err) = run() {
        eprintln!("Error: {err:#}");
        process::exit(1);
    }
}
