#!/usr/bin/env python
"""
The flag types that ship with the cli library have many things in common, and
so we can take advantage of the `go generate` command to create much of the
source code from a list of definitions.  These definitions attempt to cover
the parts that vary between flag types, and should evolve as needed.

An example of the minimum definition needed is:

    {
      "name": "SomeType",
      "type": "sometype",
      "context_default": "nil"
    }

In this example, the code generated for the `cli` package will include a type
named `SomeTypeFlag` that is expected to wrap a value of type `sometype`.
Fetching values by name via `*cli.Context` will default to a value of `nil`.

A more complete, albeit somewhat redundant, example showing all available
definition keys is:

    {
      "name": "VeryMuchType",
      "type": "*VeryMuchType",
      "value": true,
      "dest": false,
      "doctail": " which really only wraps a []float64, oh well!",
      "context_type": "[]float64",
      "context_default": "nil",
      "parser": "parseVeryMuchType(f.Value.String())",
      "parser_cast": "[]float64(parsed)"
    }

The meaning of each field is as follows:

               name (string) - The type "name", which will be suffixed with
                               `Flag` when generating the type definition
                               for `cli` and the wrapper type for `altsrc`
               type (string) - The type that the generated `Flag` type for
                               `cli` is expected to "contain" as its `.Value`
                               member
                value (bool) - Should the generated `cli` type have a `Value`
                               member?
                 dest (bool) - Should the generated `cli` type support a
                               destination pointer?
            doctail (string) - Additional docs for the `cli` flag type comment
       context_type (string) - The literal type used in the `*cli.Context`
                               reader func signature
    context_default (string) - The literal value used as the default by the
                               `*cli.Context` reader funcs when no value is
                               present
             parser (string) - Literal code used to parse the flag `f`,
                               expected to have a return signature of
                               (value, error)
        parser_cast (string) - Literal code used to cast the `parsed` value
                               returned from the `parser` code
"""

from __future__ import print_function, unicode_literals

import argparse
import json
import os
import subprocess
import sys
import tempfile
import textwrap


_PY3 = sys.version_info.major == 3


class _FancyFormatter(argparse.ArgumentDefaultsHelpFormatter,
                      argparse.RawDescriptionHelpFormatter):
    pass


def main(sysargs=sys.argv[:]):
    parser = argparse.ArgumentParser(
        description='Generate flag type code!',
        formatter_class=_FancyFormatter)
    parser.add_argument(
        'package',
        type=str, default='cli', choices=_WRITEFUNCS.keys(),
        help='Package for which flag types will be generated'
    )
    parser.add_argument(
        '-i', '--in-json',
        type=argparse.FileType('r'),
        default=sys.stdin,
        help='Input JSON file which defines each type to be generated'
    )
    parser.add_argument(
        '-o', '--out-go',
        type=argparse.FileType('w'),
        default=sys.stdout,
        help='Output file/stream to which generated source will be written'
    )
    parser.epilog = __doc__

    args = parser.parse_args(sysargs[1:])
    _generate_flag_types(_WRITEFUNCS[args.package], args.out_go, args.in_json)
    return 0


def _generate_flag_types(writefunc, output_go, input_json):
    types = json.load(input_json)

    tmp = _get_named_tmp_go()
    writefunc(tmp, types)
    tmp.close()

    new_content = subprocess.check_output(
        ['goimports', tmp.name]
    ).decode('utf-8')

    print(new_content, file=output_go, end='')
    output_go.flush()
    os.remove(tmp.name)


def _get_named_tmp_go():
    tmp_args = dict(suffix='.go', mode='w', delete=False)
    if _PY3:
        tmp_args['encoding'] = 'utf-8'
    return tempfile.NamedTemporaryFile(**tmp_args)


def _set_typedef_defaults(typedef):
    typedef.setdefault('doctail', '')
    typedef.setdefault('context_type', typedef['type'])
    typedef.setdefault('dest', True)
    typedef.setdefault('parser', 'f.Value, error(nil)')
    typedef.setdefault('parser_cast', 'parsed')


def _write_cli_flag_types(outfile, types):
    _fwrite(outfile, """\
        package cli

        // WARNING: This file is generated!

        """)

    for typedef in types:
        _set_typedef_defaults(typedef)

        _fwrite(outfile, """\
        // {name}Flag is a flag with type {type}{doctail}
        type {name}Flag struct {{
            Name string
            Aliases []string
            Usage string
            EnvVars []string
            Hidden bool
            Value {type}
            DefaultText string
        """.format(**typedef))

        if typedef['dest']:
            _fwrite(outfile, """\
            Destination *{type}
            """.format(**typedef))

        _fwrite(outfile, "\n}\n\n")

        _fwrite(outfile, """\
            // String returns a readable representation of this value
            // (for usage defaults)
            func (f *{name}Flag) String() string {{
                return FlagStringer(f)
            }}

            // Names returns the names of the flag
            func (f *{name}Flag) Names() []string {{
                return flagNames(f)
            }}

            // {name} looks up the value of a local {name}Flag, returns
            // {context_default} if not found
            func (c *Context) {name}(name string) {context_type} {{
                if fs := lookupFlagSet(name, c); fs != nil {{
                    return lookup{name}(name, fs)
                }}
                return {context_default}
            }}

            func lookup{name}(name string, set *flag.FlagSet) {context_type} {{
                f := set.Lookup(name)
                if f != nil {{
                    parsed, err := {parser}
                    if err != nil {{
                        return {context_default}
                    }}
                    return {parser_cast}
                }}
                return {context_default}
            }}
            """.format(**typedef))


def _write_altsrc_flag_types(outfile, types):
    _fwrite(outfile, """\
        package altsrc

        import "gopkg.in/urfave/cli.v2"

        // WARNING: This file is generated!

        """)

    for typedef in types:
        _set_typedef_defaults(typedef)

        _fwrite(outfile, """\
        // {name}Flag is the flag type that wraps cli.{name}Flag to allow
        // for other values to be specified
        type {name}Flag struct {{
            *cli.{name}Flag
            set *flag.FlagSet
        }}

        // New{name}Flag creates a new {name}Flag
        func New{name}Flag(fl *cli.{name}Flag) *{name}Flag {{
            return &{name}Flag{{{name}Flag: fl, set: nil}}
        }}

        // Apply saves the flagSet for later usage calls, then calls the
        // wrapped {name}Flag.Apply
        func (f *{name}Flag) Apply(set *flag.FlagSet) {{
            f.set = set
            f.{name}Flag.Apply(set)
        }}

        // ApplyWithError saves the flagSet for later usage calls, then calls the
        // wrapped {name}Flag.ApplyWithError
        func (f *{name}Flag) ApplyWithError(set *flag.FlagSet) error {{
            f.set = set
            return f.{name}Flag.ApplyWithError(set)
        }}
        """.format(**typedef))


def _fwrite(outfile, text):
    print(textwrap.dedent(text), end=None, file=outfile)


_WRITEFUNCS = {
    'cli': _write_cli_flag_types,
    'altsrc': _write_altsrc_flag_types
}

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