//
// Syd: rock-solid application kernel
// src/utils/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 data_encoding::{HEXLOWER, HEXLOWER_PERMISSIVE};
use memchr::arch::all::is_equal;
use nix::{errno::Errno, unistd::isatty};

// Set global allocator to GrapheneOS allocator.
#[cfg(all(
    not(coverage),
    not(feature = "prof"),
    not(target_os = "android"),
    target_page_size_4k,
    target_pointer_width = "64"
))]
#[global_allocator]
static GLOBAL: hardened_malloc::HardenedMalloc = hardened_malloc::HardenedMalloc;

// Set global allocator to tcmalloc if profiling is enabled.
#[cfg(feature = "prof")]
#[global_allocator]
static GLOBAL: tcmalloc::TCMalloc = tcmalloc::TCMalloc;

syd::main! {
    use lexopt::prelude::*;

    syd::set_sigpipe_dfl()?;

    // Parse CLI options.
    let mut opt_encode = true;
    let mut opt_force = false;
    let mut opt_stream = false;
    let mut opt_continue_on_failure = 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('s') | Long("stream") => opt_stream = true,
            Short('C') | Long("continue-on-failure") => opt_continue_on_failure = 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()),
        }
    }

    // Validate CLI option combinations.
    if opt_encode && opt_stream {
        // SAFETY: hex-encoding newline delimited binary data is inherently unsafe.
        eprintln!("syd-hex: -s option must be used with the -d option.");
        return Err(Errno::EINVAL.into());
    }

    if opt_continue_on_failure && !opt_stream {
        // SAFETY: continue-on-failure is unsafe in batch mode.
        eprintln!("syd-hex: -C option must be used with the -s option.");
        return Err(Errno::EINVAL.into());
    }

    if !opt_encode && !opt_force && isatty(std::io::stdout())? {
        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
    #[expect(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)?)),
    };

    if !opt_stream {
        assert!(
            !opt_continue_on_failure,
            "attempt to continue-on-failure in batch mode!"
        );

        // 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.saturating_add(count);
                buffer
            } else {
                &buffer[..count]
            };
            if opt_encode {
                // Hex-encode and write the chunk.
                let encoded = HEXLOWER.encode(buffer);
                print!("{encoded}");
            } else {
                // Hex-decode and write the chunk.
                let data = std::str::from_utf8(buffer)?;
                let data = data.split_whitespace().collect::<String>();
                match HEXLOWER_PERMISSIVE.decode(data.as_bytes()) {
                    Ok(decoded) => {
                        std::io::stdout().write_all(&decoded)?;
                    }
                    Err(error) => {
                        eprintln!("syd-hex: Error decoding hex: {error}");
                        return Ok(ExitCode::FAILURE);
                    }
                }
            }
        }
    } else {
        // --stream
        assert!(!opt_encode, "attempt to hex-encode stream!");

        let line_limit: usize = opt_limit.unwrap_or(0).try_into().unwrap_or(0);
        for (idx, line) in input.lines().enumerate() {
            // Stop reading if limit is hit.
            if line_limit != 0 && idx > line_limit {
                break;
            }

            // Hex-decode and write the line.
            let line = match line {
                Ok(line) => line,
                Err(error) => {
                    eprintln!("syd-hex: Error reading line {idx}: {error}!");
                    if !opt_continue_on_failure {
                        return Ok(ExitCode::FAILURE);
                    } else {
                        continue;
                    }
                }
            };
            let line = line.trim_end();
            let line = line.split_whitespace().collect::<String>();
            match HEXLOWER_PERMISSIVE.decode(line.as_bytes()) {
                Ok(decoded) => {
                    std::io::stdout().write_all(&decoded)?;
                    std::io::stdout().write_all(b"\n")?;
                }
                Err(error) => {
                    eprintln!("syd-hex: Error decoding hex on line {idx}: {error}!");
                    if !opt_continue_on_failure {
                        return Ok(ExitCode::FAILURE);
                    } else {
                        continue;
                    }
                }
            }
        }
    }

    Ok(ExitCode::SUCCESS)
}

fn help() {
    println!("Usage: syd-hex [-hdefkls] <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 -s with -d to hex-decode with newline-delimited chunks.");
    println!("Use -C with -s to warn and continue in case of read or encoding errors.");
    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.");
    println!("Use -l <line-count> with -s to exit after count lines are read.");
}
