"""
This module makes it possible to instantiate a new Troposphere Template object
from an existing CloudFormation Template.

Usage:
    from troposphere.template_generator import TemplateGenerator
    import json

    with open("myCloudFormationTemplate.json") as f:
        json_template = json.load(f)

    template = TemplateGenerator(json_template)
    template.to_json()
"""

import inspect
import pkgutil
import importlib
import os

from collections import Sequence, Mapping

from troposphere import (
    Template, Ref,
    Output, Parameter,  # AWSDeclarations
    AWSObject,  # covers resources
    AWSHelperFn, GenericHelperFn,  # covers ref, fn::, etc
    Tags, autoscaling, cloudformation)
from troposphere.policies import UpdatePolicy, CreationPolicy


class TemplateGenerator(Template):
    DEPRECATED_MODULES = ['troposphere.dynamodb2']

    _inspect_members = set()
    _inspect_resources = {}
    _inspect_functions = {}

    def __init__(self, cf_template):
        """
        Instantiates a new Troposphere Template based on an existing
        Cloudformation Template.
        """
        super(TemplateGenerator, self).__init__()
        self._reference_map = {}
        if 'AWSTemplateFormatVersion' in cf_template:
            self.add_version(cf_template['AWSTemplateFormatVersion'])
        if 'Transform' in cf_template:
            self.add_transform(cf_template['Transform'])
        if 'Description' in cf_template:
            self.add_description(cf_template['Description'])
        if 'Metadata' in cf_template:
            self.add_metadata(cf_template['Metadata'])
        for k, v in cf_template.get('Parameters', {}).iteritems():
            self.add_parameter(self._create_instance(Parameter, v, k))
        for k, v in cf_template.get('Mappings', {}).iteritems():
            self.add_mapping(k, self._convert_definition(v))
        for k, v in cf_template.get('Conditions', {}).iteritems():
            self.add_condition(k, self._convert_definition(v, k))
        for k, v in cf_template.get('Resources', {}).iteritems():
            self.add_resource(self._convert_definition(
                                    v, k,
                                    self._get_resource_type_cls(v)
            ))
        for k, v in cf_template.get('Outputs', {}).iteritems():
            self.add_output(self._create_instance(Output, v, k))

    @property
    def inspect_members(self):
        """
        Returns the list of all troposphere members we are able to
        construct
        """
        if not self._inspect_members:
            TemplateGenerator._inspect_members = \
                self._import_all_troposphere_modules()
        return self._inspect_members

    @property
    def inspect_resources(self):
        """ Returns a map of `ResourceType: ResourceClass` """
        if not self._inspect_resources:
            d = {}
            for m in self.inspect_members:
                if issubclass(m, (AWSObject, cloudformation.AWSCustomObject)) \
                        and hasattr(m, 'resource_type'):
                    d[m.resource_type] = m

            TemplateGenerator._inspect_resources = d

        return self._inspect_resources

    @property
    def inspect_functions(self):
        """ Returns a map of `FunctionName: FunctionClass` """
        if not self._inspect_functions:
            d = {}
            for m in self.inspect_members:
                if issubclass(m, AWSHelperFn):
                    d[m.__name__] = m

            TemplateGenerator._inspect_functions = d

        return self._inspect_functions

    def _get_resource_type_cls(self, resource):
        """Attempts to return troposphere class that represents Type of
        provided resource. Attempts to find the troposphere class who's
        `resource_type` field is the same as the provided resources `Type`
        field.

        :param resource: Resource to find troposphere class for
        :return: None: If provided resource does not have a `Type` field
                       If no class found for provided resource
                 type: Type of provided resource
        """
        # If provided resource does not have `Type` field
        if 'Type' not in resource:
            return None

        # Attempt to find troposphere resource with:
        #   `resource_type` == resource['Type']
        try:
            return self.inspect_resources[resource['Type']]
        except KeyError:
            # If no resource with `resource_type` == resource['Type'] found
            return None

    def _convert_definition(self, definition, ref=None, cls=None):
        """
        Converts any object to its troposphere equivalent, if applicable.
        This function will recurse into lists and mappings to create
        additional objects as necessary.

        :param {*} definition: Object to convert
        :param str ref: Name of key in parent dict that the provided definition
                        is from, can be None
        :param type cls: Troposphere class which represents provided definition
        """
        if isinstance(definition, Mapping):
            if 'Type' in definition:  # this is an AWS Resource
                expected_type = None
                if cls is not None:
                    expected_type = cls
                else:
                    # if the user uses the custom way to name custom resources,
                    # we'll dynamically create a new subclass for this use and
                    # pass that instead of the typical CustomObject resource
                    try:
                        expected_type = self._generate_custom_type(
                            definition['Type'])
                    except TypeError:
                        # If definition['Type'] turns out not to be a custom
                        # type (aka doesn't start with "Custom::")

                        # Make sure expected_type is nothing (as
                        # it always should be)
                        assert not expected_type

                if expected_type:
                    args = self._normalize_properties(definition)
                    return self._create_instance(expected_type, args, ref)

            if len(definition) == 1:  # This might be a function?
                function_type = self._get_function_type(
                    definition.keys()[0])
                if function_type:
                    return self._create_instance(
                        function_type, definition.values()[0])

            # nothing special here - return as dict
            d = {}
            for k, v in definition.iteritems():
                d[k] = self._convert_definition(v)
            return d

        elif (isinstance(definition, Sequence) and
                not isinstance(definition, basestring)):
            return [self._convert_definition(v) for v in definition]

        # anything else is returned as-is
        return definition

    def _create_instance(self, cls, args, ref=None):
        """
        Returns an instance of `cls` with `args` passed as arguments.

        Recursively inspects `args` to create nested objects and functions as
        necessary.

        `cls` will only be considered only if it's an object we track
         (i.e.: troposphere objects).

        If `cls` has a `props` attribute, nested properties will be
         instanciated as troposphere Property objects as necessary.

        If `cls` is a list and contains a single troposphere type, the
         returned value will be a list of instances of that type.
        """
        if isinstance(cls, Sequence):
            if len(cls) == 1:
                # a list of 1 type means we must provide a list of such objects
                if (isinstance(args, basestring) or
                        not isinstance(args, Sequence)):
                    args = [args]
                return [self._create_instance(cls[0], v) for v in args]

        if isinstance(cls, Sequence) or cls not in self.inspect_members:
            # this object doesn't map to any known object. could be a string
            # or int, or a Ref... or a list of types such as
            # [basestring, FindInMap, Ref] or maybe a
            # validator such as `integer` or `port_range`
            return self._convert_definition(args)

        elif issubclass(cls, AWSHelperFn):
            # special handling for functions, we want to handle it before
            # entering the other conditions.
            try:
                if issubclass(cls, Tags):
                    arg_dict = {}
                    for d in args:
                        arg_dict[d['Key']] = d['Value']
                    return cls(arg_dict)

                if (isinstance(args, Sequence) and
                        not isinstance(args, basestring)):
                    return cls(*self._convert_definition(args))

                if issubclass(cls, autoscaling.Metadata):
                    return self._generate_autoscaling_metadata(cls, args)

                args = self._convert_definition(args)
                if isinstance(args, Ref) and issubclass(cls, Ref):
                    # watch out for double-refs...
                    # this can happen if an object's .props has 'Ref'
                    # as the expected type (which is wrong and should be
                    # changed to basestring!)
                    return args

                return cls(args)

            except TypeError as ex: