import argparse
from pathlib import Path

CHUNK_SIZE = 1024 * 1024


def hex_to_bin(in_path: Path, out_path: Path) -> None:
    carry = ""
    with in_path.open("r", encoding="ascii", errors="ignore") as fin, out_path.open("wb") as fout:
        while True:
            chunk = fin.read(CHUNK_SIZE)
            if not chunk:
                break
            data = carry + chunk.replace("\n", "").replace("\r", "").strip()
            if len(data) % 2:
                carry = data[-1]
                data = data[:-1]
            else:
                carry = ""
            if data:
                fout.write(bytes.fromhex(data))
        if carry:
            print(f"Warning: odd hex length, dropped trailing nibble '{carry}'")


def bin_to_hex(in_path: Path, out_path: Path) -> None:
    with in_path.open("rb") as fin, out_path.open("w", encoding="ascii") as fout:
        while True:
            chunk = fin.read(CHUNK_SIZE)
            if not chunk:
                break
            fout.write(chunk.hex())


def resolve_output_path(input_path: Path, mode: str, output: str | None) -> Path:
    if output:
        return Path(output)
    if mode == "hex2bin":
        return input_path.with_suffix(".bin")
    return input_path.with_suffix(".hex")


def detect_mode(input_path: Path, mode: str | None) -> str:
    if mode:
        if mode not in {"hex2bin", "bin2hex"}:
            raise ValueError("mode must be 'hex2bin' or 'bin2hex'")
        return mode
    suffix = input_path.suffix.lower()
    if suffix == ".hex":
        return "hex2bin"
    if suffix == ".bin":
        return "bin2hex"
    raise ValueError("Unable to detect mode from extension; use --mode")


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="Convert between hex text and binary files for arbitrary input files."
    )
    parser.add_argument("input", help="Input file path (.hex or .bin)")
    parser.add_argument("--output", "-o", help="Output file path")
    parser.add_argument("--mode", "-m", choices=["hex2bin", "bin2hex"],
                        help="Conversion mode. If omitted, inferred from input extension")
    parser.add_argument("--chunk", type=int, default=CHUNK_SIZE,
                        help="Chunk size for streaming conversion (bytes)")
    return parser.parse_args()


def main() -> None:
    args = parse_args()
    input_path = Path(args.input)
    if not input_path.exists():
        raise FileNotFoundError(f"Input file not found: {input_path}")

    mode = detect_mode(input_path, args.mode)
    output_path = resolve_output_path(input_path, mode, args.output)
    print(f"Converting {input_path} → {output_path} ({mode})")

    global CHUNK_SIZE
    CHUNK_SIZE = max(1024, args.chunk)

    if mode == "hex2bin":
        hex_to_bin(input_path, output_path)
    else:
        bin_to_hex(input_path, output_path)
    print("Done.")


if __name__ == "__main__":
    main()
