import argparse
import base64
import sys
from base64 import b64decode as b64decodeValidate
from base64 import encodebytes as b64encodebytes
from timeit import default_timer as timer

import pybase64


def bench_one(duration, data, enc, dec, encbytes, altchars=None,
              validate=False):
    duration = duration / 2.0

    if not validate and altchars is None:
        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('{0:<32s} {1:9.3f} MB/s ({2:,d} bytes -> {3:,d} bytes)'.format(
            encbytes.__module__ + '.' + encbytes.__name__ + ':',
            ((number * len(data)) / (1024.0 * 1024.0)) / time,
            len(data), len(encodedcontent)))

    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('{0:<32s} {1:9.3f} MB/s ({2:,d} bytes -> {3:,d} bytes)'.format(
        enc.__module__ + '.' + enc.__name__ + ':',
        ((number * len(data)) / (1024.0 * 1024.0)) / time,
        len(data), len(encodedcontent)))

    number = 0
    time = timer()
    while True:
        decodedcontent = dec(encodedcontent,
                             altchars=altchars,
                             validate=validate)
        number += 1
        if timer() - time > duration:
            break
    iter = number
    time = timer()
    while iter > 0:
        decodedcontent = dec(encodedcontent,
                             altchars=altchars,
                             validate=validate)
        iter -= 1
    time = timer() - time
    print('{0:<32s} {1:9.3f} MB/s ({3:,d} bytes -> {2:,d} bytes)'.format(
        dec.__module__ + '.' + dec.__name__ + ':',
        ((number * len(data)) / (1024.0 * 1024.0)) / time,
        len(data), len(encodedcontent)))
    assert decodedcontent == data


def readall(file):
    if file == sys.stdin:
        if hasattr(file, 'buffer'):
            # Python 3 does not honor the binary flag,
            # read from the underlying buffer
            return file.buffer.read()
        else:
            return file.read()
        # do not close the file
    else:
        try:
            return file.read()
        finally:
            file.close()


def writeall(file, data):
    if file == sys.stdout:
        if hasattr(file, 'buffer'):
            # Python 3 does not honor the binary flag,
            # write to the underlying buffer
            file.buffer.write(data)
        else:
            file.write(data)
        # do not close the file
    else:
        try:
            file.write(data)
        finally:
            file.close()


def benchmark(args):
    print(__package__ + ' ' + pybase64.get_version())
    data = readall(args.input)
    for altchars in [None, b'-_']:
        for validate in [False, True]:
            print('bench: altchars={0:s}, validate={1:s}'.format(
                  repr(altchars), repr(validate)))
            bench_one(args.duration,
                      data,
                      pybase64.b64encode,
                      pybase64.b64decode,
                      pybase64.encodebytes,
                      altchars,
                      validate)
            bench_one(args.duration,
                      data,
                      base64.b64encode,
                      b64decodeValidate,
                      b64encodebytes,
                      altchars,
                      validate)


def encode(args):
    data = readall(args.input)
    data = pybase64.b64encode(data, args.altchars)
    writeall(args.output, data)


def decode(args):
    data = readall(args.input)
    data = pybase64.b64decode(data, args.altchars, args.validate)
    writeall(args.output, data)


class LicenseAction(argparse.Action):

    def __init__(self,
                 option_strings,
                 license=None,
                 dest=argparse.SUPPRESS,
                 default=argparse.SUPPRESS,
                 help="show license information and exit"):
        super(LicenseAction, self).__init__(
            option_strings=option_strings,
            dest=dest,
            default=default,
            nargs=0,
            help=help)
        self.license = license

    def __call__(self, parser, namespace, values, option_string=None):
        print(self.license)
        parser.exit()


def main(args=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.add_argument(
        'input',
        type=argparse.FileType('rb'),
        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.add_argument(
        'input',
        type=argparse.FileType('rb'),
        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=argparse.FileType('wb'),
        default=sys.stdout,
        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.add_argument(
        'input',
        type=argparse.FileType('rb'),
        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=argparse.FileType('wb'),
        default=sys.stdout,
        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 args is None:
        args = sys.argv[1:]
    if len(args) == 0:
        args = ['-h']
    args = parser.parse_args(args=args)
    args.func(args)


if __name__ == "__main__":
    main()
