require 'cgi'

require 'vagrant'

class Numeric
  Alphabet = ('a'..'z').to_a
  def vdev
    s = ''
    q = self
    (q, r = (q - 1).divmod(26)) && s.prepend(Alphabet[r]) until q.zero?
    'vd' + s
  end
end

module VagrantPlugins
  module ProviderLibvirt
    class Config < Vagrant.plugin('2', :config)
      # manually specify URI
      # will supercede most other options if provided
      attr_accessor :uri

      # A hypervisor name to access via Libvirt.
      attr_accessor :driver

      # The name of the server, where Libvirtd is running.
      attr_accessor :host

      # If use ssh tunnel to connect to Libvirt.
      attr_accessor :connect_via_ssh
      # Path towards the Libvirt socket
      attr_accessor :socket

      # The username to access Libvirt.
      attr_accessor :username

      # Password for Libvirt connection.
      attr_accessor :password

      # ID SSH key file
      attr_accessor :id_ssh_key_file

      attr_accessor :proxy_command

      # Forward port with id 'ssh'
      attr_accessor :forward_ssh_port

      # Libvirt storage pool name, where box image and instance snapshots will
      # be stored.
      attr_accessor :storage_pool_name
      attr_accessor :storage_pool_path

      # Libvirt storage pool where the base image snapshot shall be stored
      attr_accessor :snapshot_pool_name

      # Turn on to prevent hostname conflicts
      attr_accessor :random_hostname

      # Libvirt default network
      attr_accessor :management_network_device
      attr_accessor :management_network_name
      attr_accessor :management_network_address
      attr_accessor :management_network_mode
      attr_accessor :management_network_mac
      attr_accessor :management_network_guest_ipv6
      attr_accessor :management_network_autostart
      attr_accessor :management_network_pci_bus
      attr_accessor :management_network_pci_slot
      attr_accessor :management_network_domain
      attr_accessor :management_network_mtu

      # System connection information
      attr_accessor :system_uri

      # Default host prefix (alternative to use project folder name)
      attr_accessor :default_prefix

      # Domain specific settings used while creating new domain.
      attr_accessor :title
      attr_accessor :description
      attr_accessor :uuid
      attr_accessor :memory
      attr_accessor :nodeset
      attr_accessor :memory_backing
      attr_accessor :channel
      attr_accessor :cpus
      attr_accessor :cpuset
      attr_accessor :cpu_mode
      attr_accessor :cpu_model
      attr_accessor :cpu_fallback
      attr_accessor :cpu_features
      attr_accessor :cpu_topology
      attr_accessor :shares
      attr_accessor :features
      attr_accessor :features_hyperv
      attr_accessor :clock_offset
      attr_accessor :clock_timers
      attr_accessor :numa_nodes
      attr_accessor :loader
      attr_accessor :nvram
      attr_accessor :boot_order
      attr_accessor :machine_type
      attr_accessor :machine_arch
      attr_accessor :machine_virtual_size
      attr_accessor :disk_bus
      attr_accessor :disk_device
      attr_accessor :disk_driver_opts
      attr_accessor :nic_model_type
      attr_accessor :nested
      attr_accessor :volume_cache # deprecated, kept for backwards compatibility; use disk_driver
      attr_accessor :kernel
      attr_accessor :cmd_line
      attr_accessor :initrd
      attr_accessor :dtb
      attr_accessor :emulator_path
      attr_accessor :graphics_type
      attr_accessor :graphics_autoport
      attr_accessor :graphics_port
      attr_accessor :graphics_passwd
      attr_accessor :graphics_ip
      attr_accessor :video_type
      attr_accessor :video_vram
      attr_accessor :keymap
      attr_accessor :kvm_hidden
      attr_accessor :sound_type

      # Sets the information for connecting to a host TPM device
      # Only supports socket-based TPMs
      attr_accessor :tpm_model
      attr_accessor :tpm_type
      attr_accessor :tpm_path
      attr_accessor :tpm_version

      # Configure the memballoon
      attr_accessor :memballoon_enabled
      attr_accessor :memballoon_model
      attr_accessor :memballoon_pci_bus
      attr_accessor :memballoon_pci_slot

      # Sets the max number of NICs that can be created
      # Default set to 8. Don't change the default unless you know
      # what are doing
      attr_accessor :nic_adapter_count

      # Storage
      attr_accessor :disks
      attr_accessor :cdroms

      # Inputs
      attr_accessor :inputs

      # Channels
      attr_accessor :channels

      # PCI device passthrough
      attr_accessor :pcis

      # Random number device passthrough
      attr_accessor :rng

      # Watchdog device
      attr_accessor :watchdog_dev

      # USB controller
      attr_accessor :usbctl_dev

      # USB device passthrough
      attr_accessor :usbs

      # Redirected devices
      attr_accessor :redirdevs
      attr_accessor :redirfilters

      # smartcard device
      attr_accessor :smartcard_dev

      # Suspend mode
      attr_accessor :suspend_mode

      # Autostart
      attr_accessor :autostart

      # Attach mgmt network
      attr_accessor :mgmt_attach

      # Additional qemuargs arguments
      attr_accessor :qemu_args

      # Additional qemuenv arguments
      attr_accessor :qemu_env

      # Use QEMU session instead of system
      attr_accessor :qemu_use_session

      def initialize
        @uri               = UNSET_VALUE
        @driver            = UNSET_VALUE
        @host              = UNSET_VALUE
        @port              = UNSET_VALUE
        @connect_via_ssh   = UNSET_VALUE
        @username          = UNSET_VALUE
        @password          = UNSET_VALUE
        @id_ssh_key_file   = UNSET_VALUE
        @socket            = UNSET_VALUE
        @proxy_command     = UNSET_VALUE
        @forward_ssh_port  = UNSET_VALUE # forward port with id 'ssh'
        @storage_pool_name = UNSET_VALUE
        @snapshot_pool_name = UNSET_VALUE
        @random_hostname   = UNSET_VALUE
        @management_network_device  = UNSET_VALUE
        @management_network_name    = UNSET_VALUE
        @management_network_address = UNSET_VALUE
        @management_network_mode = UNSET_VALUE
        @management_network_mac  = UNSET_VALUE
        @management_network_guest_ipv6 = UNSET_VALUE
        @management_network_autostart = UNSET_VALUE
        @management_network_pci_slot = UNSET_VALUE
        @management_network_pci_bus = UNSET_VALUE
        @management_network_domain = UNSET_VALUE
        @management_network_mtu = UNSET_VALUE

        # System connection information
        @system_uri      = UNSET_VALUE

        # Domain specific settings.
        @title             = UNSET_VALUE
        @description       = UNSET_VALUE
        @uuid              = UNSET_VALUE
        @memory            = UNSET_VALUE
        @nodeset           = UNSET_VALUE
        @memory_backing    = UNSET_VALUE
        @cpus              = UNSET_VALUE
        @cpuset            = UNSET_VALUE
        @cpu_mode          = UNSET_VALUE
        @cpu_model         = UNSET_VALUE
        @cpu_fallback      = UNSET_VALUE
        @cpu_features      = UNSET_VALUE
        @cpu_topology      = UNSET_VALUE
        @shares            = UNSET_VALUE
        @features          = UNSET_VALUE
        @features_hyperv   = UNSET_VALUE
        @clock_offset      = UNSET_VALUE
        @clock_timers      = []
        @numa_nodes        = UNSET_VALUE
        @loader            = UNSET_VALUE
        @nvram             = UNSET_VALUE
        @machine_type      = UNSET_VALUE
        @machine_arch      = UNSET_VALUE
        @machine_virtual_size = UNSET_VALUE
        @disk_bus          = UNSET_VALUE
        @disk_device       = UNSET_VALUE
        @disk_driver_opts  = {}
        @nic_model_type    = UNSET_VALUE
        @nested            = UNSET_VALUE
        @volume_cache      = UNSET_VALUE
        @kernel            = UNSET_VALUE
        @initrd            = UNSET_VALUE
        @dtb               = UNSET_VALUE
        @cmd_line          = UNSET_VALUE
        @emulator_path     = UNSET_VALUE
        @graphics_type     = UNSET_VALUE
        @graphics_autoport = UNSET_VALUE
        @graphics_port     = UNSET_VALUE
        @graphics_ip       = UNSET_VALUE
        @graphics_passwd   = UNSET_VALUE
        @video_type        = UNSET_VALUE
        @video_vram        = UNSET_VALUE
        @sound_type        = UNSET_VALUE
        @keymap            = UNSET_VALUE
        @kvm_hidden        = UNSET_VALUE

        @tpm_model         = UNSET_VALUE
        @tpm_type          = UNSET_VALUE
        @tpm_path          = UNSET_VALUE
        @tpm_version       = UNSET_VALUE

        @memballoon_enabled = UNSET_VALUE
        @memballoon_model   = UNSET_VALUE
        @memballoon_pci_bus = UNSET_VALUE
        @memballoon_pci_slot = UNSET_VALUE

        @nic_adapter_count = UNSET_VALUE

        # Boot order
        @boot_order        = []
        # Storage
        @disks             = []
        @cdroms            = []

        # Inputs
        @inputs            = UNSET_VALUE

        # Channels
        @channels          = UNSET_VALUE

        # PCI device passthrough
        @pcis              = UNSET_VALUE

        # Random number device passthrough
        @rng = UNSET_VALUE

        # Watchdog device
        @watchdog_dev      = UNSET_VALUE

        # USB controller
        @usbctl_dev        = UNSET_VALUE

        # USB device passthrough
        @usbs              = UNSET_VALUE

        # Redirected devices
        @redirdevs         = UNSET_VALUE
        @redirfilters      = UNSET_VALUE

        # smartcard device
        @smartcard_dev     = UNSET_VALUE

        # Suspend mode
        @suspend_mode      = UNSET_VALUE

        # Autostart
        @autostart         = UNSET_VALUE

        # Attach mgmt network
        @mgmt_attach       = UNSET_VALUE

        # Additional QEMU commandline arguments
        @qemu_args         = UNSET_VALUE

        # Additional QEMU commandline environment variables
        @qemu_env          = UNSET_VALUE

        @qemu_use_session  = UNSET_VALUE
      end

      def boot(device)
        @boot_order << device # append
      end

      def _get_device(disks)
        # skip existing devices and also the first one (vda)
        exist = disks.collect { |x| x[:device] } + [1.vdev.to_s]
        skip = 1 # we're 1 based, not 0 based...
        loop do
          dev = skip.vdev # get lettered device
          return dev unless exist.include?(dev)
          skip += 1
        end
      end

      def _get_cdrom_dev(cdroms)
        exist = Hash[cdroms.collect { |x| [x[:dev], true] }]
        # hda - hdc
        curr = 'a'.ord
        while curr <= 'd'.ord
          dev = 'hd' + curr.chr
          if exist[dev]
            curr += 1
            next
          else
            return dev
          end
        end

        # is it better to raise our own error, or let Libvirt cause the exception?
        raise 'Only four cdroms may be attached at a time'
      end

      def _generate_numa
        @numa_nodes.collect { |x|
          # Perform some validation of cpu values
          unless x[:cpus] =~ /^\d+-\d+$/
            raise 'numa_nodes[:cpus] must be in format "integer-integer"'
          end

          # Convert to KiB
          x[:memory] = x[:memory].to_i * 1024
        }

        # Grab the value of the last @numa_nodes[:cpus] and verify @cpus matches
        # Note: [:cpus] is zero based and @cpus is not, so we need to +1
        last_cpu = @numa_nodes.last[:cpus]
        last_cpu = last_cpu.scan(/\d+$/)[0]
        last_cpu = last_cpu.to_i + 1

        if @cpus != last_cpu.to_i
          raise 'The total number of numa_nodes[:cpus] must equal config.cpus'
        end

        @numa_nodes
      end

      def cpu_feature(options = {})
        if options[:name].nil? || options[:policy].nil?
          raise 'CPU Feature name AND policy must be specified'
        end

        @cpu_features = [] if @cpu_features == UNSET_VALUE

        @cpu_features.push(name:   options[:name],
                           policy: options[:policy])
      end

      def hyperv_feature(options = {})
        if options[:name].nil? || options[:state].nil?
          raise 'Feature name AND state must be specified'
        end

        if options[:name] == 'spinlocks' && options[:retries].nil?
          raise 'Feature spinlocks requires retries parameter'
        end

        @features_hyperv = []  if @features_hyperv == UNSET_VALUE

        if options[:name] == 'spinlocks'
          @features_hyperv.push(name:   options[:name],
                             state: options[:state],
                             retries: options[:retries])
        else
          @features_hyperv.push(name:   options[:name],
                             state: options[:state])
        end
      end

      def clock_timer(options = {})
        if options[:name].nil?
          raise 'Clock timer name must be specified'
        end

        options.each do |key, value|
          case key
            when :name, :track, :tickpolicy, :frequency, :mode, :present
              if value.nil?
                raise "Value of timer option #{key} is nil"
              end
            else
              raise "Unknown clock timer option: #{key}"
          end
        end

        @clock_timers.push(options.dup)
      end

      def cputopology(options = {})
        if options[:sockets].nil? || options[:cores].nil? || options[:threads].nil?
          raise 'CPU topology must have all of sockets, cores and threads specified'
        end

        if @cpu_topology == UNSET_VALUE
          @cpu_topology = {}
        end

        @cpu_topology[:sockets] = options[:sockets]
        @cpu_topology[:cores] = options[:cores]
        @cpu_topology[:threads] = options[:threads]
      end

      def memorybacking(option, config = {})
        case option
        when :source
          raise 'Source type must be specified' if config[:type].nil?
        when :access
          raise 'Access mode must be specified' if config[:mode].nil?
        when :allocation
          raise 'Allocation mode must be specified' if config[:mode].nil?
        end

        @memory_backing = [] if @memory_backing == UNSET_VALUE
        @memory_backing.push(name: option,
                             config: config)
      end

      def input(options = {})
        if options[:type].nil? || options[:bus].nil?
          raise 'Input type AND bus must be specified'
        end

        @inputs = [] if @inputs == UNSET_VALUE

        @inputs.push(type: options[:type],
                     bus:  options[:bus])
      end

      def channel(options = {})
        if options[:type].nil?
          raise 'Channel type must be specified.'
        elsif options[:type] == 'unix' && options[:target_type] == 'guestfwd'
          # Guest forwarding requires a target (ip address) and a port
          if options[:target_address].nil? || options[:target_port].nil? ||
             options[:source_path].nil?
            raise 'guestfwd requires target_address, target_port and source_path'
          end
        end

        @channels = [] if @channels == UNSET_VALUE

        @channels.push(type: options[:type],
                       source_mode: options[:source_mode],
                       source_path: options[:source_path],
                       target_address: options[:target_address],
                       target_name: options[:target_name],
                       target_port: options[:target_port],
                       target_type: options[:target_type])
      end

      def random(options = {})
        if !options[:model].nil? && options[:model] != 'random'
          raise 'The only supported rng backend is "random".'
        end

        @rng = {} if @rng == UNSET_VALUE

        @rng[:model] = options[:model]
      end

      def pci(options = {})
        if options[:bus].nil? || options[:slot].nil? || options[:function].nil?
          raise 'Bus AND slot AND function must be specified. Check `lspci` for that numbers.'
        end

        @pcis = [] if @pcis == UNSET_VALUE

        if options[:domain].nil?
          pci_domain = '0x0000'
        else
          pci_domain = options[:domain]
        end

        @pcis.push(domain:    pci_domain,
                   bus:       options[:bus],
                   slot:      options[:slot],
                   function:  options[:function])
      end

      def watchdog(options = {})
        if options[:model].nil?
          raise 'Model must be specified.'
        end

        if @watchdog_dev == UNSET_VALUE
            @watchdog_dev = {}
        end

        @watchdog_dev[:model] = options[:model]
        @watchdog_dev[:action] = options[:action] || 'reset'
      end


      def usb_controller(options = {})
        if options[:model].nil?
          raise 'USB controller model must be specified.'
        end

        if @usbctl_dev == UNSET_VALUE
            @usbctl_dev = {}
        end

        @usbctl_dev[:model] = options[:model]
        @usbctl_dev[:ports] = options[:ports]
      end

      def usb(options = {})
        if (options[:bus].nil? || options[:device].nil?) && options[:vendor].nil? && options[:product].nil?
          raise 'Bus and device and/or vendor and/or product must be specified. Check `lsusb` for these.'
        end

        @usbs = [] if @usbs == UNSET_VALUE

        @usbs.push(bus:           options[:bus],
                   device:        options[:device],
                   vendor:        options[:vendor],
                   product:       options[:product],
                   startupPolicy: options[:startupPolicy])
      end

      def redirdev(options = {})
        raise 'Type must be specified.' if options[:type].nil?

        @redirdevs = [] if @redirdevs == UNSET_VALUE

        @redirdevs.push(type: options[:type])
      end

      def redirfilter(options = {})
        raise 'Option allow must be specified.' if options[:allow].nil?

        @redirfilters = [] if @redirfilters == UNSET_VALUE

        @redirfilters.push(class: options[:class] || -1,
                           vendor: options[:vendor] || -1,
                           product: options[:product] || -1,
                           version: options[:version] || -1,
                           allow: options[:allow])
      end

      def smartcard(options = {})
        if options[:mode].nil?
          raise 'Option mode must be specified.'
        elsif options[:mode] != 'passthrough'
          raise 'Currently only passthrough mode is supported!'
        elsif options[:type] == 'tcp' && (options[:source_mode].nil? || options[:source_host].nil? || options[:source_service].nil?)
          raise 'If using type "tcp", option "source_mode", "source_host" and "source_service" must be specified.'
        end

        if @smartcard_dev == UNSET_VALUE
          @smartcard_dev = {}
        end

        @smartcard_dev[:mode] = options[:mode]
        @smartcard_dev[:type] = options[:type] || 'spicevmc'
        @smartcard_dev[:source_mode] = options[:source_mode] if @smartcard_dev[:type] == 'tcp'
        @smartcard_dev[:source_host] = options[:source_host] if @smartcard_dev[:type] == 'tcp'
        @smartcard_dev[:source_service] = options[:source_service] if @smartcard_dev[:type] == 'tcp'
      end

      # Disk driver options for primary disk
      def disk_driver(options = {})
        supported_opts = [:cache, :io, :copy_on_read, :discard, :detect_zeroes]
        @disk_driver_opts = options.select { |k,_| supported_opts.include? k }
      end

      # NOTE: this will run twice for each time it's needed- keep it idempotent
      def storage(storage_type, options = {})
        if storage_type == :file
          if options[:device] == :cdrom
            _handle_cdrom_storage(options)
          else
            _handle_disk_storage(options)
          end
        end
      end

      def _handle_cdrom_storage(options = {})
        # <disk type="file" device="cdrom">
        #   <source file="/home/user/virtio-win-0.1-100.iso"/>
        #   <target dev="hdc"/>
        #   <readonly/>
        #   <address type='drive' controller='0' bus='1' target='0' unit='0'/>
        # </disk>
        #
        # note the target dev will need to be changed with each cdrom drive (hdc, hdd, etc),
        # as will the address unit number (unit=0, unit=1, etc)

        options = {
          type: 'raw',
          bus: 'ide',
          path: nil
        }.merge(options)

        cdrom = {
          type: options[:type],
          dev: options[:dev],
          bus: options[:bus],
          path: options[:path]
        }

        @cdroms << cdrom
      end

      def _handle_disk_storage(options = {})
        options = {
          type: 'qcow2',
          size: '10G', # matches the fog default
          path: nil,
          bus: 'virtio'
        }.merge(options)

        disk = {
          device: options[:device],
          type: options[:type],
          size: options[:size],
          path: options[:path],
          bus: options[:bus],
          cache: options[:cache] || 'default',
          allow_existing: options[:allow_existing],
          shareable: options[:shareable],
          serial: options[:serial],
          io: options[:io],
          copy_on_read: options[:copy_on_read],
          discard: options[:discard],
          detect_zeroes: options[:detect_zeroes],
          pool: options[:pool], # overrides storage_pool setting for additional disks
          wwn: options[:wwn],
        }

        @disks << disk # append
      end

      def qemuargs(options = {})
        @qemu_args = [] if @qemu_args == UNSET_VALUE

        @qemu_args << options if options[:value]
      end

      def qemuenv(options = {})
        @qemu_env = {} if @qemu_env == UNSET_VALUE

        @qemu_env.merge!(options)
      end

      def _default_uri
        # Determine if any settings except driver provided explicitly, if not
        # and the LIBVIRT_DEFAULT_URI var is set, use that.
        #
        # Skipping driver because that may be set on individual boxes rather
        # than by the user.
        if [
            @connect_via_ssh, @host, @username, @password,
            @id_ssh_key_file, @qemu_use_session, @socket,
        ].none?{ |v| v != UNSET_VALUE }
          if ENV.fetch('LIBVIRT_DEFAULT_URI', '') != ""
            @uri = ENV['LIBVIRT_DEFAULT_URI']
          end
        end
      end

      # code to generate URI from from either the LIBVIRT_URI environment
      # variable or a config moved out of the connect action
      def _generate_uri(qemu_use_session)
        # builds the Libvirt connection URI from the given driver config
        # Setup connection uri.
        uri = @driver.dup
        virt_path = case uri
                    when 'qemu', 'kvm'
                      qemu_use_session ? '/session' : '/system'
                    when 'openvz', 'uml', 'phyp', 'parallels'
                      '/system'
                    when '@en', 'esx'
                      '/'
                    when 'vbox', 'vmwarews', 'hyperv'
                      '/session'
                    else
                      raise "Require specify driver #{uri}"
        end
        if uri == 'kvm'
          uri = 'qemu' # use QEMU uri for KVM domain type
        end

        # turn on ssh if an ssh key file is explicitly provided
        if @connect_via_ssh == UNSET_VALUE && @id_ssh_key_file && @id_ssh_key_file != UNSET_VALUE
          @connect_via_ssh = true
        end

        params = {}

        if @connect_via_ssh == true
          finalize_id_ssh_key_file

          uri << '+ssh://'
          uri << @username + '@' if @username && @username != UNSET_VALUE

          uri << ( @host && @host != UNSET_VALUE ? @host : 'localhost' )

          params['no_verify'] = '1'
          params['keyfile'] = @id_ssh_key_file if @id_ssh_key_file
        else
          uri << '://'
          uri << @host if @host && @host != UNSET_VALUE
        end

        uri << virt_path

        # set path to Libvirt socket
        params['socket'] = @socket if @socket

        uri << "?" + params.map{|pair| pair.join('=')}.join('&') if !params.empty?
        uri
      end

      def _parse_uri(uri)
        begin
          URI.parse(uri)
        rescue
          raise "@uri set to invalid uri '#{uri}'"
        end
      end

      def finalize!
        _default_uri if @uri == UNSET_VALUE

        # settings which _generate_uri
        @driver = 'kvm' if @driver == UNSET_VALUE
        @password = nil if @password == UNSET_VALUE
        @socket = nil if @socket == UNSET_VALUE

        # If uri isn't set then let's build one from various sources.
        # Default to passing false for qemu_use_session if it's not set.
        if @uri == UNSET_VALUE
          @uri = _generate_uri(@qemu_use_session == UNSET_VALUE ? false : @qemu_use_session)
        end

        finalize_from_uri
        finalize_proxy_command

        # forward port with id 'ssh'
        @forward_ssh_port = false if @forward_ssh_port == UNSET_VALUE

        @storage_pool_name = 'default' if @storage_pool_name == UNSET_VALUE
        @snapshot_pool_name = @storage_pool_name if @snapshot_pool_name == UNSET_VALUE
        @storage_pool_path = nil if @storage_pool_path == UNSET_VALUE
        @random_hostname = false if @random_hostname == UNSET_VALUE
        @management_network_device = 'virbr0' if @management_network_device == UNSET_VALUE
        @management_network_name = 'vagrant-libvirt' if @management_network_name == UNSET_VALUE
        @management_network_address = '192.168.121.0/24' if @management_network_address == UNSET_VALUE
        @management_network_mode = 'nat' if @management_network_mode == UNSET_VALUE
        @management_network_mac = nil if @management_network_mac == UNSET_VALUE
        @management_network_guest_ipv6 = 'yes' if @management_network_guest_ipv6 == UNSET_VALUE
        @management_network_autostart = false if @management_network_autostart == UNSET_VALUE
        @management_network_pci_bus = nil if @management_network_pci_bus == UNSET_VALUE
        @management_network_pci_slot = nil if @management_network_pci_slot == UNSET_VALUE
        @management_network_domain = nil if @management_network_domain == UNSET_VALUE
        @management_network_mtu = nil if @management_network_mtu == UNSET_VALUE
        @system_uri      = 'qemu:///system' if @system_uri == UNSET_VALUE

        # Domain specific settings.
        @title = '' if @title == UNSET_VALUE
        @description = '' if @description == UNSET_VALUE
        @uuid = '' if @uuid == UNSET_VALUE
        @memory = 512 if @memory == UNSET_VALUE
        @nodeset = nil if @nodeset == UNSET_VALUE
        @memory_backing = [] if @memory_backing == UNSET_VALUE
        @cpus = 1 if @cpus == UNSET_VALUE
        @cpuset = nil if @cpuset == UNSET_VALUE
        @cpu_mode = 'host-model' if @cpu_mode == UNSET_VALUE
        @cpu_model = if (@cpu_model == UNSET_VALUE) && (@cpu_mode == 'custom')
                       'qemu64'
                     elsif @cpu_mode != 'custom'
                       ''
                     else
                       @cpu_model
          end
        @cpu_topology = {} if @cpu_topology == UNSET_VALUE
        @cpu_fallback = 'allow' if @cpu_fallback == UNSET_VALUE
        @cpu_features = [] if @cpu_features == UNSET_VALUE
        @shares = nil if @shares == UNSET_VALUE
        @features = ['acpi','apic','pae'] if @features == UNSET_VALUE
        @features_hyperv = [] if @features_hyperv == UNSET_VALUE
        @clock_offset = 'utc' if @clock_offset == UNSET_VALUE
        @clock_timers = [] if @clock_timers == UNSET_VALUE
        @numa_nodes = @numa_nodes == UNSET_VALUE ? nil : _generate_numa
        @loader = nil if @loader == UNSET_VALUE
        @nvram = nil if @nvram == UNSET_VALUE
        @machine_type = nil if @machine_type == UNSET_VALUE
        @machine_arch = nil if @machine_arch == UNSET_VALUE
        @machine_virtual_size = nil if @machine_virtual_size == UNSET_VALUE
        @disk_bus = 'virtio' if @disk_bus == UNSET_VALUE
        @disk_device = 'vda' if @disk_device == UNSET_VALUE
        @disk_driver_opts = {} if @disk_driver_opts == UNSET_VALUE
        @nic_model_type = nil if @nic_model_type == UNSET_VALUE
        @nested = false if @nested == UNSET_VALUE
        @volume_cache = nil if @volume_cache == UNSET_VALUE
        @kernel = nil if @kernel == UNSET_VALUE
        @cmd_line = '' if @cmd_line == UNSET_VALUE
        @initrd = '' if @initrd == UNSET_VALUE
        @dtb = nil if @dtb == UNSET_VALUE
        @graphics_type = 'vnc' if @graphics_type == UNSET_VALUE
        @graphics_autoport = 'yes' if @graphics_port == UNSET_VALUE
        @graphics_autoport = 'no' if @graphics_port != UNSET_VALUE
        if (@graphics_type != 'vnc' && @graphics_type != 'spice') ||
           @graphics_passwd == UNSET_VALUE
          @graphics_passwd = nil
        end
        @graphics_port = -1 if @graphics_port == UNSET_VALUE
        @graphics_ip = '127.0.0.1' if @graphics_ip == UNSET_VALUE
        @video_type = 'cirrus' if @video_type == UNSET_VALUE
        @video_vram = 9216 if @video_vram == UNSET_VALUE
        @sound_type = nil if @sound_type == UNSET_VALUE
        @keymap = 'en-us' if @keymap == UNSET_VALUE
        @kvm_hidden = false if @kvm_hidden == UNSET_VALUE
        @tpm_model = 'tpm-tis' if @tpm_model == UNSET_VALUE
        @tpm_type = 'passthrough' if @tpm_type == UNSET_VALUE
        @tpm_path = nil if @tpm_path == UNSET_VALUE
        @tpm_version = nil if @tpm_version == UNSET_VALUE
        @memballoon_enabled = nil if @memballoon_enabled == UNSET_VALUE
        @memballoon_model = 'virtio' if @memballoon_model == UNSET_VALUE
        @memballoon_pci_bus = '0x00' if @memballoon_pci_bus == UNSET_VALUE
        @memballoon_pci_slot = '0x0f' if @memballoon_pci_slot == UNSET_VALUE
        @nic_adapter_count = 8 if @nic_adapter_count == UNSET_VALUE
        @emulator_path = nil if @emulator_path == UNSET_VALUE

        # Boot order
        @boot_order = [] if @boot_order == UNSET_VALUE

        # Storage
        @disks = [] if @disks == UNSET_VALUE
        @disks.map! do |disk|
          disk[:device] = _get_device(@disks) if disk[:device].nil?
          disk
        end
        @cdroms = [] if @cdroms == UNSET_VALUE
        @cdroms.map! do |cdrom|
          cdrom[:dev] = _get_cdrom_dev(@cdroms) if cdrom[:dev].nil?
          cdrom
        end

        # Inputs
        @inputs = [{ type: 'mouse', bus: 'ps2' }] if @inputs == UNSET_VALUE

        # Channels
        @channels = [] if @channels == UNSET_VALUE

        # PCI device passthrough
        @pcis = [] if @pcis == UNSET_VALUE

        # Random number generator passthrough
        @rng = {} if @rng == UNSET_VALUE

        # Watchdog device
        @watchdog_dev = {} if @watchdog_dev == UNSET_VALUE

        # USB controller
        @usbctl_dev = {} if @usbctl_dev == UNSET_VALUE

        # USB device passthrough
        @usbs = [] if @usbs == UNSET_VALUE

        # Redirected devices
        @redirdevs = [] if @redirdevs == UNSET_VALUE
        @redirfilters = [] if @redirfilters == UNSET_VALUE

        # smartcard device
        @smartcard_dev = {} if @smartcard_dev == UNSET_VALUE

        # Suspend mode
        @suspend_mode = 'pause' if @suspend_mode == UNSET_VALUE

        # Autostart
        @autostart = false if @autostart == UNSET_VALUE

        # Attach mgmt network
        @mgmt_attach = true if @mgmt_attach == UNSET_VALUE

        # Additional QEMU commandline arguments
        @qemu_args = [] if @qemu_args == UNSET_VALUE

        # Additional QEMU commandline environment variables
        @qemu_env = {} if @qemu_env == UNSET_VALUE
      end

      def validate(machine)
        errors = _detected_errors

        # The @uri and @qemu_use_session should not conflict
        uri = _parse_uri(@uri)
        if (uri.scheme.start_with? "qemu") && (uri.path.include? "session")
          if @qemu_use_session != true
            errors << "the URI and qemu_use_session configuration conflict: uri:'#{@uri}' qemu_use_session:'#{@qemu_use_session}'"
          end
        end

        machine.provider_config.disks.each do |disk|
          if disk[:path] && (disk[:path][0] == '/')
            errors << "absolute volume paths like '#{disk[:path]}' not yet supported"
          end
        end

        machine.config.vm.networks.each do |_type, opts|
          if opts[:mac]
            opts[:mac].downcase!
            if opts[:mac] =~ /\A([0-9a-f]{12})\z/
              opts[:mac] = opts[:mac].scan(/../).join(':')
            end
            unless opts[:mac] =~ /\A([0-9a-f]{2}:){5}([0-9a-f]{2})\z/
              errors << "Configured NIC MAC '#{opts[:mac]}' is not in 'xx:xx:xx:xx:xx:xx' or 'xxxxxxxxxxxx' format"
            end
          end
        end

        if !machine.provider_config.volume_cache.nil? and machine.provider_config.volume_cache != UNSET_VALUE
          machine.ui.warn("Libvirt Provider: volume_cache is deprecated. Use disk_driver :cache => '#{machine.provider_config.volume_cache}' instead.")

          if !machine.provider_config.disk_driver_opts.empty?
            machine.ui.warn("Libvirt Provider: volume_cache has no effect when disk_driver is defined.")
          end
        end

        { 'Libvirt Provider' => errors }
      end

      def merge(other)
        super.tap do |result|
          c = disks.dup
          c += other.disks
          result.disks = c

          c = cdroms.dup
          c += other.cdroms
          result.cdroms = c

          result.disk_driver_opts = disk_driver_opts.merge(other.disk_driver_opts)
          
          c = clock_timers.dup
          c += other.clock_timers
          result.clock_timers = c

          c = qemu_env != UNSET_VALUE ? qemu_env.dup : {}
          c.merge!(other.qemu_env) if other.qemu_env != UNSET_VALUE
          result.qemu_env = c
        end
      end

      private

      def finalize_from_uri
        # Parse uri to extract individual components
        uri = _parse_uri(@uri)

        # only set @connect_via_ssh if not explicitly to avoid overriding
        # and allow an error to occur if the @uri and @connect_via_ssh disagree
        @connect_via_ssh = uri.scheme.include? "ssh" if @connect_via_ssh == UNSET_VALUE

        # Set qemu_use_session based on the URI if it wasn't set by the user
        if @qemu_use_session == UNSET_VALUE
          if (uri.scheme.start_with? "qemu") && (uri.path.include? "session")
            @qemu_use_session = true
          else
            @qemu_use_session = false
          end
        end

        # Extract host and username values from uri if provided, otherwise nil
        @host = uri.host
        @port = uri.port
        @username = uri.user
        if uri.query
          params = CGI.parse(uri.query)
          @id_ssh_key_file = params['keyfile'].first if params.has_key?('keyfile')
        end

        finalize_id_ssh_key_file
      end

      def resolve_ssh_key_file(key_file)
        # set ssh key for access to Libvirt host
        # if no slash, prepend $HOME/.ssh/
        key_file.prepend("#{ENV['HOME']}/.ssh/") if key_file && key_file !~ /\A\//

        key_file
      end

      def finalize_id_ssh_key_file
        # resolve based on the following roles
        #  1) if @connect_via_ssh is set to true, and id_ssh_key_file not current set,
        #     set default if the file exists
        #  2) if supplied the key name, attempt to expand based on user home
        #  3) otherwise set to nil

        if @connect_via_ssh == true && @id_ssh_key_file == UNSET_VALUE
          # set default if using ssh while allowing a user using nil to disable this
          id_ssh_key_file = resolve_ssh_key_file('id_rsa')
          id_ssh_key_file = nil if !File.file?(id_ssh_key_file)
        elsif @id_ssh_key_file != UNSET_VALUE
          id_ssh_key_file = resolve_ssh_key_file(@id_ssh_key_file)
        else
          id_ssh_key_file = nil
        end

        @id_ssh_key_file = id_ssh_key_file
      end

      def finalize_proxy_command
        if @connect_via_ssh
          if @proxy_command == UNSET_VALUE
            proxy_command = "ssh '#{@host}' "
            proxy_command << "-p #{@port} " if @port
            proxy_command << "-l '#{@username}' " if @username
            proxy_command << "-i '#{@id_ssh_key_file}' " if @id_ssh_key_file
            proxy_command << '-W %h:%p'
          else
            inputs = { host: @host }
            inputs << { port: @port } if @port
            inputs[:username] = @username if @username
            inputs[:id_ssh_key_file] = @id_ssh_key_file if @id_ssh_key_file

            proxy_command = @proxy_command
            # avoid needing to escape '%' symbols
            inputs.each do |key, value|
              proxy_command.gsub!("{#{key}}", value)
            end
          end

          @proxy_command = proxy_command
        else
          @proxy_command = nil
        end
      end
    end
  end
end