# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements.  See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You 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 re

from aria.modeling import (Type, RelationshipType, PolicyType, ServiceModel, NodeTemplate,
                           RequirementTemplate, RelationshipTemplate, CapabilityTemplate,
                           GroupTemplate, PolicyTemplate, SubstitutionTemplate, MappingTemplate,
                           InterfaceTemplate, OperationTemplate, ArtifactTemplate, Metadata,
                           Parameter)

from ..data_types import coerce_value

def create_service_model(context): # pylint: disable=too-many-locals,too-many-branches
    model = ServiceModel()

    model.description = context.presentation.get('service_template', 'description', 'value')

    metadata = context.presentation.get('service_template', 'metadata')
    if metadata is not None:
        substitution_template = Metadata()
        substitution_template.values['template_name'] = metadata.template_name
        substitution_template.values['template_author'] = metadata.template_author
        substitution_template.values['template_version'] = metadata.template_version
        custom = metadata.custom
        if custom:
            for name, v in custom.iteritems():
                substitution_template.values[name] = v
        model.metadata = substitution_template

    create_types(context,
                 context.modeling.node_types,
                 context.presentation.get('service_template', 'node_types'))
    create_types(context,
                 context.modeling.group_types,
                 context.presentation.get('service_template', 'group_types'))
    create_types(context,
                 context.modeling.capability_types,
                 context.presentation.get('service_template', 'capability_types'))
    create_types(context,
                 context.modeling.relationship_types,
                 context.presentation.get('service_template', 'relationship_types'),
                 create_relationship_type)
    create_types(context,
                 context.modeling.policy_types,
                 context.presentation.get('service_template', 'policy_types'),
                 create_policy_type)
    create_types(context,
                 context.modeling.artifact_types,
                 context.presentation.get('service_template', 'artifact_types'))
    create_types(context,
                 context.modeling.interface_types,
                 context.presentation.get('service_template', 'interface_types'))

    topology_template = context.presentation.get('service_template', 'topology_template')
    if topology_template is not None:
        create_properties_from_values(model.inputs, topology_template._get_input_values(context))
        create_properties_from_values(model.outputs, topology_template._get_output_values(context))

    node_templates = context.presentation.get('service_template', 'topology_template',
                                              'node_templates')
    if node_templates:
        for node_template_name, node_template in node_templates.iteritems():
            model.node_templates[node_template_name] = create_node_template(context, node_template)

    groups = context.presentation.get('service_template', 'topology_template', 'groups')
    if groups:
        for group_name, group in groups.iteritems():
            model.group_templates[group_name] = create_group_template(context, group)

    policies = context.presentation.get('service_template', 'topology_template', 'policies')
    if policies:
        for policy_name, policy in policies.iteritems():
            model.policy_templates[policy_name] = create_policy_template(context, policy)

    substitution_mappings = context.presentation.get('service_template', 'topology_template',
                                                     'substitution_mappings')
    if substitution_mappings is not None:
        substitution_template = SubstitutionTemplate(substitution_mappings.node_type)
        capabilities = substitution_mappings.capabilities
        if capabilities:
            for mapped_capability_name, capability in capabilities.iteritems():
                substitution_template.capability_templates[mapped_capability_name] = \
                    MappingTemplate(mapped_capability_name, capability.node_template,
                                    capability.capability)
        requirements = substitution_mappings.requirements
        if requirements:
            for mapped_requirement_name, requirement in requirements.iteritems():
                substitution_template.requirement_templates[mapped_requirement_name] = \
                    MappingTemplate(mapped_requirement_name, requirement.node_template,
                                    requirement.requirement)
        model.substitution_template = substitution_template

    return model

def create_node_template(context, node_template):
    model = NodeTemplate(name=node_template._name, type_name=node_template.type)

    if node_template.description:
        model.description = node_template.description.value

    create_properties_from_values(model.properties, node_template._get_property_values(context))
    create_interface_templates(context, model.interface_templates,
                               node_template._get_interfaces(context))

    artifacts = node_template._get_artifacts(context)
    if artifacts:
        for artifact_name, artifact in artifacts.iteritems():
            model.artifact_templates[artifact_name] = create_artifact_template(context, artifact)

    requirements = node_template._get_requirements(context)
    if requirements:
        for _, requirement in requirements:
            model.requirement_templates.append(create_requirement_template(context, requirement))

    capabilities = node_template._get_capabilities(context)
    if capabilities:
        for capability_name, capability in capabilities.iteritems():
            model.capability_templates[capability_name] = create_capability_template(context,
                                                                                     capability)

    create_node_filter_constraint_lambdas(context, node_template.node_filter,
                                          model.target_node_template_constraints)

    return model

def create_interface_template(context, interface):
    the_type = interface._get_type(context)

    model = InterfaceTemplate(name=interface._name, type_name=the_type._name)

    if the_type.description:
        model.description = the_type.description.value

    inputs = interface.inputs
    if inputs:
        for input_name, the_input in inputs.iteritems():
            model.inputs[input_name] = Parameter(the_input.value.type, the_input.value.value,
                                                 the_input.value.description)

    operations = interface.operations
    if operations:
        for operation_name, operation in operations.iteritems():
            model.operation_templates[operation_name] = create_operation_template(context,
                                                                                  operation)

    return model if model.operation_templates else None

def create_operation_template(context, operation): # pylint: disable=unused-argument
    model = OperationTemplate(name=operation._name)

    if operation.description:
        model.description = operation.description.value

    implementation = operation.implementation
    if implementation is not None:
        model.implementation = implementation.primary
        dependencies = implementation.dependencies
        if dependencies is not None:
            model.dependencies = dependencies

    inputs = operation.inputs
    if inputs:
        for input_name, the_input in inputs.iteritems():
            model.inputs[input_name] = Parameter(the_input.value.type, the_input.value.value,
                                                 the_input.value.description)

    return model

def create_artifact_template(context, artifact):
    model = ArtifactTemplate(name=artifact._name, type_name=artifact.type,
                             source_path=artifact.file)

    if artifact.description:
        model.description = artifact.description.value

    model.target_path = artifact.deploy_path

    repository = artifact._get_repository(context)
    if repository is not None:
        model.repository_url = repository.url
        credential = repository._get_credential(context)
        if credential:
            for k, v in credential.iteritems():
                model.repository_credential[k] = v

    create_properties_from_values(model.properties, artifact._get_property_values(context))

    return model

def create_requirement_template(context, requirement):
    model = {'name': requirement._name}

    node, node_variant = requirement._get_node(context)
    if node is not None:
        if node_variant == 'node_type':
            model['target_node_type_name'] = node._name
        else:
            model['target_node_template_name'] = node._name

    capability, capability_variant = requirement._get_capability(context)
    if capability is not None:
        if capability_variant == 'capability_type':
            model['target_capability_type_name'] = capability._name
        else:
            model['target_capability_name'] = capability._name

    model = RequirementTemplate(**model)

    create_node_filter_constraint_lambdas(context, requirement.node_filter,
                                          model.target_node_template_constraints)

    relationship = requirement.relationship
    if relationship is not None:
        model.relationship_template = create_relationship_template(context, relationship)

    return model

def create_relationship_type(context, relationship_type): # pylint: disable=unused-argument
    return RelationshipType(relationship_type._name)

def create_policy_type(context, policy_type): # pylint: disable=unused-argument
    return PolicyType(policy_type._name)

def create_relationship_template(context, relationship):
    relationship_type, relationship_type_variant = relationship._get_type(context)
    if relationship_type_variant == 'relationship_type':
        model = RelationshipTemplate(type_name=relationship_type._name)
    else:
        relationship_template = relationship_type
        relationship_type = relationship_template._get_type(context)
        model = RelationshipTemplate(type_name=relationship_type._name,
                                     template_name=relationship_template._name)
        if relationship_template.description:
            model.description = relationship_template.description.value

    create_properties_from_assignments(model.properties, relationship.properties)
    create_interface_templates(context, model.source_interface_templates, relationship.interfaces)

    return model

def create_capability_template(context, capability):
    capability_type = capability._get_type(context)
    model = CapabilityTemplate(name=capability._name, type_name=capability_type._name)

    capability_definition = capability._get_definition(context)
    if capability_definition.description:
        model.description = capability_definition.description.value
    occurrences = capability_definition.occurrences
    if occurrences is not None:
        model.min_occurrences = occurrences.value[0]
        if occurrences.value[1] != 'UNBOUNDED':
            model.max_occurrences = occurrences.value[1]

    valid_source_types = capability_definition.valid_source_types
    if valid_source_types:
        model.valid_source_node_type_names = valid_source_types

    create_properties_from_assignments(model.properties, capability.properties)

    return model

def create_group_template(context, group):
    group_type = group._get_type(context)
    model = GroupTemplate(name=group._name, type_name=group_type._name)

    if group.description:
        model.description = group.description.value

    create_properties_from_values(model.properties, group._get_property_values(context))
    create_interface_templates(context, model.interface_templates, group._get_interfaces(context))

    members = group.members
    if members:
        for member in members:
            model.member_node_template_names.append(member)

    return model

def create_policy_template(context, policy):
    policy_type = policy._get_type(context)
    model = PolicyTemplate(name=policy._name, type_name=policy_type._name)

    if policy.description:
        model.description = policy.description.value

    create_properties_from_values(model.properties, policy._get_property_values(context))

    node_templates, groups = policy._get_targets(context)
    for node_template in node_templates:
        model.target_node_template_names.append(node_template._name)
    for group in groups:
        model.target_group_template_names.append(group._name)

    return model

#
# Utils
#

def create_types(context, root, types, normalize=None):
    if types is None:
        return

    def added_all():
        for name in types:
            if root.get_descendant(name) is None:
                return False
        return True

    while not added_all():
        for name, the_type in types.iteritems():
            if root.get_descendant(name) is None:
                parent_type = the_type._get_parent(context)
                if normalize:
                    model = normalize(context, the_type)
                else:
                    model = Type(the_type._name)
                if the_type.description:
                    model.description = the_type.description.value
                if parent_type is None:
                    root.children.append(model)
                else:
                    container = root.get_descendant(parent_type._name)
                    if container is not None:
                        container.children.append(model)

def create_properties_from_values(properties, source_properties):
    if source_properties:
        for property_name, prop in source_properties.iteritems():
            properties[property_name] = Parameter(prop.type, prop.value, prop.description)

def create_properties_from_assignments(properties, source_properties):
    if source_properties:
        for property_name, prop in source_properties.iteritems():
            properties[property_name] = Parameter(prop.value.type, prop.value.value,
                                                  prop.value.description)

def create_interface_templates(context, interfaces, source_interfaces):
    if source_interfaces:
        for interface_name, interface in source_interfaces.iteritems():
            interface = create_interface_template(context, interface)
            if interface is not None:
                interfaces[interface_name] = interface

def create_node_filter_constraint_lambdas(context, node_filter, node_type_constraints):
    if node_filter is None:
        return

    properties = node_filter.properties
    if properties is not None:
        for property_name, constraint_clause in properties:
            func = create_constraint_clause_lambda(context, node_filter, constraint_clause,
                                                   property_name, None)
            if func is not None:
                node_type_constraints.append(func)

    capabilities = node_filter.capabilities
    if capabilities is not None:
        for capability_name, capability in capabilities:
            properties = capability.properties
            if properties is not None:
                for property_name, constraint_clause in properties:
                    func = create_constraint_clause_lambda(context, node_filter, constraint_clause,
                                                           property_name, capability_name)
                    if func is not None:
                        node_type_constraints.append(func)

def create_constraint_clause_lambda(context, node_filter, constraint_clause, property_name, # pylint: disable=too-many-return-statements
                                    capability_name):
    constraint_key = constraint_clause._raw.keys()[0]
    the_type = constraint_clause._get_type(context)

    def coerce_constraint(constraint, container):
        constraint = coerce_value(context, node_filter, the_type, None, None, constraint,
                                  constraint_key) if the_type is not None else constraint
        if hasattr(constraint, '_evaluate'):
            constraint = constraint._evaluate(context, container)
        return constraint

    def get_value(node_type):
        if capability_name is not None:
            capability = node_type.capability_templates.get(capability_name)
            prop = capability.properties.get(property_name) if capability is not None else None
            return prop.value if prop is not None else None
        value = node_type.properties.get(property_name)
        return value.value if value is not None else None

    if constraint_key == 'equal':
        def equal(node_type, container):
            constraint = coerce_constraint(constraint_clause.equal, container)
            value = get_value(node_type)
            return value == constraint

        return equal

    elif constraint_key == 'greater_than':
        def greater_than(node_type, container):
            constraint = coerce_constraint(constraint_clause.greater_than, container)
            value = get_value(node_type)
            return value > constraint

        return greater_than

    elif constraint_key == 'greater_or_equal':
        def greater_or_equal(node_type, container):
            constraint = coerce_constraint(constraint_clause.greater_or_equal, container)
            value = get_value(node_type)
            return value >= constraint

        return greater_or_equal

    elif constraint_key == 'less_than':
        def less_than(node_type, container):
            constraint = coerce_constraint(constraint_clause.less_than, container)
            value = get_value(node_type)
            return value < constraint

        return less_than

    elif constraint_key == 'less_or_equal':
        def less_or_equal(node_type, container):
            constraint = coerce_constraint(constraint_clause.less_or_equal, container)
            value = get_value(node_type)
            return value <= constraint

        return less_or_equal

    elif constraint_key == 'in_range':
        def in_range(node_type, container):
            lower, upper = constraint_clause.in_range
            lower, upper = coerce_constraint(lower, container), coerce_constraint(upper, container)
            value = get_value(node_type)
            if value < lower:
                return False
            if (upper != 'UNBOUNDED') and (value > upper):
                return False
            return True

        return in_range

    elif constraint_key == 'valid_values':
        def valid_values(node_type, container):
            constraint = tuple(coerce_constraint(v, container)
                               for v in constraint_clause.valid_values)
            value = get_value(node_type)
            return value in constraint

        return valid_values

    elif constraint_key == 'length':
        def length(node_type, container): # pylint: disable=unused-argument
            constraint = constraint_clause.length
            value = get_value(node_type)
            return len(value) == constraint

        return length

    elif constraint_key == 'min_length':
        def min_length(node_type, container): # pylint: disable=unused-argument
            constraint = constraint_clause.min_length
            value = get_value(node_type)
            return len(value) >= constraint

        return min_length

    elif constraint_key == 'max_length':
        def max_length(node_type, container): # pylint: disable=unused-argument
            constraint = constraint_clause.max_length
            value = get_value(node_type)
            return len(value) >= constraint

        return max_length

    elif constraint_key == 'pattern':
        def pattern(node_type, container): # pylint: disable=unused-argument
            constraint = constraint_clause.pattern
            # Note: the TOSCA 1.0 spec does not specify the regular expression grammar, so we will
            # just use Python's
            value = node_type.properties.get(property_name)
            return re.match(constraint, str(value)) is not None

        return pattern

    return None
