#!/usr/bin/env python

# Copyright (c) 2006-2019  Andrey Golovigin
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

from __future__ import unicode_literals

import optparse
import sys

import six

from pybtex import __version__, errors
from pybtex.plugin import enumerate_plugin_names, find_plugin
from pybtex.textutils import add_period


def check_plugin(option, option_string, value):
    return find_plugin(option.plugin_group, value)


class PybtexOption(optparse.Option):
    ATTRS = optparse.Option.ATTRS + ['plugin_group']
    TYPES = optparse.Option.TYPES + ('load_plugin',)
    TYPE_CHECKER = dict(optparse.Option.TYPE_CHECKER, load_plugin=check_plugin)
    STANDARD_OPTIONS = {}


make_option = PybtexOption


def make_standard_option(*args, **kwargs):
    option = make_option(*args, **kwargs)
    PybtexOption.STANDARD_OPTIONS[option.dest] = option
    return option


def standard_option(name):
    return PybtexOption.STANDARD_OPTIONS[name]


make_standard_option(
    '--strict', dest='strict',
    help='turn warnings into errors',
    action='callback',
    callback=lambda option, opt, value, parser: errors.set_strict_mode(True)
)

make_standard_option(
    '-f', '--bibliography-format', dest='bib_format',
    help='bibliograpy format (%plugin_choices)',
    type='load_plugin',
    plugin_group='pybtex.database.input',
    metavar='FORMAT',
)

make_standard_option(
    '-b', '--output-backend', dest='output_backend',
    help='output backend (%plugin_choices)',
    type='load_plugin',
    plugin_group='pybtex.backends',
    metavar='BACKEND',
)

make_standard_option(
    '--min-crossrefs',
    type='int', dest='min_crossrefs',
    help='include item after NUMBER crossrefs; default 2',
    metavar='NUMBER',
)

make_standard_option(
    '--keyless-bibtex-entries',
    action='store_true', dest='keyless_entries',
    help='allow BibTeX entries without keys and generate unnamed-<number> keys for them'
)

make_standard_option(
    '-s', '--style',
    type='string', dest='style', help='bibliography formatting style',
)

make_standard_option(
    '--label-style', dest='label_style',
    help='label formatting style (%plugin_choices)',
    type='load_plugin',
    plugin_group='pybtex.style.labels',
    metavar='STYLE',
)

make_standard_option(
    '--name-style', dest='name_style',
    help='name formatting style (%plugin_choices)',
    type='load_plugin',
    plugin_group='pybtex.style.names',
    metavar='STYLE',
)

make_standard_option(
    '--sorting-style', dest='sorting_style',
    help='sorting style (%plugin_choices)',
    type='load_plugin',
    plugin_group='pybtex.style.sorting',
    metavar='STYLE',
)

make_standard_option(
    '--abbreviate-names',
    action='store_true', dest='abbreviate_names',
    help='use abbreviated name formatting style',
)

make_standard_option(
    '-e', '--encoding',
    action='store', type='string', dest='encoding',
    help='default encoding',
    metavar='ENCODING',
)

make_standard_option(
    '--input-encoding',
    action='store', type='string', dest='input_encoding',
    metavar='ENCODING',
)

make_standard_option(
    '--output-encoding',
    action='store', type='string', dest='output_encoding',
    metavar='ENCODING',
)


BaseHelpFormatter = optparse.IndentedHelpFormatter


class PybtexHelpFormatter(BaseHelpFormatter):
    def get_plugin_choices(self, plugin_group):
        return ', '.join(sorted(enumerate_plugin_names(plugin_group)))

    def expand_default(self, option):
        result = BaseHelpFormatter.expand_default(self, option)
        if option.plugin_group:
            plugin_choices = self.get_plugin_choices(option.plugin_group)
            result = result.replace('%plugin_choices', plugin_choices)
        return result


class CommandLine(object):
    options = ()
    option_defaults = None
    legacy_options = ()
    prog = None
    args = None
    description = ''
    num_args = 0

    def __init__(self):
        self.opt_parser = self.make_option_parser()

    def __call__(self):
        from pybtex.exceptions import PybtexError
        try:
            self.main()
        except PybtexError as error:
            errors.print_error(error)
            sys.exit(1)

    def make_option_parser(self):
        opt_parser = optparse.OptionParser(
            prog=self.prog,
            option_class=PybtexOption,
            formatter=PybtexHelpFormatter(),
            usage='%prog ' + self.args,
            description=add_period(self.description.capitalize()),
            version='%%prog-%s' % __version__
        )
        for option_group, option_list in self.options:
            if option_group is None:
                container = opt_parser
            else:
                container = optparse.OptionGroup(opt_parser, option_group)
                opt_parser.add_option_group(container)
            for option in option_list:
                container.add_option(option)

        if self.option_defaults:
            opt_parser.set_defaults(**self.option_defaults)

        return opt_parser

    def run(self, options, args):
        raise NotImplementedError

    def recognize_legacy_optons(self, args):
        """Grok some legacy long options starting with a single `-'."""
        return [self._replace_legacy_option(arg) for arg in args]

    def _replace_legacy_option(self, arg):
        # sys.argv contains byte strings in Python 2 and unicode strings in Python 3

        try:
            # all legacy options are ASCII-only
            unicode_arg = arg if isinstance(arg, six.text_type) else arg.decode('ASCII')
        except UnicodeDecodeError:
            return arg

        if unicode_arg.split('=', 1)[0] in self.legacy_options:
            return type(arg)('-') + arg
        else:
            return arg

    def _extract_kwargs(self, options):
        return dict(
            (option.dest, getattr(options, option.dest))
            for option_group, option_list in self.options
            for option in option_list
        )

    def main(self):
        errors.set_strict_mode(False)
        argv = self.recognize_legacy_optons(sys.argv[1:])
        options, args = self.opt_parser.parse_args(argv)
        if len(args) != self.num_args:
            self.opt_parser.print_help()
            sys.exit(1)
        kwargs = self._extract_kwargs(options)
        self.run(*args, **kwargs)
        sys.exit(errors.error_code)
