#
#  Cython - Compilation-wide options and pragma declarations
#

from __future__ import absolute_import

class ShouldBeFromDirective(object):

    known_directives = []

    def __init__(self, options_name, directive_name=None, dissallow=False):
        self.options_name = options_name
        self.directive_name = directive_name or options_name
        self.dissallow = dissallow
        self.known_directives.append(self)

    def __nonzero__(self):
        self._bad_access()

    def __int__(self):
        self._bad_access()

    def _bad_access(self):
        raise RuntimeError(repr(self))

    def __repr__(self):
        return (
        "Illegal access of '%s' from Options module rather than directive '%s'"
        % (self.options_name, self.directive_name))

# Include docstrings.
docstrings = True

# Embed the source code position in the docstrings of functions and classes.
embed_pos_in_docstring = False

# Copy the original source code line by line into C code comments
# in the generated code file to help with understanding the output.
emit_code_comments = True

pre_import = None  # undocumented

# Decref global variables in this module on exit for garbage collection.
# 0: None, 1+: interned objects, 2+: cdef globals, 3+: types objects
# Mostly for reducing noise in Valgrind, only executes at process exit
# (when all memory will be reclaimed anyways).
generate_cleanup_code = False

# Should tp_clear() set object fields to None instead of clearing them to NULL?
clear_to_none = True

# Generate an annotated HTML version of the input source files.
annotate = False

# When annotating source files in HTML, include coverage information from
# this file.
annotate_coverage_xml = None

# This will abort the compilation on the first error occurred rather than trying
# to keep going and printing further error messages.
fast_fail = False

# Make all warnings into errors.
warning_errors = False

# Make unknown names an error.  Python raises a NameError when
# encountering unknown names at runtime, whereas this option makes
# them a compile time error.  If you want full Python compatibility,
# you should disable this option and also 'cache_builtins'.
error_on_unknown_names = True

# Make uninitialized local variable reference a compile time error.
# Python raises UnboundLocalError at runtime, whereas this option makes
# them a compile time error. Note that this option affects only variables
# of "python object" type.
error_on_uninitialized = True

# This will convert statements of the form "for i in range(...)"
# to "for i from ..." when i is a cdef'd integer type, and the direction
# (i.e. sign of step) can be determined.
# WARNING: This may change the semantics if the range causes assignment to
# i to overflow. Specifically, if this option is set, an error will be
# raised before the loop is entered, whereas without this option the loop
# will execute until an overflowing value is encountered.
convert_range = True

# Perform lookups on builtin names only once, at module initialisation
# time.  This will prevent the module from getting imported if a
# builtin name that it uses cannot be found during initialisation.
cache_builtins = True

# Generate branch prediction hints to speed up error handling etc.
gcc_branch_hints = True

# Enable this to allow one to write your_module.foo = ... to overwrite the
# definition if the cpdef function foo, at the cost of an extra dictionary
# lookup on every call.
# If this is false it generates only the Python wrapper and no override check.
lookup_module_cpdef = False

# Whether or not to embed the Python interpreter, for use in making a
# standalone executable or calling from external libraries.
# This will provide a method which initialises the interpreter and
# executes the body of this module.
embed = None

# In previous iterations of Cython, globals() gave the first non-Cython module
# globals in the call stack.  Sage relies on this behavior for variable injection.
old_style_globals = ShouldBeFromDirective('old_style_globals')

# Allows cimporting from a pyx file without a pxd file.
cimport_from_pyx = False

# max # of dims for buffers -- set lower than number of dimensions in numpy, as
# slices are passed by value and involve a lot of copying
buffer_max_dims = 8

# Number of function closure instances to keep in a freelist (0: no freelists)
closure_freelist_size = 8


def get_directive_defaults():
  # To add an item to this list, all accesses should be changed to use the new
  # directive, and the global option itself should be set to an instance of
  # ShouldBeFromDirective.
  for old_option in ShouldBeFromDirective.known_directives:
    value = globals().get(old_option.options_name)
    assert old_option.directive_name in _directive_defaults
    if not isinstance(value, ShouldBeFromDirective):
        if old_option.disallow:
            raise RuntimeError(
                "Option '%s' must be set from directive '%s'" % (
                old_option.option_name, old_option.directive_name))
        else:
            # Warn?
            _directive_defaults[old_option.directive_name] = value
  return _directive_defaults

# Declare compiler directives
_directive_defaults = {
    'boundscheck' : True,
    'nonecheck' : False,
    'initializedcheck' : True,
    'embedsignature' : False,
    'locals' : {},
    'auto_cpdef': False,
    'cdivision': False, # was True before 0.12
    'cdivision_warnings': False,
    'overflowcheck': False,
    'overflowcheck.fold': True,
    'always_allow_keywords': False,
    'allow_none_for_extension_args': True,
    'wraparound' : True,
    'ccomplex' : False, # use C99/C++ for complex types and arith
    'callspec' : "",
    'final' : False,
    'internal' : False,
    'profile': False,
    'no_gc_clear': False,
    'no_gc': False,
    'linetrace': False,
    'emit_code_comments': True,  # copy original source code into C code comments
    'annotation_typing': False,  # read type declarations from Python function annotations
    'infer_types': None,
    'infer_types.verbose': False,
    'autotestdict': True,
    'autotestdict.cdef': False,
    'autotestdict.all': False,
    'language_level': 2,
    'fast_getattr': False, # Undocumented until we come up with a better way to handle this everywhere.
    'py2_import': False, # For backward compatibility of Cython's source code in Py3 source mode
    'c_string_type': 'bytes',
    'c_string_encoding': '',
    'type_version_tag': True,   # enables Py_TPFLAGS_HAVE_VERSION_TAG on extension types
    'unraisable_tracebacks': False,
    'old_style_globals': False,

    # set __file__ and/or __path__ to known source/target path at import time (instead of not having them available)
    'set_initial_path' : None,  # SOURCEFILE or "/full/path/to/module"

    'warn': None,
    'warn.undeclared': False,
    'warn.unreachable': True,
    'warn.maybe_uninitialized': False,
    'warn.unused': False,
    'warn.unused_arg': False,
    'warn.unused_result': False,
    'warn.multiple_declarators': True,

# optimizations
    'optimize.inline_defnode_calls': True,
    'optimize.unpack_method_calls': True,   # increases code size when True
    'optimize.use_switch': True,

# remove unreachable code
    'remove_unreachable': True,

# control flow debug directives
    'control_flow.dot_output': "", # Graphviz output filename
    'control_flow.dot_annotate_defs': False, # Annotate definitions

# test support
    'test_assert_path_exists' : [],
    'test_fail_if_path_exists' : [],

# experimental, subject to change
    'binding': None,
    'freelist': 0,

    'formal_grammar': False,
}

# Extra warning directives
extra_warnings = {
    'warn.maybe_uninitialized': True,
    'warn.unreachable': True,
    'warn.unused': True,
}

def one_of(*args):
    def validate(name, value):
        if value not in args:
            raise ValueError("%s directive must be one of %s, got '%s'" % (
                name, args, value))
        else:
            return value
    return validate


def normalise_encoding_name(option_name, encoding):
    """
    >>> normalise_encoding_name('c_string_encoding', 'ascii')
    'ascii'
    >>> normalise_encoding_name('c_string_encoding', 'AsCIi')
    'ascii'
    >>> normalise_encoding_name('c_string_encoding', 'us-ascii')
    'ascii'
    >>> normalise_encoding_name('c_string_encoding', 'utF8')
    'utf8'
    >>> normalise_encoding_name('c_string_encoding', 'utF-8')
    'utf8'
    >>> normalise_encoding_name('c_string_encoding', 'deFAuLT')
    'default'
    >>> normalise_encoding_name('c_string_encoding', 'default')
    'default'
    >>> normalise_encoding_name('c_string_encoding', 'SeriousLyNoSuch--Encoding')
    'SeriousLyNoSuch--Encoding'
    """
    if not encoding:
        return ''
    if encoding.lower() in ('default', 'ascii', 'utf8'):
        return encoding.lower()
    import codecs
    try:
        decoder = codecs.getdecoder(encoding)
    except LookupError:
        return encoding  # may exists at runtime ...
    for name in ('ascii', 'utf8'):
        if codecs.getdecoder(name) == decoder:
            return name
    return encoding


# Override types possibilities above, if needed
directive_types = {
    'final' : bool,  # final cdef classes and methods
    'internal' : bool,  # cdef class visibility in the module dict
    'infer_types' : bool, # values can be True/None/False
    'binding' : bool,
    'cfunc' : None, # decorators do not take directive value
    'ccall' : None,
    'inline' : None,
    'staticmethod' : None,
    'cclass' : None,
    'returns' : type,
    'set_initial_path': str,
    'freelist': int,
    'c_string_type': one_of('bytes', 'bytearray', 'str', 'unicode'),
    'c_string_encoding': normalise_encoding_name,
}

for key, val in _directive_defaults.items():
    if key not in directive_types:
        directive_types[key] = type(val)

directive_scopes = { # defaults to available everywhere
    # 'module', 'function', 'class', 'with statement'
    'final' : ('cclass', 'function'),
    'inline' : ('function',),
    'staticmethod' : ('function',),  # FIXME: analysis currently lacks more specific function scope
    'no_gc_clear' : ('cclass',),
    'no_gc' : ('cclass',),
    'internal' : ('cclass',),
    'autotestdict' : ('module',),
    'autotestdict.all' : ('module',),
    'autotestdict.cdef' : ('module',),
    'set_initial_path' : ('module',),
    'test_assert_path_exists' : ('function', 'class', 'cclass'),
    'test_fail_if_path_exists' : ('function', 'class', 'cclass'),
    'freelist': ('cclass',),
    'emit_code_comments': ('module',),
    'annotation_typing': ('module',),  # FIXME: analysis currently lacks more specific function scope
    # Avoid scope-specific to/from_py_functions for c_string.
    'c_string_type': ('module',),
    'c_string_encoding': ('module',),
    'type_version_tag': ('module', 'cclass'),
    'language_level': ('module',),
    # globals() could conceivably be controlled at a finer granularity,
    # but that would complicate the implementation
    'old_style_globals': ('module',),
}


def parse_directive_value(name, value, relaxed_bool=False):
    """
    Parses value as an option value for the given name and returns
    the interpreted value. None is returned if the option does not exist.

    >>> print(parse_directive_value('nonexisting', 'asdf asdfd'))
    None
    >>> parse_directive_value('boundscheck', 'True')
    True
    >>> parse_directive_value('boundscheck', 'true')
    Traceback (most recent call last):
       ...
    ValueError: boundscheck directive must be set to True or False, got 'true'

    >>> parse_directive_value('c_string_encoding', 'us-ascii')
    'ascii'
    >>> parse_directive_value('c_string_type', 'str')
    'str'
    >>> parse_directive_value('c_string_type', 'bytes')
    'bytes'
    >>> parse_directive_value('c_string_type', 'bytearray')
    'bytearray'
    >>> parse_directive_value('c_string_type', 'unicode')
    'unicode'
    >>> parse_directive_value('c_string_type', 'unnicode')
    Traceback (most recent call last):
    ValueError: c_string_type directive must be one of ('bytes', 'bytearray', 'str', 'unicode'), got 'unnicode'
    """
    type = directive_types.get(name)
    if not type:
        return None
    orig_value = value
    if type is bool:
        value = str(value)
        if value == 'True':
            return True
        if value == 'False':
            return False
        if relaxed_bool:
            value = value.lower()
            if value in ("true", "yes"):
                return True
            elif value in ("false", "no"):
                return False
        raise ValueError("%s directive must be set to True or False, got '%s'" % (
            name, orig_value))
    elif type is int:
        try:
            return int(value)
        except ValueError:
            raise ValueError("%s directive must be set to an integer, got '%s'" % (
                name, orig_value))
    elif type is str:
        return str(value)
    elif callable(type):
        return type(name, value)
    else:
        assert False


def parse_directive_list(s, relaxed_bool=False, ignore_unknown=False,
                         current_settings=None):
    """
    Parses a comma-separated list of pragma options. Whitespace
    is not considered.

    >>> parse_directive_list('      ')
    {}
    >>> (parse_directive_list('boundscheck=True') ==
    ... {'boundscheck': True})
    True
    >>> parse_directive_list('  asdf')
    Traceback (most recent call last):
       ...
    ValueError: Expected "=" in option "asdf"
    >>> parse_directive_list('boundscheck=hey')
    Traceback (most recent call last):
       ...
    ValueError: boundscheck directive must be set to True or False, got 'hey'
    >>> parse_directive_list('unknown=True')
    Traceback (most recent call last):
       ...
    ValueError: Unknown option: "unknown"
    >>> warnings = parse_directive_list('warn.all=True')
    >>> len(warnings) > 1
    True
    >>> sum(warnings.values()) == len(warnings)  # all true.
    True
    """
    if current_settings is None:
        result = {}
    else:
        result = current_settings
    for item in s.split(','):
        item = item.strip()
        if not item:
            continue
        if not '=' in item:
            raise ValueError('Expected "=" in option "%s"' % item)
        name, value = [s.strip() for s in item.strip().split('=', 1)]
        if name not in _directive_defaults:
            found = False
            if name.endswith('.all'):
                prefix = name[:-3]
                for directive in _directive_defaults:
                    if directive.startswith(prefix):
                        found = True
                        parsed_value = parse_directive_value(directive, value, relaxed_bool=relaxed_bool)
                        result[directive] = parsed_value
            if not found and not ignore_unknown:
                raise ValueError('Unknown option: "%s"' % name)
        else:
            parsed_value = parse_directive_value(name, value, relaxed_bool=relaxed_bool)
            result[name] = parsed_value
    return result
