import os
import argparse
import platform
import zipfile
import re
import sys

import reCBZ
from reCBZ import wrappers, util
from reCBZ.config import Config
from reCBZ.profiles import profiles_list


def print_title() -> None:
    align = int(Config.term_width() / 2) - 11
    if align > 21: align = 21
    if align + 22 > Config.term_width() or align < 0:
        align = 0
    align = align * ' '
    title_multiline = (f"{align}┬─┐┌─┐┌─┐┌┐ ┌─┐ ┌─┐┬ ┬\n"
                       f"{align}├┬┘├┤ │  ├┴┐┌─┘ ├─┘└┬┘\n"
                       f"{align}┴└─└─┘└─┘└─┘└─┘o┴   ┴")
    print(title_multiline)


def unix_like_glob(arglist:list) -> list:
    """What we're essentially trying to supplant here is powershell's obtuse
    Get-ChildItem 'pattern' | foreach {cmd $_.FullName}, because it's just
    ridiculous expecting users to memorize that when a simple asterisk could do
    the job. PS if you're using Windows this is a reminder your OS SUCKS ASS"""
    import glob

    new = []
    for arg in arglist:
        if '*' in arg:
            new.extend(glob.glob(str(arg)))
        else:
            new.append(arg)

    return new


def main():
    # o god who art in heaven please guard mine anime girls
    mutually_exclusive_groups = []
    wiki='https://github.com/avalonv/reCBZ/wiki'
    desc='''accepted formats: .zip, .epub, .cbz. repacks to .cbz by default'''
    parser = argparse.ArgumentParser(
            prog=reCBZ.CMDNAME,
            usage="%(prog)s [options] files",
            description=desc,
            epilog=f"for detailed documentation, see {wiki}",)

    parser.add_argument( "-v", "--verbose", # log_group
        default=None,
        dest="loglevel",
        action="count",
        help="increase verbosity of messages, repeatable: -vv")
    parser.add_argument( "-s", "--silent", # log_group
        default=None,
        const=-1,
        dest="loglevel",
        action="store_const",
        help="disable progress messages")
    parser.add_argument( "-d", "--dry",
        default=None,
        dest="no_write",
        action="store_true",
        help="dry run, no changes are saved at the end (safe)")
    parser.add_argument( "-O", "--overwrite",
        default=None,
        dest="overwrite",
        action="store_true",
        help="overwrite the original archive")
    parser.add_argument( "-F", "--force",
        default=None,
        dest="force_write",
        action="store_true",
        help="write archive even if there are page errors (dangerous)")
    # parser.add_argument( "-u", "--unpack", # mode_group
    #     default=None,
    #     const='unpack',
    #     dest="mode",
    #     action="store_const",
    #     help="unpack the archive to the current directory (safe)")
    parser.add_argument( "--compare", # mode_group
        default=None,
        const='compare',
        dest="mode",
        action="store_const",
        help=argparse.SUPPRESS)
    parser.add_argument( "-a", "--assist", # mode_group
        default=None,
        const='assist',
        dest="mode",
        action="store_const",
        help="calculate size for each image format, ask which to use")
    parser.add_argument( "-A" ,"--auto", # mode_group
        default=None,
        const='auto',
        dest="mode",
        action="store_const",
        help="calculate size for each image format, pick smallest one")
    parser.add_argument( "-J" ,"--join", # mode_group
        default=None,
        const='join',
        dest="mode",
        action="store_const",
        help="append the contents of each file to the leftmost file")
    parser.add_argument( "--noprev",
        default=False,
        dest="noprev",
        action="store_true",
        help="ignore previously repacked files")
    log_group = ('verbose', 'silent')
    mutually_exclusive_groups.append(log_group)
    mode_group = ('compare', 'assist', 'a', 'auto', 'A', 'join', 'J')
    mutually_exclusive_groups.append(mode_group)

    archive_group = parser.add_argument_group(title="archive options")
    archive_group.add_argument( "-p", "--profile",
        default=None,
        # best to handle it externally as we always convert to upper case
        # choices=([prof.nickname for prof in profiles_list]),
        metavar="",
        dest="profile",
        type=str,
        help="target eReader profile. run --profiles to see options")
    archive_group.add_argument( "--profiles",
        default=False,
        dest="show_profiles",
        action='store_true',
        help=argparse.SUPPRESS)
    archive_group.add_argument( "--epub", # ext_group
        default=None,
        const='epub',
        dest="archive_format",
        action="store_const",
        help="save archive as epub")
    archive_group.add_argument( "--zip", # ext_group
        default=None,
        const='zip',
        dest="archive_format",
        action="store_const",
        help="save archive as zip")
    archive_group.add_argument( "--cbz", # ext_group
        default=None,
        const='cbz',
        dest="archive_format",
        action="store_const",
        help="save archive as cbz")
    archive_group.add_argument( "--compress",
        default=None,
        dest="compress_zip",
        action="store_true",
        help="attempt to further compress the archive when repacking")
    ext_group = ('epub', 'zip', 'cbz')
    mutually_exclusive_groups.append(ext_group)

    images_group = parser.add_argument_group(title="image options")
    images_group.add_argument( "-c", "--convert",
        default=None,
        choices=('jpeg', 'png', 'webp', 'webpll'),
        metavar="",
        dest="img_format",
        type=str,
        help="format to convert pages to: jpeg, webp, webpll, or png")
    images_group.add_argument( "--imgfmt", # deprecated
        default=None,
        choices=('jpeg', 'png', 'webp', 'webpll'),
        metavar="",
        dest="img_format",
        type=str,
        help=argparse.SUPPRESS)
    images_group.add_argument( "--quality",
        default=None,
        choices=(range(1,101)),
        metavar="0-95",
        dest="img_quality",
        type=int,
        help="save quality for lossy formats. >90 not recommended")
    images_group.add_argument( "--size",
        default=None,
        metavar="WidthxHeight",
        dest="size_str",
        type=str,
        help="rescale images to the specified resolution")
    images_group.add_argument( "--noup", # rescale_group
        default=None,
        dest="no_upscale",
        action="store_true",
        help="disable upscaling with --size")
    images_group.add_argument( "--nodown", # rescale_group
        default=None,
        dest="no_downscale",
        action="store_true",
        help="disable downscaling with --size")
    images_group.add_argument( "--nowebp",
        default=None,
        const=f'{Config.blacklisted_fmts} webp webpll',
        dest="blacklisted_fmts",
        action="store_const",
        help="exclude webp from --auto and --assist")
    images_group.add_argument( "--bw", # color_group
        default=None,
        dest="grayscale",
        action="store_true",
        help="convert images to grayscale")
    images_group.add_argument( "--color", # color_group
        default=None,
        dest="grayscale",
        action="store_false",
        help="preserve color when using --profile")
    rescale_group = ('noup', 'nodown')
    color_group = ('bw', 'color')
    mutually_exclusive_groups.append(rescale_group)
    mutually_exclusive_groups.append(color_group)

    others_group = parser.add_argument_group(title="other")
    others_group.add_argument("--process", # process_group
        default=None,
        choices=(range(1,33)),
        metavar="1-32",
        dest="processes",
        type=int,
        help="maximum number of processes to spawn")
    others_group.add_argument( "--sequential", # process_group
        default=None,
        dest="no_parallel",
        action="store_true",
        help="disable multiprocessing")
    others_group.add_argument( "--config",
        default=None,
        dest="show_config",
        action="store_true",
        help="show settings and exit")
    others_group.add_argument( "--version",
        default=None,
        dest="show_version",
        action="store_true",
        help="show version and exit")
    process_group = ('process', 'sequential')
    mutually_exclusive_groups.append(process_group)

    args, unknown_args = parser.parse_known_args()

    if args.show_version:
        print(f'{reCBZ.CMDNAME} v{reCBZ.__version__}')
        exit(0)

    # this is admittedly rather dumb, but we're trying to overcome argparse's
    # inability to include options in both groups and mutually_exclusive_groups
    # at the same time
    # more details here: https://github.com/python/cpython/issues/55193
    bad_args = []
    for mutex_group in mutually_exclusive_groups:
        group_matches = []
        for arg in sys.argv:
            for mutex_arg in mutex_group:
                p = re.compile(f'^[-]{{1,2}}{mutex_arg}')
                if p.match(arg) and arg not in group_matches:
                    group_matches.append(arg)
        if len(group_matches) >= 2: # only handle one pair at a time
            bad_args.append(group_matches[:2])

    if len(bad_args) >= 1:
        arg1, arg2 = bad_args[0] # only handle one group at a time
        print(f'{reCBZ.CMDNAME}: error: argument {arg1} not allowed with argument {arg2}')
        exit(1)

    # set profile first, ensure it can be overridden by explicit options
    if args.profile is not None:
        prof_name = args.profile.upper()
        try:
            Config.set_profile(prof_name)
        except ValueError:
            print(f'{reCBZ.CMDNAME}: profile: invalid option "{prof_name}"')
            exit(1)

    if args.size_str is not None:
        newsize = args.size_str.lower().strip()
        try:
            newsize = tuple(map(int,newsize.split('x')))
            assert len(newsize) == 2
            Config.img_size = newsize
        except (ValueError, AssertionError):
            print(f'{reCBZ.CMDNAME}: size: invalid option "{args.size_str}"')
            exit(1)

    # this is probably not the most pythonic way to do this
    # I'm sorry guido-san...
    for key, val in args.__dict__.items():
        if key in Config.__dict__.keys() and val is not None:
            setattr(Config, key, val)

    if args.show_config:
        private = re.compile('classmethod|^_')
        for key, val in Config.__dict__.items():
            if not private.search(str(key)) and not private.search(str(val)):
                print(f"{key} = {val}")
        exit(0)

    if args.show_profiles:
        print(f'{reCBZ.CMDNAME} -p ...')
        for prof in profiles_list:
            print(prof.nickname, '=', prof.desc)
        exit(0)

    # parse files
    if platform.system() == 'Windows':
        unknown_args = unix_like_glob(unknown_args)
    paths = []
    for arg in unknown_args:
        if os.path.isfile(arg):
            paths.append(arg)
        elif os.path.isdir(arg):
            print(f'{reCBZ.CMDNAME}: {arg}: is a directory')
            parser.print_usage()
            exit(1)
        else:
            parser.print_help()
            print(f'\nunknown file or option: {arg}')
            exit(1)

    if len(paths) <= 0:
        print(f'{reCBZ.CMDNAME}: missing input file (see --help)')
        parser.print_usage()
        exit(1)

    if args.mode == 'join':
        if not len(paths) >= 2:
            print(f'{reCBZ.CMDNAME}: join: at least two files are needed')
            exit(1)

    if args.noprev:
        new = []
        for filename in paths:
            comment = zipfile.ZipFile(filename).comment
            if not comment == str.encode(Config.ZIPCOMMENT):
                new.append(filename)
        diff = len(paths) - len(new)
        if diff > 0:
            print(f'{reCBZ.CMDNAME}: noprev: ignoring {diff} files')
            if len(new) == 0:
                exit(1)
            else:
                paths = new

    # everything passed. do stuff
    exit_code = 0
    if reCBZ.SHOWTITLE: print_title()
    try:
        if args.mode == 'join':
            wrappers.join_archives(paths[0], paths[1:])
        for filename in paths:
            try:
                if args.mode is None:
                    wrappers.repack_archive(filename)
                elif args.mode == 'unpack':
                    wrappers.unpack_archive(filename)
                elif args.mode == 'compare':
                    wrappers.compare_fmts_archive(filename)
                elif args.mode == 'assist':
                    wrappers.assist_repack_archive(filename)
                elif args.mode == 'auto':
                    wrappers.auto_repack_archive(filename)
            except (wrappers.AbortedRepackError, wrappers.AbortedCompareError):
                exit_code = 2
                continue
    except (KeyboardInterrupt, util.MPrunnerInterrupt):
        print('\nGoooooooooodbye')
        exit(1)
    except wrappers.AbortedRepackError:
        exit_code = 2

    return exit_code

if __name__ == '__main__':
    # catching errors here won't always work, presumably because of namespace
    # mangling
    exit(main())
