#![feature(yggdrasil_os)]
use std::{
    io::{self, Read, Seek, SeekFrom, Write},
    process::ExitCode, time::{Duration, Instant},
};

use clap::Parser;
use libutil::fmt::FormatSize;
use sysutils::{Input, Output};

#[derive(Parser)]
struct Args {
    #[arg(short)]
    source: Option<String>,
    #[arg(short)]
    destination: Option<String>,

    #[arg(long)]
    src_skip: Option<u64>,
    #[arg(long, default_value_t = 512)]
    src_bs: u64,
    #[arg(short, long, default_value_t = usize::MAX)]
    count: usize,
    #[arg(short)]
    throughput: bool,

    // TODO: remove this when pipes are a thing
    #[arg(short = 'x', long)]
    as_hex: bool,
}

fn dump_block(offset: u64, data: &[u8]) {
    const WINDOW_SIZE: usize = 16;
    let window_count = (data.len() + WINDOW_SIZE) / WINDOW_SIZE;

    for iw in 0..window_count {
        let off = iw * WINDOW_SIZE;
        let len = core::cmp::min(data.len() - off, WINDOW_SIZE);
        if len == 0 {
            break;
        }

        let window = &data[off..off + len];

        print!("{:08X}: ", offset + off as u64);
        for i in 0..WINDOW_SIZE {
            if i < window.len() {
                print!("{:02X}", window[i]);
            } else {
                print!("  ");
            }

            if i % 2 == 1 {
                print!(" ");
            }
        }

        for &ch in window {
            if ch.is_ascii_graphic() || ch == b' ' {
                print!("{}", ch as char);
            } else {
                print!(".");
            }
        }

        println!();
    }
}

fn print_throughput(duration: Duration, bytes_read: usize) {
    let ms = duration.as_millis() as u64;
    let read_per_ms = bytes_read as u64 / ms;
    let read_total = FormatSize::default(bytes_read as u64);
    let read_speed = FormatSize::default(read_per_ms * 1000);

    eprintln!("{read_speed}/s ({read_total} in {duration:?})");
}

fn run<I: Read + Seek, O: Write>(
    mut input: I,
    mut output: O,
    src_position: u64,
    src_block_size: u64,
    mut count: usize,
    opts: &Args
) -> io::Result<()> {
    const STATS_INTERVAL: Duration = Duration::from_secs(3);

    let mut block = vec![0; src_block_size as usize];
    let mut offset = 0;
    let mut bytes_read = 0;

    input.seek(SeekFrom::Start(src_position * src_block_size))?;

    let mut total_delta = Duration::ZERO;

    while count != 0 {
        let start = Instant::now();
        let read_count = input.read(&mut block)?;
        let read_time = Instant::now() - start;

        bytes_read += read_count;
        total_delta += read_time;

        if opts.throughput && total_delta > STATS_INTERVAL {
            print_throughput(total_delta, bytes_read);
            total_delta = Duration::ZERO;
            bytes_read = 0;
        }

        if read_count == 0 {
            if opts.throughput && bytes_read != 0 {
                print_throughput(total_delta, bytes_read);
            }
            break;
        }

        if opts.as_hex {
            dump_block(
                (src_position + offset) * src_block_size,
                &block[..read_count],
            );
        } else {
            output.write_all(&block[..read_count])?;
        }

        count -= 1;
        offset += 1;
    }

    Ok(())
}

fn main() -> ExitCode {
    let args = Args::parse();

    let src_path = args.source.as_deref().unwrap_or("-");
    let dst_path = args.destination.as_deref().unwrap_or("-");

    if src_path == dst_path && src_path != "-" {
        eprintln!("Input and output cannot be the same: {}", src_path);
        return ExitCode::FAILURE;
    }

    if args.as_hex && dst_path != "-" {
        eprintln!("--as-hex requires stdout output");
        return ExitCode::FAILURE;
    }

    let input = Input::open_str(src_path).unwrap();
    let output = Output::open_str(dst_path).unwrap();

    let src_position = args.src_skip.unwrap_or(0);

    match run(
        input,
        output,
        src_position,
        args.src_bs,
        args.count,
        &args
    ) {
        Ok(_) => ExitCode::SUCCESS,
        Err(e) => {
            eprintln!("Error: {}", e);
            ExitCode::FAILURE
        }
    }
}
