#
#   Cython - Command Line Parsing
#


import os
from argparse import ArgumentParser, Action, SUPPRESS, RawDescriptionHelpFormatter
from . import Options

TOOL_DESCRIPTION = """\
Cython (https://cython.org/) is a compiler for code written in the \
Cython language.  Cython is based on Pyrex by Greg Ewing.
"""

EPILOG = """
Environment variables:
  CYTHON_CACHE_DIR: the base directory containing Cython's caches.
"""


class ParseDirectivesAction(Action):
    def __call__(self, parser, namespace, values, option_string=None):
        old_directives = dict(getattr(namespace, self.dest,
                                      Options.get_directive_defaults()))
        directives = Options.parse_directive_list(
            values, relaxed_bool=True, current_settings=old_directives)
        setattr(namespace, self.dest, directives)


class ParseOptionsAction(Action):
    def __call__(self, parser, namespace, values, option_string=None):
        options = dict(getattr(namespace, self.dest, {}))
        for opt in values.split(','):
            if '=' in opt:
                n, v = opt.split('=', 1)
                v = v.lower() not in ('false', 'f', '0', 'no')
            else:
                n, v = opt, True
            options[n] = v
        setattr(namespace, self.dest, options)


class ParseCommaListAction(Action):
    def __call__(self, parser, namespace, values, option_string=None):
        options = list(getattr(namespace, self.dest, []))

        seen = set(options)
        for opt in values.split(','):
            if opt not in seen:
                seen.add(opt)
                options.append(opt)

        setattr(namespace, self.dest, options)


class ParseCompileTimeEnvAction(Action):
    def __call__(self, parser, namespace, values, option_string=None):
        old_env = dict(getattr(namespace, self.dest, {}))
        new_env = Options.parse_compile_time_env(values, current_settings=old_env)
        setattr(namespace, self.dest, new_env)


class ActivateAllWarningsAction(Action):
    def __call__(self, parser, namespace, values, option_string=None):
        directives = getattr(namespace, 'compiler_directives', {})
        directives.update(Options.extra_warnings)
        namespace.compiler_directives = directives


class SetLenientAction(Action):
    def __call__(self, parser, namespace, values, option_string=None):
        namespace.error_on_unknown_names = False
        namespace.error_on_uninitialized = False


class SetGDBDebugAction(Action):
    def __call__(self, parser, namespace, values, option_string=None):
        namespace.gdb_debug = True
        namespace.output_dir = os.curdir


class SetGDBDebugOutputAction(Action):
    def __call__(self, parser, namespace, values, option_string=None):
        namespace.gdb_debug = True
        namespace.output_dir = values


class SetAnnotateCoverageAction(Action):
    def __call__(self, parser, namespace, values, option_string=None):
        namespace.annotate = True
        namespace.annotate_coverage_xml = values


class CythonSharedModuleArgumentParser(ArgumentParser):
    # Build the epilog lazily at request as it may take time to produce.
    @property
    def epilog(self):
        import textwrap
        from ..Build import SharedModule
        return (
            '\nValid feature names for the shared module:\n  FEATURES: ' +
            '\n'.join(textwrap.wrap(
                ' , '.join(SharedModule.list_of_features()),
                subsequent_indent=" "*12,
            )) + '\n' +
            EPILOG
        )

    @epilog.setter
    def epilog(self, value):
        pass


def add_compile_arguments(parser, include_sources=True, for_shared=False):
    if not for_shared:
        parser.add_argument("-V", "--version", dest='show_version', action='store_const', const=1,
                          help='Display version number of cython compiler')
    parser.add_argument("-l", "--create-listing", dest='use_listing_file', action='store_const', const=1,
                      help='Write error messages to a listing file')
    parser.add_argument("-I", "--include-dir", dest='include_path', action='append',
                      help='Search for include files in named directory '
                           '(multiple include directories are allowed).')
    parser.add_argument("-o", "--output-file", dest='output_file', action='store', type=str,
                      help='Specify name of generated C file')
    parser.add_argument("-t", "--timestamps", dest='timestamps', action='store_const', const=1,
                      help='Only compile newer source files')
    parser.add_argument("-f", "--force", dest='timestamps', action='store_const', const=0,
                      help='Compile all source files (overrides implied -t)')
    parser.add_argument("-v", "--verbose", dest='verbose', action='count',
                      help='Be verbose, print file names on multiple compilation')
    if not for_shared:
        parser.add_argument("-p", "--embed-positions", dest='embed_pos_in_docstring', action='store_const', const=1,
                          help='If specified, the positions in Cython files of each '
                               'function definition is embedded in its docstring.')
    parser.add_argument("--cleanup", dest='generate_cleanup_code', action='store', type=int,
                      help='Release interned objects on python exit, for memory debugging. '
                           'Level indicates aggressiveness, default 0 releases nothing.')
    parser.add_argument("--cache", dest='cache', action='store_true',
                      help='Enables Cython compilation cache.')
    parser.add_argument("-w", "--working", dest='working_path', action='store', type=str,
                      help='Sets the working directory for Cython (the directory modules are searched from)')
    parser.add_argument("--gdb", action=SetGDBDebugAction, nargs=0,
                      help='Output debug information for cygdb')
    parser.add_argument("--gdb-outdir", action=SetGDBDebugOutputAction, type=str,
                      help='Specify gdb debug information output directory. Implies --gdb.')
    parser.add_argument("-D", "--no-docstrings", dest='docstrings', action='store_false',
                      help='Strip docstrings from the compiled module.')
    parser.add_argument('-a', '--annotate', action='store_const', const='default', dest='annotate',
                      help='Produce a colorized HTML version of the source.')
    parser.add_argument('--annotate-fullc', action='store_const', const='fullc', dest='annotate',
                      help='Produce a colorized HTML version of the source '
                           'which includes entire generated C/C++-code.')
    parser.add_argument("--annotate-coverage", dest='annotate_coverage_xml', action=SetAnnotateCoverageAction, type=str,
                      help='Annotate and include coverage information from cov.xml.')
    parser.add_argument("--line-directives", dest='emit_linenums', action='store_true',
                      help='Produce #line directives pointing to the .pyx source')
    parser.add_argument("-+", "--cplus", dest='cplus', action='store_const', const=1,
                      help='Output a C++ rather than C file.')
    if not for_shared:
        parser.add_argument('--embed', action='store_const', const='main',
                          help='Generate a main() function that embeds the Python interpreter. '
                               'Pass --embed=<method_name> for a name other than main().')
        parser.add_argument('--embed-modules', action='store', type=comma_list,
                          help='')
        parser.add_argument('-2', dest='language_level', action='store_const', const=2,
                          help='Compile based on Python-2 syntax and code semantics.')
        parser.add_argument('-3', dest='language_level', action='store_const', const=3,
                          help='Compile based on Python-3 syntax and code semantics.')
        parser.add_argument('--3str', dest='language_level', action='store_const', const='3',
                          help='Compile based on Python-3 syntax and code semantics (same as -3 since Cython 3.1).')
        parser.add_argument("--lenient", action=SetLenientAction, nargs=0,
                          help='Change some compile time errors to runtime errors to '
                               'improve Python compatibility')
        parser.add_argument("--capi-reexport-cincludes", dest='capi_reexport_cincludes', action='store_true',
                          help='Add cincluded headers to any auto-generated header files.')
    parser.add_argument("--fast-fail", dest='fast_fail', action='store_true',
                      help='Abort the compilation on the first error')
    parser.add_argument("-Werror", "--warning-errors", dest='warning_errors', action='store_true',
                      help='Make all warnings into errors')
    parser.add_argument("-Wextra", "--warning-extra", action=ActivateAllWarningsAction, nargs=0,
                      help='Enable extra warnings')

    parser.add_argument('-X', '--directive', metavar='NAME=VALUE,...',
                      dest='compiler_directives', type=str,
                      action=ParseDirectivesAction,
                      help='Overrides a compiler directive')
    if not for_shared:
        parser.add_argument('-E', '--compile-time-env', metavar='NAME=VALUE,...',
                          dest='compile_time_env', type=str,
                          action=ParseCompileTimeEnvAction,
                          help='Provides compile time env like DEF would do.')
    parser.add_argument("--module-name",
                      dest='module_name', type=str, action='store',
                      help='Fully qualified module name. If not given, is '
                           'deduced from the import path if source file is in '
                           'a package, or equals the filename otherwise.')
    parser.add_argument('-M', '--depfile', action='store_true', help='produce depfiles for the sources')

    # Shared module setup.
    parser.add_argument("--shared", dest='shared_utility_qualified_name', action='store', type=str,
                        help='Imports utility code from shared module specified by fully qualified module name.')

    if include_sources:
        parser.add_argument('sources', nargs='*', default=[])

    # TODO: add help
    parser.add_argument("-z", "--pre-import", dest='pre_import', action='store', type=str, help=SUPPRESS)
    parser.add_argument("--convert-range", dest='convert_range', action='store_true', help=SUPPRESS)
    parser.add_argument("--no-c-in-traceback", dest='c_line_in_traceback', action='store_false', help=SUPPRESS)
    parser.add_argument("--cimport-from-pyx", dest='cimport_from_pyx', action='store_true', help=SUPPRESS)
    parser.add_argument("--old-style-globals", dest='old_style_globals', action='store_true', help=SUPPRESS)

    # debug stuff:
    from . import DebugFlags
    for name in vars(DebugFlags):
        if name.startswith("debug"):
            option_name = name.replace('_', '-')
            parser.add_argument("--" + option_name, action='store_true', help=SUPPRESS)


def create_cython_argparser():
    parser = ArgumentParser(
        description=TOOL_DESCRIPTION,
        argument_default=SUPPRESS,
        formatter_class=RawDescriptionHelpFormatter,
        epilog=EPILOG,
    )
    add_compile_arguments(parser, include_sources=True, for_shared=False)

    parser.add_argument("--generate-shared", dest='shared_c_file_path', action='store', type=str,
                        help='Generates shared module with specified name.')

    return parser


def create_cython_subcommand_parser():
    parser = CythonSharedModuleArgumentParser(
        description=TOOL_DESCRIPTION,
        argument_default=SUPPRESS,
        formatter_class=RawDescriptionHelpFormatter,
    )
    parser.add_argument("-V", "--version", dest='show_version', action='store_const', const=1,
                        help='Display version number of cython compiler')

    subparsers = parser.add_subparsers(dest='command', help='Subcommands')

    # Generate-shared subcommand
    generate_parser = subparsers.add_parser(
        'generate-shared',
        help='Generate shared module C/C++ file',
        argument_default=SUPPRESS,
        formatter_class=RawDescriptionHelpFormatter,
    )
    generate_parser.add_argument('shared_c_file_path', metavar='OUTPUT_FILE', type=str,
                                 help='Path to the generated shared C/C++ file.')
    generate_parser.add_argument("--only", dest='shared_utility_features_enabled',
                                 action=ParseCommaListAction, type=str, metavar='FEATURES',
                                 help='Comma separate list of features to move to the shared module exclusively.')
    generate_parser.add_argument("--exclude", dest='shared_utility_features_disabled',
                                 action=ParseCommaListAction, type=str, metavar='FEATURES',
                                 help='Comma separate list of features to exclude from the shared module.')
    add_compile_arguments(generate_parser, include_sources=False, for_shared=True)

    return parser


def comma_list(string):
    return string.split(',')


def parse_command_line_raw(parser, args):
    # special handling for --embed and --embed=xxxx as they aren't correctly parsed
    def filter_out_embed_options(args):
        with_embed, without_embed = [], []
        for x in args:
            if x == '--embed' or x.startswith('--embed='):
                with_embed.append(x)
            else:
                without_embed.append(x)
        return with_embed, without_embed

    with_embed, args_without_embed = filter_out_embed_options(args)

    arguments, unknown = parser.parse_known_args(args_without_embed)

    if getattr(arguments, 'command', None) == 'generate-shared':
        if with_embed:
            parser.error("unknown option " + with_embed[0].split('=', 1)[0])
        shared_c_path = getattr(arguments, 'shared_c_file_path', None)
        if shared_c_path and shared_c_path.startswith('-'):
            parser.error("unknown option " + shared_c_path)

    try:
        sources = arguments.sources
        del arguments.sources
    except AttributeError:
        sources = []

    # unknown can be either debug, embed or input files or really unknown
    for option in unknown:
        if option.startswith('-'):
            parser.error("unknown option " + option)
        else:
            sources.append(option)

    # embed-stuff must be handled extra:
    for x in with_embed:
        if x == '--embed':
            name = 'main'  # default value
        else:
            name = x[len('--embed='):]
        setattr(arguments, 'embed', name)

    return arguments, sources


def parse_command_line(args):
    is_subcommand = args and args[0] == "generate-shared"
    if is_subcommand:
        parser = create_cython_subcommand_parser()
    else:
        parser = create_cython_argparser()

    arguments, sources = parse_command_line_raw(parser, args)

    command = getattr(arguments, 'command', None)
    if command:
        del arguments.command

    work_dir = getattr(arguments, 'working_path', '')
    for source in sources:
        if work_dir and not os.path.isabs(source):
            source = os.path.join(work_dir, source)
        if not os.path.exists(source):
            import errno
            raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), source)

    options = Options.CompilationOptions(Options.default_options)
    for name, value in vars(arguments).items():
        if name.startswith('debug'):
            from . import DebugFlags
            if name in dir(DebugFlags):
                setattr(DebugFlags, name, value)
            else:
                parser.error("Unknown debug flag: %s\n" % name)
        elif hasattr(Options, name):
            setattr(Options, name, value)
        else:
            setattr(options, name, value)

    if options.use_listing_file and len(sources) > 1:
        parser.error("cython: Only one source file allowed when using -o\n")

    if options.shared_c_file_path:
        if len(sources) > 0:
            parser.error("cython: Source file not allowed when using generate-shared\n")
    elif len(sources) == 0 and not options.show_version:
        parser.error("cython: Need at least one source file\n")

    if options.shared_utility_features_disabled or options.shared_utility_features_enabled:
        if not options.shared_c_file_path:
            parser.error("cython: --only and --exclude require generate-shared")

    if Options.embed and len(sources) > 1:
        parser.error("cython: Only one source file allowed when using --embed\n")

    if options.module_name:
        if options.timestamps:
            parser.error("cython: Cannot use --module-name with --timestamps\n")
        if len(sources) > 1:
            parser.error("cython: Only one source file allowed when using --module-name\n")

    return options, sources
