#
# Because we create configurations and operate on resulting machines during
# the course of a single CLI command run we need to be able to reload the
# configurations based on updates to fetch Vagrant VMs and run Vagrant machine
# actions.  TODO: Inquire about some form of inclusion in Vagrant core?
#
module Vagrant
class Vagrantfile

  def reload
    @loader.clear_config_cache(@keys)
    @config, _ = @loader.load(@keys)
  end
end

module Config
class Loader
  def clear_config_cache(sources = nil)
    @config_cache = {}

    if sources
      keep = {}
      sources.each do |source|
        keep[source] = @sources[source] if @sources[source]
      end
      @sources = keep
    end
  end
end
end
end

#-------------------------------------------------------------------------------

module CORL
module Vagrant
module Config

  @@logger = Util::Logger.new('vagrant')

  def self.logger
    @@logger
  end

  #
  # Vagrant node network container
  #
  @@network = nil

  def self.network=network
    @@network = network
  end

  def self.network
    @@network
  end

  #
  # Whether or not we are re-rendering this configuration
  #
  @@rerender = false

  #
  # Gateway CORL configurator for Vagrant.
  #
  def self.register(directory, config, &code)
    ::Vagrant.require_version ">= 1.5.0"

    config_network = network
    config_network = load_network(directory) unless config_network

    if config_network
      # Vagrant settings
      unless configure_vagrant(config_network, config.vagrant)
        raise "Configuration of Vagrant general settings failed"
      end

      config_network.nodes(:vagrant, true).each do |node_name, node|
        config.vm.define node.id.to_sym do |machine|
          render("\n")
          render("config.vm.define '#{node.id}' do |node|")

          # SSH settings
          unless configure_ssh(node, machine)
            raise "Configuration of Vagrant VM SSH settings failed"
          end

          # VM settings
          unless configure_vm(node, machine)
            raise "Configuration of Vagrant VM failed: #{node_name}"
          end

          # Provisioner configuration
          unless configure_provisioner(network, node, machine, &code)
            raise "Configuration of Vagrant provisioner failed: #{node_name}"
          end
        end
      end
    end
    @@rerender = true
  end

  #---

  def self.load_network(directory)
    # Load network if it exists
    @@network = CORL.network(directory, CORL.config(:vagrant_network, { :directory => directory }))
  end

  #---

  def self.configure_vagrant(network, vagrant)
    success = true
    Util::Data.hash(network.settings(:vagrant_config)).each do |name, data|
      if vagrant.respond_to?("#{name}=")
        data = Util::Data.value(data)
        render("config.vagrant.#{name} = %property", { :property => data })
        vagrant.send("#{name}=", data)
      else
        params = parse_params(data)
        render("config.vagrant.#{name} %params", { :params => params })
        vagrant.send(name, params)
      end
    end
    success
  end

  #---

  def self.configure_ssh(node, machine)
    success = true

    render("  node.ssh.username = %property", { :property => node.user })
    machine.ssh.username = node.user

    render("  node.ssh.guest_port = %property", { :property => node.ssh_port })
    machine.ssh.guest_port = node.ssh_port

    if node.cache_setting(:use_private_key, false)
      key_dir     = node.network.key_cache_directory
      key_name    = node.plugin_name

      ssh_config  = ::CORL::Config.new({
        :keypair  => node.keypair,
        :key_dir  => key_dir,
        :key_name => key_name
      })

      if keypair = Util::SSH.unlock_private_key(node.private_key, ssh_config)
        if keypair.is_a?(String)
          render("  node.ssh.private_key_path = %property", { :property => keypair })
          machine.ssh.private_key_path = keypair
        else
          private_key_file = keypair.private_key_file(key_dir, key_name)
          node.keypair     = keypair
          render("  node.ssh.private_key_path = %property", { :property => private_key_file })
          machine.ssh.private_key_path = private_key_file
        end
      end
      unless keypair && File.exists?(machine.ssh.private_key_path)
        render("  node.ssh.private_key_path = %property", { :property => node.private_key })
        machine.ssh.private_key_path = node.private_key
      end
    end

    render("\n")

    Util::Data.hash(node.ssh).each do |name, data|
      if machine.ssh.respond_to?("#{name}=")
        data = Util::Data.value(data)
        render("  node.ssh.#{name} = %property", { :property => data })
        machine.ssh.send("#{name}=", data)
      else
        params = parse_params(data)
        render("  node.ssh.#{name} %params", { :params => params })
        machine.ssh.send(name, params)
      end
    end
    success
  end

  #---

  def self.configure_vm(node, machine)
    vm_config = Util::Data.hash(Util::Data.clone(node.vm))
    success   = true

    render("  node.vm.hostname = %property", { :property => node.hostname })
    machine.vm.hostname = node.hostname

    box      = node.cache_setting(:box)
    box_url  = node.cache_setting(:box_url)
    box_file = nil

    if box_url
      box_file = box_url.gsub(/^file\:\/\//, '')
      unless File.exists?(box_file)
        box_url = nil
        node.clear_cache
      end
    end

    if vm_config.has_key?(:private_network)
      network_options = Util::Data.hash(vm_config[:private_network])

      if node[:public_ip]
        network_options[:ip] = node[:public_ip]
      end
      render("  node.vm.network :private_network, %params", { :params => network_options })
      machine.vm.network :private_network, network_options
      vm_config.delete(:private_network)
      render("\n")

    elsif vm_config.has_key?(:public_network)
      network_options = Util::Data.hash(vm_config[:public_network])

      render("  node.vm.network :public_network, %params", { :params => network_options })
      machine.vm.network :public_network, network_options
      vm_config.delete(:public_network)
      render("\n")
    end

    if vm_config.has_key?(:provision)
      Util::Data.array(vm_config[:provision]).each do |provisioner|
        if provisioner.is_a?(String)
          render("  node.vm.provision :#{provisioner}")
          machine.vm.provision provisioner
        else
          provision_options = Util::Data.symbol_map(provisioner)
          provision_type    = provision_options.delete(:type)

          if provision_type
            render("  node.vm.provision :#{provision_type}, %params", { :params => provision_options })
            machine.vm.provision provision_type, provision_options
          end
        end
      end
      vm_config.delete(:provision)
    end

    vm_config.each do |name, data|
      case name.to_sym
      # Network interfaces
      when :forwarded_ports
        data.each do |forward_name, info|
          forward_config = CORL::Config.new({ :auto_correct => true }).import(info)

          forward_config.keys do |key|
            forward_config[key] = Util::Data.value(forward_config[key])
          end
          forward_options = forward_config.export
          render("  node.vm.network :forwarded_port, %params", { :params => forward_options })
          machine.vm.network :forwarded_port, forward_options
        end
        render("\n")
      when :usable_port_range
        low, high = data.to_s.split(/\s*--?\s*/)
        render("  node.vm.usable_port_range = #{low}..#{high}")
        machine.vm.usable_port_range = Range.new(low, high)

      # Provider specific settings
      when :providers
        data.each do |provider, info|
          provider          = provider.to_sym
          info              = Util::Data.symbol_map(info)
          already_processed = {}

          machine.vm.provider provider do |interface, override|
            render("  node.vm.provider '#{provider}' do |provider, override|  # for #{node.hostname}") unless already_processed[provider]

            if box || box_url
              if provider != :docker
                if box && box_url
                  render("    override.vm.box = %property", { :property => box }) unless already_processed[provider]
                  override.vm.box = box

                  render("    override.vm.box_url = %property", { :property => box_url }) unless already_processed[provider]
                  override.vm.box_url = box_url
                end
              else
                if box_file
                  render("    provider.build_dir = %property", { :property => box_file }) unless already_processed[provider]
                  interface.build_dir = box_file
                else
                  render("    provider.image = %property", { :property => box }) unless already_processed[provider]
                  interface.image = box
                end
              end
              render("\n")
            end

            if info.has_key?(:private_network)
              network_options = info[:private_network].is_a?(Hash) ? info[:private_network] : { :ip => info[:private_network] }

              render("    node.vm.network :private_network, %params", { :params => network_options }) unless already_processed[provider]
              machine.vm.network :private_network, network_options
              info.delete(:private_network)

            elsif info.has_key?(:public_network)
              network_options = info[:public_network].is_a?(Hash) ? info[:public_network] : { :ip => info[:public_network] }

              render("    node.vm.network :public_network, %params", { :params => network_options })
              machine.vm.network :public_network, network_options
              info.delete(:public_network)
            end

            if info.has_key?(:override) && info[:override].has_key?(:provision)
              Util::Data.array(info[:override][:provision]).each do |provisioner|
                if provisioner.is_a?(String)
                  render("    override.vm.provision :#{provisioner}")
                  override.vm.provision provisioner
                else
                  provision_options = Util::Data.symbol_map(provisioner)
                  provision_type    = provision_options.delete(:type)

                  if provision_type
                    render("    override.vm.provision :#{provision_type}, %params", { :params => provision_options })
                    override.vm.provision provision_type, provision_options
                  end
                end
              end
              info[:override].delete(:provision)
            end

            info.each do |property, item|
              if property.to_sym == :override
                configure_provider_overrides(provider, machine, override, item, already_processed[provider], [], '    ')
              else
                if interface.respond_to?("#{property}=")
                  render("    provider.#{property} = %property", { :property => item }) unless already_processed[provider]
                  interface.send("#{property}=", item)
                else
                  params = parse_params(item)
                  render("    provider.#{property} %params", { :params => params }) unless already_processed[provider]
                  interface.send(property, params)
                end
              end
            end

            # Server shares
            unless configure_shares(node, provider, override, already_processed[provider], '    ')
              raise "Configuration of Vagrant shares failed: #{node_name}"
            end

            unless already_processed[provider]
              render("  end")
              render("\n")
            end
            already_processed[provider] = 1
          end
        end
      # All other basic VM settings...
      else
        if machine.vm.respond_to?("#{name}=")
          render("  node.vm.#{name} = %property", { :property => data })
          machine.vm.send("#{name}=", data)
        else
          params = parse_params(data)
          render("  node.vm.#{name} %params", { :params => params })
          machine.vm.send(name, params)
        end
      end
      render("\n")
    end
    success
  end

  #---

  def self.configure_shares(node, provider, machine, already_processed, indent = '')
    use_nfs          = provider.to_sym != :docker
    bindfs_installed = Gems.exist?('vagrant-bindfs')
    success          = true

    if use_nfs && bindfs_installed
      machine.vm.synced_folder ".", "/vagrant", disabled: true
      machine.vm.synced_folder ".", "/tmp/vagrant", :type => "nfs"
      machine.bindfs.bind_folder "/tmp/vagrant", "/vagrant"
    end

    render("\n") unless already_processed

    Util::Data.hash(node.shares).each do |name, options|
      config = CORL::Config.ensure(options)

      if config[:type].to_sym == :nfs && ! use_nfs
        config.delete(:type)
      end

      share_type = config.get(:type, nil)
      local_dir  = config.delete(:local, '')
      remote_dir = config.delete(:remote, '')

      config.init(:create, true)

      unless local_dir.empty? || remote_dir.empty?
        bindfs_options = config.delete(:bindfs, {})
        share_options  = {}

        config.keys.each do |key|
          share_options[key] = Util::Data.value(config[key])
        end

        if share_type && share_type.to_sym == :nfs && bindfs_installed
          final_dir  = remote_dir
          remote_dir = [ '/tmp', remote_dir.sub(/^\//, '') ].join('/')

          render("#{indent}override.bindfs.bind_folder '#{remote_dir}', '#{final_dir}', %params", { :params => bindfs_options }) unless already_processed
          machine.bindfs.bind_folder remote_dir, final_dir, bindfs_options
        end

        render("#{indent}override.vm.synced_folder '#{local_dir}', '#{remote_dir}', %params", { :params => share_options }) unless already_processed
        machine.vm.synced_folder local_dir, remote_dir, share_options
      end
    end
    success
  end

  #---

  def self.configure_provisioner(network, node, machine, &code)
    success = true

    unless node[:docker_host]
      # CORL provisioning
      machine.vm.provision :corl do |provisioner|
        provisioner.network = network
        provisioner.node    = node

        code.call(node, machine, provisioner) if code
      end
    end
    success
  end

  #---

  def self.configure_provider_overrides(provider, machine, config, data, already_processed, parents = [], indent = '')
    data.each do |name, info|
      label = (parents.empty? ? name : "#{parents.join('.')}.#{name}")

      if info.is_a?(Hash)
        configure_provider_overrides(provider, machine.send(name), config.send(name), info, already_processed, [ parents, name].flatten, indent)
      else
        if machine.respond_to?("#{name}=")
          render("#{indent}override.#{label} = %property", { :property => info }) unless already_processed
          config.send("#{name}=", info)
        else
          params = parse_params(info)
          render("#{indent}override.#{label} %params", { :params => params }) unless already_processed
          config.send(name, params)
        end
      end
    end
  end

  #---

  def self.parse_params(data)
    params = data
    if data.is_a?(Hash)
      params = []
      data.each do |key, item|
        unless Util::Data.undef?(item)
          params << ( key.match(/^\:/) ? key.gsub(/^\:/, '').to_sym : key.to_s )
          unless Util::Data.empty?(item)
            value = item
            value = ((item.is_a?(String) && item.match(/^\:/)) ? item.gsub(/^\:/, '').to_sym : item)
            params << Util::Data.value(value)
          end
        end
      end
    end
    params
  end

  #---

  def self.render(statement, inputs = {})
    return unless [ :debug, :info, :warn, :error ].include?(Nucleon.log_level)

    if statement =~ /^\s+$/
      puts statement
    else
      Util::Data.clone(inputs).each do |name, data|
        rendered_data = render_value(data)

        if rendered_data.empty?
          statement.gsub!(/,\s*$/, '')
        end
        statement.gsub!("\%#{name}", rendered_data)
      end
      Core.ui_group('') do |ui|
        ui.success(statement, { :prefix => false })
      end
    end
  end

  #---

  def self.render_value(data)
    rendered_value = ''

    case data
    when Hash
      keypairs = []
      data.each do |name, value|
        keypairs << "#{name}: " + render_value(value)
      end
      rendered_value = keypairs.join(', ')
    when Array
      unless data.empty?
        data.collect! {|value| render_value(value) }
        rendered_value = '[' + data.join(', ') + ']'
      end
    when String
      if check_numeric(data) || data[0] == ':'
        rendered_value = data.to_s
      else
        if data =~ /\'/
          rendered_value = "\"#{data}\""
        else
          rendered_value = "'#{data}'"
        end
      end
    when Symbol
      rendered_value = ":#{data}"
    else
      rendered_value = data.to_s
    end
    rendered_value
  end

  #---

  def self.check_numeric(string)
    return true if string =~ /^\d+$/
    begin
      Float(string)
      return true
    rescue
      return false
    end
  end
end
end
end