require 'cfndsl/JSONable'
require 'cfndsl/names'

module CfnDsl
  class CloudFormationTemplate < JSONable
    ##
    # Handles the overall template object
    dsl_attr_setter :AWSTemplateFormatVersion, :Description
    dsl_content_object :Condition, :Parameter, :Output, :Resource, :Mapping

    def initialize
      @AWSTemplateFormatVersion = "2010-09-09"
    end

    def generateOutput()
      puts self.to_json  # uncomment for pretty printing # {:space => ' ', :indent => '  ', :object_nl => "\n", :array_nl => "\n" }
    end

    @@globalRefs = {
       "AWS::NotificationARNs" => 1,
       "AWS::Region" => 1,
       "AWS::StackId" => 1,
       "AWS::StackName" => 1,
       "AWS::AccountId" => 1,
       "AWS::NoValue" => 1
    }

    def isValidRef( ref, origin=nil)
      ref = ref.to_s
      origin = origin.to_s if origin

      return true if @@globalRefs.has_key?( ref )

      return true if @Parameters && @Parameters.has_key?( ref )

      if( @Resources.has_key?( ref ) ) then
          return !origin || !@_ResourceRefs || !@_ResourceRefs[ref] || !@_ResourceRefs[ref].has_key?(origin)
      end

      return false
    end

    def checkRefs()
      invalids = []
      @_ResourceRefs = {}
      if(@Resources)  then
        @Resources.keys.each do |resource|
          @_ResourceRefs[resource.to_s] = @Resources[resource].references({})
        end
        @_ResourceRefs.keys.each do |origin|
          @_ResourceRefs[origin].keys.each do |ref|
            invalids.push "Invalid Reference: Resource #{origin} refers to #{ref}" unless isValidRef(ref,origin)
          end
        end
      end
      outputRefs = {}
      if(@Outputs) then
        @Outputs.keys.each do |resource|
          outputRefs[resource.to_s] = @Outputs[resource].references({})
        end
        outputRefs.keys.each do |origin|
          outputRefs[origin].keys.each do |ref|
            invalids.push "Invalid Reference: Output #{origin} refers to #{ref}" unless isValidRef(ref,nil)
          end
        end
      end
      return invalids.length>0 ? invalids : nil
    end


    names = {}
    nametypes = {}
    CfnDsl::Types::AWS_Types["Resources"].each_pair do |name, type|
      # Subclass ResourceDefintion and generate property methods
      klass = Class.new(CfnDsl::ResourceDefinition)
      klassname = name.split("::").join("_")
      CfnDsl::Types.const_set( klassname, klass )
      type["Properties"].each_pair do |pname, ptype|
        if( ptype.instance_of? String )
          create_klass = CfnDsl::Types.const_get( ptype );

          klass.class_eval do
            CfnDsl::methodNames(pname) do |method|
              define_method(method) do |*values, &block|
                if( values.length <1 ) then
                  values.push create_klass.new
                end
                @Properties ||= {}
                @Properties[pname] ||= CfnDsl::PropertyDefinition.new( *values )
                @Properties[pname].value.instance_eval &block if block
                @Properties[pname].value
              end
            end
          end
        else
          #Array version
          sing_name = CfnDsl::Plurals.singularize( pname )
          create_klass = CfnDsl::Types.const_get( ptype[0] )
          klass.class_eval do
            CfnDsl::methodNames(pname) do |method|
              define_method(method) do |*values, &block|
                if( values.length < 1 ) then
                  values.push []
                end
                @Properties ||= {}
                @Properties[pname] ||= PropertyDefinition.new( *values )
                @Properties[pname].value.instance_eval &block if block
                @Properties[pname].value
              end
            end

            CfnDsl::methodNames(sing_name) do |method|
              define_method(method) do |value=nil, &block|
                @Properties ||= {}
                @Properties[pname] ||= PropertyDefinition.new( [] )
                if( !value ) then
                  value = create_klass.new
                end
                @Properties[pname].value.push value
                value.instance_eval &block if block
                value
              end
            end
          end
        end

      end
      parts = name.split "::"
      while( parts.length > 0)
        abreve_name = parts.join "_"
        if( names.has_key? abreve_name ) then
          # this only happens if there is an ambiguity
          names[abreve_name] = nil
        else
          names[abreve_name] = CfnDsl::Types.const_get(klassname)
          nametypes[abreve_name] = name
        end
        parts.shift
      end


    end

    #Define property setter methods for each of the unambiguous type names
    names.each_pair do |typename,type|
      if(type) then
        class_eval do
          CfnDsl::methodNames(typename) do |method|
            define_method(method) do |name,*values,&block|
              name = name.to_s
              @Resources ||= {}
              resource = @Resources[name] ||= type.new(*values)
              resource.instance_eval &block if block
              resource.instance_variable_set( "@Type", nametypes[typename] )
              resource
            end
          end
        end
      end
    end
  end
end