#     Copyright 2018, Kay Hayen, mailto:kay.hayen@gmail.com
#
#     Part of "Nuitka", an optimizing Python compiler that is compatible and
#     integrates with CPython, but also works on its own.
#
#     Licensed under the Apache License, Version 2.0 (the "License");
#     you may not use this file except in compliance with the License.
#     You may obtain a copy of the License at
#
#        http://www.apache.org/licenses/LICENSE-2.0
#
#     Unless required by applicable law or agreed to in writing, software
#     distributed under the License is distributed on an "AS IS" BASIS,
#     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#     See the License for the specific language governing permissions and
#     limitations under the License.
#
""" Code generation for locals and globals handling.

This also includes writing back to locals for exec statements.
"""

from nuitka.nodes.shapes.BuiltinTypeShapes import ShapeTypeDict

from .CodeHelpers import (
    decideConversionCheckNeeded,
    withObjectCodeTemporaryAssignment
)
from .Emission import SourceCodeCollector
from .ErrorCodes import getErrorExitBoolCode
from .Indentation import indented
from .PythonAPICodes import generateCAPIObjectCode
from .templates.CodeTemplatesVariables import (
    template_set_locals_dict_value,
    template_set_locals_mapping_value,
    template_update_locals_dict_value,
    template_update_locals_mapping_value
)
from .VariableCodes import (
    getLocalVariableDeclaration,
    getVariableReferenceCode
)
from .VariableDeclarations import VariableDeclaration


def generateBuiltinLocalsRefCode(to_name, expression, emit, context):
    locals_scope = expression.getLocalsScope()

    locals_declaration = context.addLocalsDictName(locals_scope.getCodeName())

    with withObjectCodeTemporaryAssignment(to_name, "locals_ref_value", expression, emit, context) \
      as value_name:

        emit(
            "%s = %s;" % (
                value_name,
                locals_declaration,
            )
        )


def generateBuiltinLocalsCode(to_name, expression, emit, context):
    provider = expression.getParentVariableProvider()

    variable_traces = expression.getVariableTraces()
    updated   = expression.isExpressionBuiltinLocalsUpdated()

    # Locals is sorted of course.
    def _sorted(variables):
        locals_owner = context.getOwner()

        if locals_owner.isExpressionOutlineBody():
            locals_owner = locals_owner.getParentVariableProvider()

        variable_order = locals_owner.getProvidedVariableOrder()

        return sorted(
            variables,
            key = lambda variable_desc: variable_order.index(variable_desc[0].getName()),
        )

    # Optimization will have made this "globals", and it wouldn't be
    # about local variables at all.
    assert not provider.isCompiledPythonModule(), provider

    with withObjectCodeTemporaryAssignment(to_name, "locals_ref_value", expression, emit, context) \
      as value_name:

        if updated:
            locals_scope = expression.getLocalsScope()

            locals_declaration = context.addLocalsDictName(locals_scope.getCodeName())
            is_dict = locals_scope.getTypeShape() is ShapeTypeDict
            # For Python3 it may really not be a dictionary.

            # TODO: Creation is not needed for classes.
            emit(
                """\
if (%(locals_dict)s == NULL) %(locals_dict)s = PyDict_New();
%(to_name)s = %(locals_dict)s;
Py_INCREF( %(to_name)s );""" % {
                    "to_name"     : value_name,
                    "locals_dict" : locals_declaration,
                }
            )
            context.addCleanupTempName(value_name)

            initial = False
        else:
            emit(
                "%s = PyDict_New();" % (
                    to_name,
                )
            )

            context.addCleanupTempName(value_name)

            initial = True
            is_dict = True

        for local_var, variable_trace in _sorted(variable_traces):
            _getVariableDictUpdateCode(
                target_name    = value_name,
                variable       = local_var,
                variable_trace = variable_trace,
                is_dict        = is_dict,
                initial        = initial,
                emit           = emit,
                context        = context
            )


def generateBuiltinGlobalsCode(to_name, expression, emit, context):
    with withObjectCodeTemporaryAssignment(to_name, "globals_value", expression, emit, context) \
      as value_name:
        emit(
            "%(to_name)s = (PyObject *)moduledict_%(module_identifier)s;" % {
                "to_name"           : value_name,
                "module_identifier" : context.getModuleCodeName()
            },
        )


def _getLocalVariableList(provider):
    if provider.isExpressionFunctionBody():
        include_closure = not provider.isUnoptimized()
    elif provider.isExpressionClassBody():
        include_closure = False
    else:
        include_closure = True

    return [
        variable
        for variable in
        provider.getVariables()
        if not variable.isModuleVariable()
        if (include_closure or variable.getOwner() is provider)
    ]


def _getVariableDictUpdateCode(target_name, variable, variable_trace, initial,
                               is_dict, emit, context):
    # TODO: Variable could known to be set here, get a hand at that
    # information.

    variable_declaration = getLocalVariableDeclaration(context, variable, variable_trace)

    variable_c_type = variable_declaration.getCType()

    test_code = variable_c_type.getLocalVariableInitTestCode(variable_declaration, False)

    access_code = SourceCodeCollector()

    getVariableReferenceCode(
        to_name          = VariableDeclaration("PyObject *", "value", None, None),
        variable         = variable,
        variable_trace   = variable_trace,
        needs_check      = False,
        conversion_check = True,
        emit             = access_code,
        context          = context
    )

    if is_dict:
        if initial:
            template = template_set_locals_dict_value
        else:
            template = template_update_locals_dict_value

        emit(
             template % {
                "dict_name"   : target_name,
                "var_name"    : context.getConstantCode(
                    constant = variable.getName()
                ),
                "test_code"   : test_code,
                "access_code" : indented(access_code.codes)
            }
        )
    else:
        if initial:
            template = template_set_locals_mapping_value
        else:
            template = template_update_locals_mapping_value

        res_name = context.getBoolResName()

        emit(
            template % {
                "mapping_name" : target_name,
                "var_name"     : context.getConstantCode(
                    constant = variable.getName()
                ),
                "test_code"    : test_code,
                "access_code"  : access_code,
                "tmp_name"     : res_name
            }
        )

        getErrorExitBoolCode(
            condition = "%s == false" % res_name,
            emit      = emit,
            context   = context
        )


def generateBuiltinDir1Code(to_name, expression, emit, context):
    generateCAPIObjectCode(
        to_name          = to_name,
        capi             = "PyObject_Dir",
        arg_desc         = (
            ("dir_arg", expression.getValue()),
        ),
        may_raise        = expression.mayRaiseException(BaseException),
        conversion_check = decideConversionCheckNeeded(to_name, expression),
        source_ref       = expression.getCompatibleSourceReference(),
        emit             = emit,
        context          = context
    )


def generateBuiltinVarsCode(to_name, expression, emit, context):
    generateCAPIObjectCode(
        to_name          = to_name,
        capi             = "LOOKUP_VARS",
        arg_desc         = (
            ("vars_arg", expression.getSource()),
        ),
        may_raise        = expression.mayRaiseException(BaseException),
        conversion_check = decideConversionCheckNeeded(to_name, expression),
        source_ref       = expression.getCompatibleSourceReference(),
        emit             = emit,
        context          = context
    )
