#
# 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.

import importlib
import logging
import os
import six

from toscaparser.functions import GetAttribute
from toscaparser.functions import GetInput
from toscaparser.functions import GetProperty
from toscaparser.relationship_template import RelationshipTemplate
from translator.common.exception import ToscaClassAttributeError
from translator.common.exception import ToscaClassImportError
from translator.common.exception import ToscaModImportError
from translator.conf.config import ConfigProvider as translatorConfig
from translator.hot.syntax.hot_resource import HotResource
from translator.hot.tosca.tosca_block_storage_attachment import (
    ToscaBlockStorageAttachment
    )

###########################
# Module utility Functions
# for dynamic class loading
###########################


def _generate_type_map():
    '''Generate TOSCA translation types map.

    Load user defined classes from location path specified in conf file.
    Base classes are located within the tosca directory.

    '''

    # Base types directory
    BASE_PATH = 'translator/hot/tosca'

    # Custom types directory defined in conf file
    custom_path = translatorConfig.get_value('DEFAULT',
                                             'custom_types_location')

    # First need to load the parent module, for example 'contrib.hot',
    # for all of the dynamically loaded classes.
    classes = []
    _load_classes((BASE_PATH, custom_path), classes)
    try:
        types_map = {clazz.toscatype: clazz for clazz in classes}
    except AttributeError as e:
        raise ToscaClassAttributeError(message=e.message)

    return types_map


def _load_classes(locations, classes):
    '''Dynamically load all the classes from the given locations.'''

    for cls_path in locations:
        # Use the absolute path of the class path
        abs_path = os.path.dirname(os.path.abspath(__file__))
        abs_path = abs_path.replace('translator/hot', cls_path)

        # Grab all the tosca type module files in the given path
        mod_files = [f for f in os.listdir(abs_path) if f.endswith('.py')
                     and not f.startswith('__init__')
                     and f.startswith('tosca_')]

        # For each module, pick out the target translation class
        for f in mod_files:
            # NOTE: For some reason the existing code does not use the map to
            # instantiate ToscaBlockStorageAttachment. Don't add it to the map
            # here until the dependent code is fixed to use the map.
            if f == 'tosca_block_storage_attachment.py':
                continue

            mod_name = cls_path + '/' + f.strip('.py')
            mod_name = mod_name.replace('/', '.')
            try:
                mod = importlib.import_module(mod_name)
                target_name = getattr(mod, 'TARGET_CLASS_NAME')
                clazz = getattr(mod, target_name)
                classes.append(clazz)
            except ImportError:
                raise ToscaModImportError(mod_name=mod_name)
            except AttributeError:
                if target_name:
                    raise ToscaClassImportError(name=target_name,
                                                mod_name=mod_name)
                else:
                    # TARGET_CLASS_NAME is not defined in module.
                    # Re-raise the exception
                    raise

##################
# Module constants
##################

SECTIONS = (TYPE, PROPERTIES, REQUIREMENTS, INTERFACES, LIFECYCLE, INPUT) = \
           ('type', 'properties', 'requirements',
            'interfaces', 'lifecycle', 'input')

# TODO(anyone):  the following requirement names should not be hard-coded
# in the translator.  Since they are basically arbitrary names, we have to get
# them from TOSCA type definitions.
# To be fixed with the blueprint:
# https://blueprints.launchpad.net/heat-translator/+spec/tosca-custom-types
REQUIRES = (CONTAINER, DEPENDENCY, DATABASE_ENDPOINT, CONNECTION, HOST) = \
           ('container', 'dependency', 'database_endpoint',
            'connection', 'host')

INTERFACES_STATE = (CREATE, START, CONFIGURE, START, DELETE) = \
                   ('create', 'stop', 'configure', 'start', 'delete')


TOSCA_TO_HOT_REQUIRES = {'container': 'server', 'host': 'server',
                         'dependency': 'depends_on', "connects": 'depends_on'}

TOSCA_TO_HOT_PROPERTIES = {'properties': 'input'}
log = logging.getLogger('heat-translator')

TOSCA_TO_HOT_TYPE = _generate_type_map()


class TranslateNodeTemplates(object):
    '''Translate TOSCA NodeTemplates to Heat Resources.'''

    def __init__(self, tosca, hot_template):
        self.tosca = tosca
        self.nodetemplates = self.tosca.nodetemplates
        self.hot_template = hot_template
        # list of all HOT resources generated
        self.hot_resources = []
        # mapping between TOSCA nodetemplate and HOT resource
        self.hot_lookup = {}

    def translate(self):
        return self._translate_nodetemplates()

    def _recursive_handle_properties(self, resource):
        '''Recursively handle the properties of the depends_on_nodes nodes.'''
        # Use of hashtable (dict) here should be faster?
        if resource in self.processed_resources:
            return
        self.processed_resources.append(resource)
        for depend_on in resource.depends_on_nodes:
            self._recursive_handle_properties(depend_on)

        resource.handle_properties()

    def _translate_nodetemplates(self):

        suffix = 0
        # Copy the TOSCA graph: nodetemplate
        for node in self.nodetemplates:
            hot_node = TOSCA_TO_HOT_TYPE[node.type](node)
            self.hot_resources.append(hot_node)
            self.hot_lookup[node] = hot_node

            # BlockStorage Attachment is a special case,
            # which doesn't match to Heat Resources 1 to 1.
            if node.type == "tosca.nodes.Compute":
                volume_name = None
                requirements = node.requirements
                if requirements:
                    # Find the name of associated BlockStorage node
                    for requires in requirements:
                        for value in requires.values():
                            if isinstance(value, dict):
                                for node_name in value.values():
                                    for n in self.nodetemplates:
                                        if n.name == node_name:
                                            volume_name = node_name
                                            break
                            else:  # unreachable code !
                                for n in self.nodetemplates:
                                    if n.name == node_name:
                                        volume_name = node_name
                                        break

                    suffix = suffix + 1
                    attachment_node = self._get_attachment_node(node,
                                                                suffix,
                                                                volume_name)
                    if attachment_node:
                        self.hot_resources.append(attachment_node)

        # Handle life cycle operations: this may expand each node
        # into multiple HOT resources and may change their name
        lifecycle_resources = []
        for resource in self.hot_resources:
            expanded = resource.handle_life_cycle()
            if expanded:
                lifecycle_resources += expanded
        self.hot_resources += lifecycle_resources

        # Handle configuration from ConnectsTo relationship in the TOSCA node:
        # this will generate multiple HOT resources, set of 2 for each
        # configuration
        connectsto_resources = []
        for node in self.nodetemplates:
            for requirement in node.requirements:
                for endpoint, details in six.iteritems(requirement):
                    relation = None
                    if isinstance(details, dict):
                        target = details.get('node')
                        relation = details.get('relationship')
                    else:
                        target = details
                    if (target and relation and
                            not isinstance(relation, six.string_types)):
                        interfaces = relation.get('interfaces')
                        connectsto_resources += \
                            self._create_connect_configs(node,
                                                         target,
                                                         interfaces)
        self.hot_resources += connectsto_resources

        # Copy the initial dependencies based on the relationship in
        # the TOSCA template
        for node in self.nodetemplates:
            for node_depend in node.related_nodes:
                # if the source of dependency is a server and the
                # relationship type is 'tosca.relationships.HostedOn',
                # add dependency as properties.server
                if node_depend.type == 'tosca.nodes.Compute' and \
                   node.related[node_depend].type == \
                   node.type_definition.HOSTEDON:
                    self.hot_lookup[node].properties['server'] = \
                        {'get_resource': self.hot_lookup[node_depend].name}
                # for all others, add dependency as depends_on
                else:
                    self.hot_lookup[node].depends_on.append(
                        self.hot_lookup[node_depend].top_of_chain())

                self.hot_lookup[node].depends_on_nodes.append(
                    self.hot_lookup[node_depend].top_of_chain())

        # handle hosting relationship
        for resource in self.hot_resources:
            resource.handle_hosting()

        # handle built-in properties of HOT resources
        # if a resource depends on other resources,
        # their properties need to be handled first.
        # Use recursion to handle the properties of the
        # dependent nodes in correct order
        self.processed_resources = []
        for resource in self.hot_resources:
            self._recursive_handle_properties(resource)

        # handle resources that need to expand to more than one HOT resource
        expansion_resources = []
        for resource in self.hot_resources:
            expanded = resource.handle_expansion()
            if expanded:
                expansion_resources += expanded
        self.hot_resources += expansion_resources

        # Resolve function calls:  GetProperty, GetAttribute, GetInput
        # at this point, all the HOT resources should have been created
        # in the graph.
        for resource in self.hot_resources:
            # traverse the reference chain to get the actual value
            inputs = resource.properties.get('input_values')
            if inputs:
                for name, value in six.iteritems(inputs):
                    if isinstance(value, GetAttribute):
                        # for the attribute
                        # get the proper target type to perform the translation
                        args = value.result()
                        target = args[0]
                        hot_target = self.find_hot_resource(target)

                        inputs[name] = hot_target.get_hot_attribute(args[1],
                                                                    args)
                    else:
                        if isinstance(value, GetProperty) or \
                                isinstance(value, GetInput):
                            inputs[name] = value.result()

        return self.hot_resources

    def _get_attachment_node(self, node, suffix, volume_name):
        attach = False
        ntpl = self.nodetemplates
        for key, value in node.relationships.items():
            if key.is_derived_from('tosca.relationships.AttachesTo'):
                if value.is_derived_from('tosca.nodes.BlockStorage'):
                    attach = True
            if attach:
                relationship_tpl = None
                for req in node.requirements:
                    for key, val in req.items():
                        attach = val
                        relship = val.get('relationship')
                        for rkey, rval in val.items():
                            if relship and isinstance(relship, dict):
                                for rkey, rval in relship.items():
                                    if rkey == 'type':
                                        relationship_tpl = val
                                        attach = rval
                                    elif rkey == 'template':
                                        rel_tpl_list = \
                                            (self.tosca.topology_template.
                                             _tpl_relationship_templates())
                                        relationship_tpl = rel_tpl_list[rval]
                                        attach = rval
                                    else:
                                        continue
                            elif isinstance(relship, str):
                                attach = relship
                                relationship_tpl = val
                                relationship_templates = \
                                    self.tosca._tpl_relationship_templates()
                                if 'relationship' in relationship_tpl and \
                                   attach not in \
                                   self.tosca._tpl_relationship_types() and \
                                   attach in relationship_templates:
                                    relationship_tpl['relationship'] = \
                                        relationship_templates[attach]
                                break
                        if relationship_tpl:
                            rval_new = attach + "_" + str(suffix)
                            att = RelationshipTemplate(
                                relationship_tpl, rval_new,
                                self.tosca._tpl_relationship_types())
                            hot_node = ToscaBlockStorageAttachment(att, ntpl,
                                                                   node.name,
                                                                   volume_name
                                                                   )
                            return hot_node

    def find_hot_resource(self, name):
        for resource in self.hot_resources:
            if resource.name == name:
                return resource

    def _find_tosca_node(self, tosca_name):
        for node in self.nodetemplates:
            if node.name == tosca_name:
                return node

    def _find_hot_resource_for_tosca(self, tosca_name):
        for node in self.nodetemplates:
            if node.name == tosca_name:
                return self.hot_lookup[node]

    def _create_connect_configs(self, source_node, target_name,
                                connect_interfaces):
        connectsto_resources = []
        if connect_interfaces:
            for iname, interface in six.iteritems(connect_interfaces):
                connectsto_resources += \
                    self._create_connect_config(source_node, target_name,
                                                interface)
        return connectsto_resources

    def _create_connect_config(self, source_node, target_name,
                               connect_interface):
        connectsto_resources = []
        target_node = self._find_tosca_node(target_name)
        # the configuration can occur on the source or the target
        connect_config = connect_interface.get('pre_configure_target')
        if connect_config is not None:
            config_location = 'target'
        else:
            connect_config = connect_interface.get('pre_configure_source')
            if connect_config is not None:
                config_location = 'source'
            else:
                msg = _("Template error:  "
                        "no configuration found for ConnectsTo "
                        "in {1}").format(self.nodetemplate.name)
                log.warning(msg)
                raise Exception(msg)
        config_name = source_node.name + '_' + target_name + '_connect_config'
        implement = connect_config.get('implementation')
        if config_location == 'target':
            hot_config = HotResource(target_node,
                                     config_name,
                                     'OS::Heat::SoftwareConfig',
                                     {'config': {'get_file': implement}})
        elif config_location == 'source':
            hot_config = HotResource(source_node,
                                     config_name,
                                     'OS::Heat::SoftwareConfig',
                                     {'config': {'get_file': implement}})
        connectsto_resources.append(hot_config)
        hot_target = self._find_hot_resource_for_tosca(target_name)
        hot_source = self._find_hot_resource_for_tosca(source_node.name)
        connectsto_resources.append(hot_config.
                                    handle_connectsto(source_node,
                                                      target_node,
                                                      hot_source,
                                                      hot_target,
                                                      config_location,
                                                      connect_interface))
        return connectsto_resources