""" Manage the command line options to the form compiler executable """

from argparse import ArgumentParser
from os.path import abspath
from pytools import ImmutableRecord, memoize
import cerberus
import yaml
import pkg_resources
from six.moves import configparser
from six import StringIO
from contextlib import contextmanager


class CodegenOptionsValidator(cerberus.Validator):
    # A validator that accepts the helpstr field in the scheme
    def _validate_helpstr(self, helpstr, field, value):
        """ Describe the option

        The rule's arguments are validated against this schema:
        {'type': 'string'}
        """
        return True


def _load_scheme(form=False):
    resource_package = __name__
    if form:
        resource_path = 'options_form.yaml'
    else:
        resource_path = 'options_global.yaml'
    yaml_stream = pkg_resources.resource_string(resource_package, resource_path)
    try:
        scheme = yaml.safe_load(yaml_stream)
    except Exception as e:
        raise e
    return scheme


class CodegenGlobalOptionsArray(ImmutableRecord):
    """ A collection of form compiler arguments """
    def __init__(self, **kwargs):
        # Set the default values from the yaml scheme as defaults
        scheme = _load_scheme()
        opts = {k: v['default'] for k, v in scheme.items()}
        opts.update(**kwargs)
        ImmutableRecord.__init__(self, **opts)


class CodegenFormOptionsArray(ImmutableRecord):
    """ A collection of form-specific form compiler arguments """
    def __init__(self, **kwargs):
        # Set the default values from the yaml scheme as defaults
        scheme = _load_scheme(form=True)
        opts = {k: v['default'] for k, v in scheme.items()}
        opts.update(**kwargs)
        ImmutableRecord.__init__(self, **opts)


# Until more sophisticated logic is needed, we keep the actual option data in this module
_global_options = CodegenGlobalOptionsArray()
_form_options = {}


def show_options():
    # TODO: This needs to be adjusted to options-validation
    def subopt(arr):
        for k, v in arr.__dict__.items():
            if isinstance(v, CodegenOption) and v.helpstr is not None:
                print("{}\n    {}".format(k, v.helpstr))

    print("This is a summary of options available for the code generation process:\n")
    print("The following options can be given in the [formcompiler] section:")
    subopt(CodegenGlobalOptionsArray)

    print("\nThefollowing options can be given in a form-specific subsection of [formcompiler]:")
    subopt(CodegenFormOptionsArray)


def initialize_options():
    """ Initialize the options from the command line """
    global _global_options
    _global_options = update_options_from_commandline(_global_options)
    _global_options = update_options_from_inifile(_global_options)

    # Validate global options
    scheme_global = _load_scheme()
    validator_global = CodegenOptionsValidator(scheme_global, require_all=True)
    if not validator_global.validate(_global_options.__dict__):
        raise RuntimeError("Global options validation failed: {}".format(validator_global.errors))

    # Validate form options
    scheme_form = _load_scheme(form=True)
    validator_form = CodegenOptionsValidator(scheme_form, require_all=True)
    for form in [i.strip() for i in _global_options.operators.split(",")]:
        if not validator_form.validate(_form_options[form].__dict__):
            raise RuntimeError("Form options validation failed: {}".format(validator_form.errors))


def _scheme_type_to_type(scheme_type):
    assert isinstance(scheme_type, str)
    if scheme_type == 'string':
        return str
    if scheme_type == 'boolean':
        return bool
    if scheme_type == 'integer':
        return int
    if scheme_type == 'float':
        return float


def _transform_type(scheme_type, a):
    if scheme_type == 'boolean':
        return bool(int(a))
    else:
        return _scheme_type_to_type(scheme_type)(a)


def update_options_from_commandline(opt):
    """ Return an options array object with updated values from the commandline """
    assert isinstance(opt, CodegenGlobalOptionsArray)
    parser = ArgumentParser(description="Compile UFL files to PDELab C++ code",
                            epilog="Please report bugs to dominic.kempf@iwr.uni-heidelberg.de",
                            )
    parser.add_argument('--version', action='version', version='%(prog)s 0.1')

    # Load global options scheme
    scheme = _load_scheme()

    # Add all options that have a helpstr to the command line parser
    for k, v in scheme.items():
        if v['helpstr'] is not None:
            cmdopt = "--{}".format(k.replace('_', '-'))
            parser.add_argument(cmdopt, help=v['helpstr'], type=_scheme_type_to_type(v['type']))
    parsedargs = {k: v for k, v in vars(parser.parse_args()).items() if v is not None}
    return opt.copy(**parsedargs)


def update_options_from_inifile(opt):
    """ Return an options array object with updated values from an inifile """
    if opt.ini_file:
        config = configparser.ConfigParser()

        # Read ini file
        try:
            config.read(opt.ini_file)
        except configparser.MissingSectionHeaderError:
            # Config parser doesn't like ini files where without section. For
            # this case we introduce a [root] section on top.
            ini_str = '[root]\n' + open(opt.ini_file, 'r').read()
            ini_fp = StringIO(ini_str)
            config = configparser.RawConfigParser()
            config.readfp(ini_fp)

        # Parse global options
        scheme = _load_scheme()
        options = dict(config.items('formcompiler'))
        for k, v in options.items():
            assert k in scheme
            options[k] = _transform_type(scheme[k]['type'], v)
        opt = opt.copy(**options)

        # Parse form options
        scheme = _load_scheme(form=True)
        for form in [i.strip() for i in opt.operators.split(",")]:
            section = 'formcompiler.{}'.format(form)
            options = {}
            if config.has_section(section):
                options = dict(config.items('formcompiler.{}'.format(form)))
                for k, v in options.items():
                    assert k in scheme
                    options[k] = _transform_type(scheme[k]['type'], v)
            _form_options[form] = CodegenFormOptionsArray(**options)

    return opt


@memoize
def process_global_options(opt):
    """ Make sure that the options have been fully processed """
    opt = expand_architecture_options(opt)

    if opt.overlapping:
        opt = opt.copy(parallel=True)

    return opt


@memoize
def process_form_options(opt, form):
    if opt.sumfact:
        opt = opt.copy(unroll_dimension_loops=True,
                       quadrature_mixins="sumfact",
                       basis_mixins="sumfact",
                       accumulation_mixins="sumfact",
                       )

    if opt.blockstructured:
        opt = opt.copy(accumulation_mixins="blockstructured",
                       quadrature_mixins="blockstructured",
                       basis_mixins="blockstructured"
                       )

    if opt.control:
        opt = opt.copy(accumulation_mixins="control")

    if opt.numerical_jacobian:
        opt = opt.copy(generate_jacobians=False, generate_jacobian_apply=False)

    if opt.form is None:
        opt = opt.copy(form=form)

    if opt.classname is None:
        opt = opt.copy(classname="{}Operator".format(form))

    if opt.filename is None:
        opt = opt.copy(filename="{}_{}_file.hh".format(get_option("target_name"), opt.classname))

    if opt.block_preconditioner_pointdiagonal:
        opt = opt.copy(generate_jacobians=False,
                       basis_mixins="sumfact_pointdiagonal",
                       accumulation_mixins="sumfact_pointdiagonal",
                       )

    if opt.block_preconditioner_diagonal or opt.block_preconditioner_offdiagonal:
        assert opt.numerical_jacobian is False
        opt = opt.copy(generate_residuals=False,
                       generate_jacobians=True,
                       matrix_free=True,
                       )

    if opt.matrix_free:
        opt = opt.copy(generate_jacobian_apply=True)

    return opt


def expand_architecture_options(opt):
    if opt.architecture == "haswell":
        return opt.copy(max_vector_width=256)
    elif opt.architecture == "knl":
        return opt.copy(max_vector_width=512)
    elif opt.architecture == "skylake":
        return opt.copy(max_vector_width=512)
    else:
        raise NotImplementedError("Architecture {} not known!".format(opt.architecture))


def set_option(key, value):
    """Add the key value pair to the options.

    If the key is already in the options dictionary its value will be
    overwritten.  Form compiler arguments will always be set before
    any other options.
    """
    global _global_options
    _global_options = process_global_options(_global_options).copy(**{key: value})


def set_form_option(key, value, form=None):
    if form is None:
        from dune.codegen.generation import get_global_context_value
        form = get_global_context_value("form_identifier", 0)
    if isinstance(form, int):
        form = get_option("operators").split(",")[form].strip()
    _form_options[form] = _form_options[form].copy(**{key: value})


def get_option(key):
    processed_global_opts = process_global_options(_global_options)
    return getattr(processed_global_opts, key)


def get_form_option(key, form=None):
    if form is None:
        from dune.codegen.generation import get_global_context_value
        form = get_global_context_value("form_identifier", 0)
    if isinstance(form, int):
        form = get_option("operators").split(",")[form].strip()
    processed_form_opts = process_form_options(_form_options[form], form)
    return getattr(processed_form_opts, key)


@contextmanager
def option_context(conditional=True, **opts):
    """ A context manager that sets a given option and restores it on exit. """
    # Backup old values and set to new ones
    if conditional:
        backup = {}
        for k, v in opts.items():
            backup[k] = get_option(k)
            set_option(k, v)

    yield

    if conditional:
        # Restore old values
        for k in opts.keys():
            set_option(k, backup[k])


@contextmanager
def form_option_context(conditional=True, **opts):
    """ A context manager that sets a given form option and restores it on exit """
    if conditional:
        form = opts.pop("form", None)

        # Backup old values and set to new ones
        backup = {}
        for k, v in opts.items():
            backup[k] = get_form_option(k, form=form)
            set_form_option(k, v, form=form)

    yield

    # Restore old values
    if conditional:
        for k in opts.keys():
            set_form_option(k, backup[k], form=form)