require 'beaker-hostgenerator/data'
require 'beaker-hostgenerator/roles'
require 'beaker-hostgenerator/hypervisor'
require 'beaker-hostgenerator/parser'

module BeakerHostGenerator
  class Generator
    include BeakerHostGenerator::Data
    include BeakerHostGenerator::Parser
    include BeakerHostGenerator::Roles

    # Main host generation entry point, returns a Ruby map for the given host
    # specification and optional configuration.
    #
    # @param layout [String] The raw hosts specification user input.
    #                        For example `"centos6-64m-redhat7-64a"`.
    # @param options [Hash] Global, optional configuration such as the default
    #                       hypervisor or OS info version.
    #
    # @returns [Hash] A complete Ruby map as defining the HOSTS and CONFIG
    #                 sections as required by Beaker.
    def generate(layout, options)
      layout = prepare(layout)
      tokens = tokenize_layout(layout)
      config = {}.deep_merge(BASE_CONFIG)
      nodeid = Hash.new(1)
      ostype = nil
      bhg_version = options[:osinfo_version] || 0

      tokens.each do |token|
        if is_ostype_token?(token, bhg_version)
          if nodeid[ostype] == 1 and ostype != nil
            raise "Error: no nodes generated for #{ostype}"
          end
          ostype = token
          next
        end

        node_info = parse_node_info_token(token)

        # Build node host name
        platform = "#{ostype}-#{node_info['bits']}"
        host_name = "#{platform}-#{nodeid[ostype]}"

        node_info['platform'] = platform
        node_info['ostype'] = ostype
        node_info['nodeid'] = nodeid[ostype]

        host_config = base_host_config(options)

        # Delegate to the hypervisor
        hypervisor = BeakerHostGenerator::Hypervisor.create(node_info, options)
        host_config = hypervisor.generate_node(node_info, host_config, bhg_version)
        config['CONFIG'].deep_merge!(hypervisor.global_config())

        # Merge in any arbitrary key-value host settings. Treat the 'hostname'
        # setting specially, and don't merge it in as an arbitrary setting.
        arbitrary_settings = node_info['host_settings']
        host_name = arbitrary_settings.delete('hostname') if
          arbitrary_settings.has_key?('hostname')
        host_config.merge!(arbitrary_settings)

        if PE_USE_WIN32 && ostype =~ /windows/ && node_info['bits'] == "64"
          host_config['ruby_arch'] = 'x86'
          host_config['install_32'] = true
        end

        generate_host_roles!(host_config, node_info, options)

        config['HOSTS'][host_name] = host_config
        nodeid[ostype] += 1
      end

      # Merge in global configuration settings after the hypervisor defaults
      if options[:global_config]
        decoded = prepare(options[:global_config])
        # Support for strings without '{}' was introduced, so just double
        # check here to ensure that we pass in values surrounded by '{}'.
        if !decoded.start_with?('{')
          decoded = "{#{decoded}}"
        end
        global_config = settings_string_to_map(decoded)
        config['CONFIG'].deep_merge!(global_config)
      end

      # Munge non-string scalar values into proper data types
      unstringify_values!(config)

      return config
    end

    def get_host_roles(node_info)
      roles = []

      node_info['roles'].each_char do |c|
        roles << ROLES[c]
      end

      node_info['arbitrary_roles'].each do |role|
        roles << role
      end

      return roles
    end

    private

    def generate_host_roles!(host_config, node_info, options)
      if not options[:disable_default_role]
        host_config['roles'] = ['agent']
      else
        host_config['roles'] = []
      end

      host_config['roles'].concat get_host_roles(node_info)
      host_config['roles'].uniq!

      if not options[:disable_role_config]
        host_config['roles'].each do |role|
          host_config.deep_merge! get_role_config(role)
        end
      end
    end

    # Passes over all the values of config['HOSTS'] and config['CONFIG'] and
    # subsequent arrays to convert numbers or booleans into proper integer
    # or boolean types.
    def unstringify_values!(config)
      config['HOSTS'].each do |host, settings|
        settings.each do |k, v|
          config['HOSTS'][host][k] = unstringify_value(v)
        end
      end
      config['CONFIG'].each do |k, v|
        config['CONFIG'][k] = unstringify_value(v)
      end
    end

    # Attempts to convert numeric strings and boolean strings into proper
    # integer and boolean types. If value is an array, it will recurse
    # through those values.
    # Returns the input value if it's not a number string or boolean string.
    # For example "123" would be converted to 123, and "true"/"false" would be
    # converted to true/false.
    # The only valid boolean-strings are "true" and "false".
    def unstringify_value(value)
      result = Integer(value) rescue value
      if value == 'true'
        result = true
      elsif value == 'false'
        result = false
      elsif value.kind_of?(Array)
        value.each_with_index do |v, i|
          result[i] = unstringify_value(v)
        end
      end
      result
    end
  end
end
