from __future__ import annotations

__lazy_modules__ = ["base64", "pathlib", "timeit"]

import argparse
import base64
import sys
from pathlib import Path
from timeit import default_timer as timer

import pybase64

TYPE_CHECKING = False
if TYPE_CHECKING:
    from collections.abc import Sequence
    from types import ModuleType
    from typing import Any


def bench_one(
    duration: float,
    data: bytes,
    module: ModuleType,
    altchars: bytes | None,
    kwargs: dict[str, bool | bytes],
) -> None:
    duration = duration / 2.0
    combination_unsupported = all(
        [
            module is base64,
            "ignorechars" in kwargs or "padded" in kwargs,
            sys.version_info < (3, 15),
        ],
    )
    validate = kwargs["validate"]
    if validate and altchars is None and "ignorechars" not in kwargs and "padded" not in kwargs:
        encbytes = module.encodebytes
        number = 0
        time = timer()
        while True:
            encodedcontent = encbytes(data)
            number += 1
            if timer() - time > duration:
                break
        iter_ = number
        time = timer()
        while iter_ > 0:
            encodedcontent = encbytes(data)
            iter_ -= 1
        time = timer() - time
        print(
            "{:<24s} {:5.0f} MB/s ({:,d} bytes -> {:,d} bytes)".format(
                module.__name__ + "." + encbytes.__name__ + ":",
                ((number * len(data)) / (1024.0 * 1024.0)) / time,
                len(data),
                len(encodedcontent),
            ),
        )

    if validate and "ignorechars" not in kwargs:
        enc = module.b64encode
        if combination_unsupported:
            print("{:<24s}       N/A".format(module.__name__ + "." + enc.__name__ + ":"))
        else:
            number = 0
            time = timer()
            while True:
                encodedcontent = enc(data, altchars=altchars)
                number += 1
                if timer() - time > duration:
                    break
            iter_ = number
            time = timer()
            while iter_ > 0:
                encodedcontent = enc(data, altchars=altchars)
                iter_ -= 1
            time = timer() - time
            print(
                "{:<24s} {:5.0f} MB/s ({:,d} bytes -> {:,d} bytes)".format(
                    module.__name__ + "." + enc.__name__ + ":",
                    ((number * len(data)) / (1024.0 * 1024.0)) / time,
                    len(data),
                    len(encodedcontent),
                ),
            )

    if kwargs.get("ignorechars") == b"\n" or not validate:
        encodedcontent = pybase64.b64encode(data, altchars=altchars, wrapcol=76)
    else:
        encodedcontent = pybase64.b64encode(data, altchars=altchars)
    dec = module.b64decode
    if combination_unsupported:
        print("{:<24s}       N/A".format(module.__name__ + "." + dec.__name__ + ":"))
    else:
        number = 0
        time = timer()
        while True:
            decodedcontent = dec(encodedcontent, altchars=altchars, **kwargs)
            number += 1
            if timer() - time > duration:
                break
        iter_ = number
        time = timer()
        while iter_ > 0:
            decodedcontent = dec(encodedcontent, altchars=altchars, **kwargs)
            iter_ -= 1
        time = timer() - time
        print(
            "{:<24s} {:5.0f} MB/s ({:,d} bytes -> {:,d} bytes)".format(
                module.__name__ + "." + dec.__name__ + ":",
                ((number * len(data)) / (1024.0 * 1024.0)) / time,
                len(encodedcontent),
                len(data),
            ),
        )
        assert decodedcontent == data  # noqa: S101


def readall(file: str) -> bytes:
    if file == "-":
        return sys.stdin.buffer.read()
    return Path(file).read_bytes()


def writeall(file: str, data: bytes) -> None:
    if file == "-":
        sys.stdout.buffer.write(data)
    else:
        Path(file).write_bytes(data)


def benchmark(*, duration: float, input: str) -> None:  # noqa: A002
    print(__package__ + " " + pybase64.get_version())
    data = readall(input)
    for altchars in [None, b"-_"]:
        for validate in [True, False]:
            for ignorechars in [None, b"", b"\n"]:
                for padded in [True, False]:
                    # skip redundant combinations
                    if altchars is None and validate and padded and ignorechars == b"":
                        # same as "altchars is None and validate and padded and ignorechars is None"
                        # when altchars is not None, the alphabet translation differs
                        continue
                    kwargs: dict[str, bool | bytes] = {"validate": validate}
                    if ignorechars is not None:
                        if not validate:
                            continue
                        kwargs["ignorechars"] = ignorechars
                    if not padded:
                        kwargs["padded"] = padded
                    title = f"bench: altchars={altchars!r:s}"
                    if ignorechars is None:
                        title = f"{title}, validate={validate!r:s}"
                    else:
                        title = f"{title}, ignorechars={ignorechars!r:s}"
                    print(f"{title}, padded={padded!r:s}")
                    for module in [pybase64, base64]:
                        bench_one(duration, data, module, altchars, kwargs)


def encode(*, input: str, altchars: bytes | None, output: str) -> None:  # noqa: A002
    data = readall(input)
    data = pybase64.b64encode(data, altchars)
    writeall(output, data)


def decode(*, input: str, altchars: bytes | None, validate: bool, output: str) -> None:  # noqa: A002
    data = readall(input)
    data = pybase64.b64decode(data, altchars, validate)
    writeall(output, data)


class LicenseAction(argparse.Action):
    def __init__(
        self,
        option_strings: Sequence[str],
        dest: str,
        license: str | None = None,  # noqa: A002
        help: str | None = "show license information and exit",  # noqa: A002
    ) -> None:
        super().__init__(
            option_strings=option_strings,
            dest=dest,
            default=argparse.SUPPRESS,
            nargs=0,
            help=help,
        )
        self.license = license

    def __call__(
        self,
        parser: argparse.ArgumentParser,
        namespace: argparse.Namespace,  # noqa: ARG002
        values: str | Sequence[Any] | None,  # noqa: ARG002
        option_string: str | None = None,  # noqa: ARG002
    ) -> None:
        print(self.license)
        parser.exit()


def check_file(value: str, *, is_input: bool) -> str:
    if value == "-":
        return value
    path = Path(value)
    if is_input:
        return str(path.resolve(strict=True))
    return str(path.parent.resolve(strict=True) / path.name)


def main(argv: Sequence[str] | None = None) -> None:
    # main parser
    parser = argparse.ArgumentParser(
        prog=__package__,
        description=__package__ + " command-line tool.",
    )
    parser.add_argument(
        "-V",
        "--version",
        action="version",
        version=__package__ + " " + pybase64.get_version(),
    )
    parser.add_argument("--license", action=LicenseAction, license=pybase64.get_license_text())
    # create sub-parsers
    subparsers = parser.add_subparsers(help="tool help")
    # benchmark parser
    benchmark_parser = subparsers.add_parser("benchmark", help="-h for usage")
    benchmark_parser.add_argument(
        "-d",
        "--duration",
        metavar="D",
        dest="duration",
        type=float,
        default=1.0,
        help="expected duration for a single encode or decode test",
    )
    benchmark_parser.register("type", "input file", lambda s: check_file(s, is_input=True))
    benchmark_parser.add_argument(
        "input",
        type="input file",
        help="input file used for the benchmark",
    )
    benchmark_parser.set_defaults(func=benchmark)
    # encode parser
    encode_parser = subparsers.add_parser("encode", help="-h for usage")
    encode_parser.register("type", "input file", lambda s: check_file(s, is_input=True))
    encode_parser.register("type", "output file", lambda s: check_file(s, is_input=False))
    encode_parser.add_argument("input", type="input file", help="input file to be encoded")
    group = encode_parser.add_mutually_exclusive_group()
    group.add_argument(
        "-u",
        "--url",
        action="store_const",
        const=b"-_",
        dest="altchars",
        help="use URL encoding",
    )
    group.add_argument(
        "-a",
        "--altchars",
        dest="altchars",
        help="use alternative characters for encoding",
    )
    encode_parser.add_argument(
        "-o",
        "--output",
        dest="output",
        type="output file",
        default="-",
        help="encoded output file (default to stdout)",
    )
    encode_parser.set_defaults(func=encode)
    # decode parser
    decode_parser = subparsers.add_parser("decode", help="-h for usage")
    decode_parser.register("type", "input file", lambda s: check_file(s, is_input=True))
    decode_parser.register("type", "output file", lambda s: check_file(s, is_input=False))
    decode_parser.add_argument("input", type="input file", help="input file to be decoded")
    group = decode_parser.add_mutually_exclusive_group()
    group.add_argument(
        "-u",
        "--url",
        action="store_const",
        const=b"-_",
        dest="altchars",
        help="use URL decoding",
    )
    group.add_argument(
        "-a",
        "--altchars",
        dest="altchars",
        help="use alternative characters for decoding",
    )
    decode_parser.add_argument(
        "-o",
        "--output",
        dest="output",
        type="output file",
        default="-",
        help="decoded output file (default to stdout)",
    )
    decode_parser.add_argument(
        "--no-validation",
        dest="validate",
        action="store_false",
        help="disable validation of the input data",
    )
    decode_parser.set_defaults(func=decode)
    # ready, parse
    if argv is None:
        argv = sys.argv[1:]
    if len(argv) == 0:
        argv = ["-h"]
    args = vars(parser.parse_args(args=argv))
    func = args.pop("func")
    func(**args)


if __name__ == "__main__":
    main()
