#!/usr/bin/env python
from __future__ import print_function, unicode_literals

import argparse
import io
import logging
import os
import re
import sys


_DESCRIPTION = """\
Migrate arbitrary `.go` sources (mostly) from the v1 to v2 API.
"""
_MIGRATORS = []


def main(sysargs=sys.argv[:]):
    parser = argparse.ArgumentParser(
        description=_DESCRIPTION,
        formatter_class=argparse.ArgumentDefaultsHelpFormatter)
    parser.add_argument('path', nargs='*',
                        type=os.path.abspath, default=os.getcwd())
    parser.add_argument('-w', '--write', help='write changes back to file',
                        action='store_true', default=False)
    parser.add_argument('-q', '--quiet', help='quiet down the logging',
                        action='store_true', default=False)
    parser.add_argument('-D', '--debug', help='debug up the logging',
                        action='store_true',
                        default=(os.environ.get('DEBUG') != ''))
    parser.add_argument('--selftest', help='run internal tests',
                        action='store_true', default=False)

    args = parser.parse_args(sysargs[1:])

    if args.selftest:
        logging.basicConfig(
            level=logging.WARN,
            format='selftest: %(message)s'
        )
        test_migrators()
        return 0

    level = logging.FATAL if args.quiet else logging.INFO
    level = logging.DEBUG if args.debug else level

    logging.basicConfig(level=level, format='%(message)s')

    paths = args.path
    if len(paths) == 0:
        paths = ['.']

    for filepath in _find_candidate_files(paths):
        updated_source = _update_filepath(filepath)
        if args.write:
            logging.info('Updating %s', filepath)

            with io.open(filepath, 'w', encoding='utf-8') as outfile:
                outfile.write(updated_source)
        else:
            logging.info('// Updated %s:', filepath)
            print(updated_source)

    return 0


def _find_candidate_files(paths):
    for path in paths:
        if not os.path.isdir(path):
            yield path
            continue

        for curdir, dirs, files in os.walk(path):
            for i, dirname in enumerate(dirs[:]):
                if dirname.startswith('.'):
                    dirs.pop(i)

            for filename in files:
                if not filename.decode('utf-8').endswith('.go'):
                    continue

                filepath = os.path.join(curdir, filename)
                if not os.access(filepath, os.R_OK | os.W_OK):
                    continue

                yield filepath


def _update_filepath(filepath):
    with io.open(filepath, encoding='utf-8') as infile:
        return _update_source(infile.read())


def _update_source(source):
    for migrator, func in _MIGRATORS:
        logging.debug('Running %s migrator', migrator)
        source = func(source)
    return source


def _subfmt(pattern, replfmt, source, flags=re.UNICODE):
    def repl(match):
        return replfmt.format(**match.groupdict())
    return re.sub(pattern, repl, source, flags=flags)


def _migrator(func):
    _MIGRATORS.append((func.__name__.strip('_'), func))
    return func


@_migrator
def _slice_pointer_types(source):
    return _subfmt(
        '(?P<prefix>\\[\\])cli\\.(?P<type>Command|Author){',
        '{prefix}*cli.{type}{{', source
    )


@_migrator
def _pointer_type_literal(source):
    return _subfmt(
        '(?P<prefix>\\s+)cli\\.(?P<type>Command|Author){',
        '{prefix}&cli.{type}{{', source
    )


@_migrator
def _slice_types(source):
    return _subfmt(
        '&cli\\.(?P<type>IntSlice|StringSlice){(?P<args>[^}]*)}',
        'cli.New{type}({args})', source, flags=re.DOTALL | re.UNICODE
    )


@_migrator
def _flag_literals(source):
    return _subfmt(
        '(?P<prefix>\\s+)cli\\.(?P<type>\\w+)Flag{',
        '{prefix}&cli.{type}Flag{{', source
    )


@_migrator
def _v1_imports(source):
    return re.sub(
        '"(?:github\\.com|gopkg\\.in)/(?:codegangsta|urfave)/cli(?:\\.v1|)"',
        '"gopkg.in/urfave/cli.v2"', source, flags=re.UNICODE
    )


@_migrator
def _new_exit_error(source):
    return re.sub('cli\\.NewExitError', 'cli.Exit', source, flags=re.UNICODE)


@_migrator
def _bool_t_flag(source):
    return _subfmt(
        'cli\\.BoolTFlag{(?P<args>[^}]*)}',
        'cli.BoolFlag{{Value: true,{args}}}',
        source, flags=re.DOTALL | re.UNICODE
    )


@_migrator
def _context_args_len(source):
    return _subfmt(
        'len\\((?P<prefix>\\S+)\\.Args\\(\\)\\)',
        '{prefix}.Args().Len()', source
    )


@_migrator
def _context_args_index(source):
    return _subfmt(
        '\\.Args\\(\\)\\[(?P<index>\\d+)\\]',
        '.Args().Get({index})', source
    )


@_migrator
def _envvar_string(source):
    return re.sub(
        'EnvVar:(?P<ws>\\s+)"(?P<string>[^"]+)"',
        _envvar_string_repl, source, flags=re.UNICODE
    )


def _envvar_string_repl(match):
    return 'EnvVars:{ws}[]string{{{value}}}'.format(
        value=', '.join([
            '"{}"'.format(s) for s in
            re.split(
                '\\s*,\\s*', match.groupdict()['string'],
                flags=re.UNICODE
            )
        ]),
        **match.groupdict()
    )


@_migrator
def _flag_name_stringly(source):
    return re.sub(
        '(?P<prefix>\\s+)Name:(?P<ws>\\s+)"(?P<string>[^"]+)"',
        _flag_name_stringly_repl, source, flags=re.UNICODE
    )


def _flag_name_stringly_repl(match):
    revars = dict(match.groupdict())

    string = revars['string']
    parts = list(
        reversed(
            sorted(
                filter(lambda s: len(s.strip()) > 0, [
                    part.strip() for part in string.split(',')
                ]), key=len
            )
        )
    )

    if len(parts) == 1:
        return '{prefix}Name:{ws}"{string}"'.format(**revars)

    return (
        '{prefix}Name:{ws}"{name}", Aliases: []string{{{aliases}}}'
    ).format(
        name=parts[0],
        aliases=', '.join(['"{}"'.format(s) for s in parts[1:]]),
        **revars
    )


@_migrator
def _commands_opaque_type(source):
    return _subfmt(
        'cli\\.Commands(?P<suffix>[^B])',
        '[]*cli.Command{suffix}',
        source
    )


@_migrator
def _flag_names(source):
    return re.sub('\\.GetName\\(\\)', '.Names()[0]', source, flags=re.UNICODE)


@_migrator
def _app_categories(source):
    source = _subfmt(
        '(?P<prefix>range\\s+\\S+)\\.App\\.Categories\\(\\)',
        '{prefix}.App.Categories.Categories()', source
    )

    return re.sub(
        '\\.App\\.Categories\\(\\)', '.App.Categories',
        source, flags=re.UNICODE
    )


@_migrator
def _command_category_commands(source):
    # XXX: brittle
    return _subfmt(
        '(?P<prefix>\\s+category\\.)Commands(?P<suffix>[^(])',
        '{prefix}VisibleCommands(){suffix}', source
    )


@_migrator
def _context_bool_t(source):
    # XXX: probably brittle
    return _subfmt(
        '(?P<prefix>\\S+)(?:Global|)BoolT\\(',
        '!{prefix}Bool(', source
    )


@_migrator
def _context_global_methods(source):
    return _subfmt(
        '\\.Global(?P<method>'
        'Bool|Duration|Float64|Generic|Int|IntSlice|String|StringSlice|'
        'FlagNames|IsSet|Set'
        ')\\(',
        '.{method}(', source
    )


@_migrator
def _context_parent(source):
    # XXX: brittle
    return re.sub('\\.Parent\\(\\)', '.Lineage()[1]', source, flags=re.UNICODE)


@_migrator
def _app_init(source):
    return re.sub(
        'cli\\.NewApp\\(\\)', '(&cli.App{})', source, flags=re.UNICODE
    )


@_migrator
def _bash_complete(source):
    return re.sub(
        'BashComplete:', 'ShellComplete:',
        re.sub('\\.BashComplete', '.ShellComplete', source, flags=re.UNICODE))


@_migrator
def _enable_bash_completion(source):
    return re.sub(
        '\\.EnableBashCompletion', '.EnableShellCompletion', source, flags=re.UNICODE
    )


@_migrator
def _bash_completion_flag(source):
    return re.sub(
        'cli\\.BashCompletionFlag', 'cli.GenerateCompletionFlag', source, flags=re.UNICODE
    )


def test_migrators():
    import difflib

    for i, (source, expected) in enumerate(_MIGRATOR_TESTS):
        actual = _update_source(source)
        if expected != actual:
            udiff = difflib.unified_diff(
                expected.splitlines(), actual.splitlines(),
                fromfile='a/source.go', tofile='b/source.go', lineterm=''
            )
            for line in udiff:
                print(line)
            raise AssertionError('migrated source does not match expected')
        logging.warn('Test case %d/%d OK', i+1, len(_MIGRATOR_TESTS))


_MIGRATOR_TESTS = (
    ("""
\t\t\t&cli.StringSlice{"a", "b", "c"},
""", """
\t\t\tcli.NewStringSlice("a", "b", "c"),
"""),
    ("""
\t\tcli.IntFlag{
\t\t\tName:  "yep",
\t\t\tValue: 3,
\t\t}
""", """
\t\t&cli.IntFlag{
\t\t\tName:  "yep",
\t\t\tValue: 3,
\t\t}
"""),
    ("""
\t\tapp.Commands = []cli.Command{
\t\t\t{
\t\t\t\tName: "whatebbs",
\t\t\t},
\t\t}
""", """
\t\tapp.Commands = []*cli.Command{
\t\t\t{
\t\t\t\tName: "whatebbs",
\t\t\t},
\t\t}
"""),
    ("""
\t\tapp.Commands = []cli.Command{
\t\t\tcli.Command{
\t\t\t\tName: "whatebbs",
\t\t\t},
\t\t}
""", """
\t\tapp.Commands = []*cli.Command{
\t\t\t&cli.Command{
\t\t\t\tName: "whatebbs",
\t\t\t},
\t\t}
"""),
    ("""
\t"github.com/codegangsta/cli"
\t"github.com/urfave/cli"
\t"gopkg.in/codegangsta/cli"
\t"gopkg.in/codegangsta/cli.v1"
\t"gopkg.in/urfave/cli"
\t"gopkg.in/urfave/cli.v1"
""", """
\t"gopkg.in/urfave/cli.v2"
\t"gopkg.in/urfave/cli.v2"
\t"gopkg.in/urfave/cli.v2"
\t"gopkg.in/urfave/cli.v2"
\t"gopkg.in/urfave/cli.v2"
\t"gopkg.in/urfave/cli.v2"
"""),
    ("""
\t\t\t\treturn cli.NewExitError("foo whatebber", 9)
""", """
\t\t\t\treturn cli.Exit("foo whatebber", 9)
"""),
    ("""
\t\t\tapp.Flags = []cli.Flag{
\t\t\t\tcli.StringFlag{
\t\t\t\t\tName: "aha",
\t\t\t\t},
\t\t\t\tcli.BoolTFlag{
\t\t\t\t\tName: "blurp",
\t\t\t\t},
\t\t\t}
""", """
\t\t\tapp.Flags = []cli.Flag{
\t\t\t\t&cli.StringFlag{
\t\t\t\t\tName: "aha",
\t\t\t\t},
\t\t\t\t&cli.BoolFlag{Value: true,
\t\t\t\t\tName: "blurp",
\t\t\t\t},
\t\t\t}
"""),
    ("""
\t\t\tAction = func(c *cli.Context) error {
\t\t\t\tif c.Args()[4] == "meep" {
\t\t\t\t\treturn nil
\t\t\t\t}
\t\t\t\treturn errors.New("mope")
\t\t\t}
""", """
\t\t\tAction = func(c *cli.Context) error {
\t\t\t\tif c.Args().Get(4) == "meep" {
\t\t\t\t\treturn nil
\t\t\t\t}
\t\t\t\treturn errors.New("mope")
\t\t\t}
"""),
    ("""
\t\tapp.Flags = []cli.Flag{
\t\t\tcli.StringFlag{
\t\t\t\tName:   "toots",
\t\t\t\tEnvVar: "TOOTS,TOOTERS",
\t\t\t},
\t\t}
""", """
\t\tapp.Flags = []cli.Flag{
\t\t\t&cli.StringFlag{
\t\t\t\tName:   "toots",
\t\t\t\tEnvVars: []string{"TOOTS", "TOOTERS"},
\t\t\t},
\t\t}
"""),
    ("""
\t\tapp.Flags = []cli.Flag{
\t\t\tcli.StringFlag{
\t\t\t\tName:   "t, tootles, toots",
\t\t\t},
\t\t}
""", """
\t\tapp.Flags = []cli.Flag{
\t\t\t&cli.StringFlag{
\t\t\t\tName:   "tootles", Aliases: []string{"toots", "t"},
\t\t\t},
\t\t}
"""),
    ("""
\t\tapp := cli.NewApp()
\t\tapp.HideHelp = true
""", """
\t\tapp := (&cli.App{})
\t\tapp.HideHelp = true
""")
)


if __name__ == '__main__':
    sys.exit(main())
