//
// Syd: rock-solid application kernel
// src/syd-hex.rs: Hexadecimal encode/decode standard input.
//
// Copyright (c) 2024, 2025 Ali Polatel <alip@chesswob.org>
//
// SPDX-License-Identifier: GPL-3.0

use std::{
    fs::File,
    io::{BufRead, BufReader, Write},
    os::unix::ffi::OsStrExt,
    process::ExitCode,
};

use hex::{DisplayHex, FromHex};
use memchr::arch::all::is_equal;
use nix::{errno::Errno, unistd::isatty};
use syd::err::SydResult;

fn main() -> SydResult<ExitCode> {
    use lexopt::prelude::*;

    syd::set_sigpipe_dfl()?;

    // Parse CLI options.
    let mut opt_encode = true;
    let mut opt_force = false;
    let mut opt_limit = None;
    let mut opt_input = None;

    let mut parser = lexopt::Parser::from_env();
    while let Some(arg) = parser.next()? {
        match arg {
            Short('h') => {
                help();
                return Ok(ExitCode::SUCCESS);
            }
            Short('d') => opt_encode = false,
            Short('e') => opt_encode = true,
            Short('f') | Long("force-tty") => opt_force = true,
            Short('l') => {
                opt_limit = Some(
                    parse_size::Config::new()
                        .with_binary()
                        .parse_size(parser.value()?.as_bytes())?,
                )
            }
            Value(input) if opt_input.is_none() => opt_input = Some(input),
            _ => return Err(arg.unexpected().into()),
        }
    }

    if !opt_encode && !opt_force && isatty(libc::STDOUT_FILENO)? {
        eprintln!("syd-hex: Refusing to write unsafe output to the terminal.");
        eprintln!("syd-hex: Use -f or --force-tty to override this check.");
        return Err(Errno::EBADF.into());
    }

    // Lock stdin for efficient reading
    #[allow(clippy::disallowed_methods)]
    let mut input: Box<dyn BufRead> = match opt_input {
        None => Box::new(std::io::stdin().lock()),
        Some(path) if is_equal(path.as_bytes(), b"-") => Box::new(std::io::stdin().lock()),
        Some(path) => Box::new(BufReader::new(File::open(path)?)),
    };

    // Process the input in chunks to avoid loading
    // it all into memory at once.
    let mut nwrite: usize = 0;
    let mut buffer = [0; 64 * 1024]; // 64KB buffer size.
    while let Ok(count) = input.read(&mut buffer[..]) {
        let buffer = if count == 0 {
            break; // End of input.
        } else if let Some(lim) = opt_limit.map(|lim| lim as usize) {
            let buffer = if nwrite.checked_add(count).map(|c| c >= lim).unwrap_or(true) {
                let offset = match lim.checked_sub(nwrite) {
                    Some(0) | None => break, // Limit reached.
                    Some(n) => n,
                };
                &buffer[..offset]
            } else {
                &buffer[..count]
            };
            nwrite = nwrite.checked_add(count).unwrap_or(usize::MAX);
            buffer
        } else {
            &buffer[..count]
        };
        if opt_encode {
            // Hex-encode and write the chunk
            let encoded = buffer.to_lower_hex_string();
            print!("{encoded}");
        } else {
            // Hex-decode and write the chunk
            let data = std::str::from_utf8(buffer)?;
            let data = data.split_whitespace().collect::<String>();
            match Vec::from_hex(&data) {
                Ok(decoded) => {
                    std::io::stdout().write_all(&decoded)?;
                }
                Err(error) => {
                    eprintln!("Error decoding hex: {error}");
                    return Ok(ExitCode::FAILURE);
                }
            }
        }
    }

    Ok(ExitCode::SUCCESS)
}

fn help() {
    println!("Usage: syd-hex [-hdefl] <file|->");
    println!("Given a file, hex-encode and print.");
    println!("Given no positional arguments, hex-encode standard input.");
    println!("Use -d to hex-decode rather than hex-encode.");
    println!("Use -f to force print decoded hex to TTY (\x1b[91minsecure\x1b[0m).");
    println!("Use -l <human-size> to exit after size bytes are read.");
}
