require 'log4r'

require "vagrant/util/platform"

require File.expand_path("../base", __FILE__)

module VagrantPlugins
  module ProviderVirtualBox
    module Driver
      # Driver for VirtualBox 5.0.x
      class Version_5_0 < Base
        def initialize(uuid)
          super()

          @logger = Log4r::Logger.new("vagrant::provider::virtualbox_5_0")
          @uuid = uuid
        end

        def clear_forwarded_ports
          retryable(on: Vagrant::Errors::VBoxManageError, tries: 3, sleep: 1) do
            args = []
            read_forwarded_ports(@uuid).each do |nic, name, _, _|
              args.concat(["--natpf#{nic}", "delete", name])
            end

            execute("modifyvm", @uuid, *args) if !args.empty?
          end
        end

        def clear_shared_folders
          retryable(on: Vagrant::Errors::VBoxManageError, tries: 3, sleep: 1) do
            info = execute("showvminfo", @uuid, "--machinereadable", retryable: true)
            info.split("\n").each do |line|
              if line =~ /^SharedFolderNameMachineMapping\d+="(.+?)"$/
                execute("sharedfolder", "remove", @uuid, "--name", $1.to_s)
              end
            end
          end
        end

        def clonevm(master_id, snapshot_name)
          machine_name = "temp_clone_#{(Time.now.to_f * 1000.0).to_i}_#{rand(100000)}"
          args = ["--register", "--name", machine_name]
          if snapshot_name
            args += ["--snapshot", snapshot_name, "--options", "link"]
          end

          execute("clonevm", master_id, *args, retryable: true)
          return get_machine_id(machine_name)
        end

        def create_dhcp_server(network, options)
          retryable(on: Vagrant::Errors::VBoxManageError, tries: 3, sleep: 1) do
            begin
              execute("dhcpserver", "add", "--ifname", network,
                      "--ip", options[:dhcp_ip],
                      "--netmask", options[:netmask],
                      "--lowerip", options[:dhcp_lower],
                      "--upperip", options[:dhcp_upper],
                      "--enable")
            rescue Vagrant::Errors::VBoxManageError => e
              return if e.extra_data[:stderr] == 'VBoxManage: error: DHCP server already exists'
              raise
            end
          end
        end

        def create_host_only_network(options)
          # Create the interface
          execute("hostonlyif", "create", retryable: true) =~ /^Interface '(.+?)' was successfully created$/
          name = $1.to_s

          # Get the IP so we can determine v4 vs v6
          ip = IPAddr.new(options[:adapter_ip])

          # Configure
          if ip.ipv4?
            execute("hostonlyif", "ipconfig", name,
                    "--ip", options[:adapter_ip],
                    "--netmask", options[:netmask],
                    retryable: true)
          elsif ip.ipv6?
            execute("hostonlyif", "ipconfig", name,
                    "--ipv6", options[:adapter_ip],
                    "--netmasklengthv6", options[:netmask].to_s,
                    retryable: true)
          else
            raise "BUG: Unknown IP type: #{ip.inspect}"
          end

          # Return the details
          return {
            name: name,
            ip:   options[:adapter_ip],
            netmask: options[:netmask],
            dhcp: nil
          }
        end

        def create_snapshot(machine_id, snapshot_name)
          execute("snapshot", machine_id, "take", snapshot_name, retryable: true)
        end

        def delete_snapshot(machine_id, snapshot_name)
          # Start with 0%
          last = 0
          total = ""
          yield 0 if block_given?

          # Snapshot and report the % progress
          execute("snapshot", machine_id, "delete", snapshot_name, retryable: true) do |type, data|
            if type == :stderr
              # Append the data so we can see the full view
              total << data.gsub("\r", "")

              # Break up the lines. We can't get the progress until we see an "OK"
              lines = total.split("\n")

              # The progress of the import will be in the last line. Do a greedy
              # regular expression to find what we're looking for.
              match = /.+(\d{2})%/.match(lines.last)
              if match
                current = match[1].to_i
                if current > last
                  last = current
                  yield current if block_given?
                end
              end
            end
          end
        end

        def list_snapshots(machine_id)
          output = execute(
            "snapshot", machine_id, "list", "--machinereadable",
            retryable: true)

          result = []
          output.split("\n").each do |line|
            if line =~ /^SnapshotName.*?="(.+?)"$/i
              result << $1.to_s
            end
          end

          result.sort
        rescue Vagrant::Errors::VBoxManageError => e
          d = e.extra_data
          return [] if d[:stderr].include?("does not have") || d[:stdout].include?("does not have")
          raise
        end

        def restore_snapshot(machine_id, snapshot_name)
          # Start with 0%
          last = 0
          total = ""
          yield 0 if block_given?

          execute("snapshot", machine_id, "restore", snapshot_name, retryable: true) do |type, data|
            if type == :stderr
              # Append the data so we can see the full view
              total << data.gsub("\r", "")

              # Break up the lines. We can't get the progress until we see an "OK"
              lines = total.split("\n")

              # The progress of the import will be in the last line. Do a greedy
              # regular expression to find what we're looking for.
              match = /.+(\d{2})%/.match(lines.last)
              if match
                current = match[1].to_i
                if current > last
                  last = current
                  yield current if block_given?
                end
              end
            end
          end
        end

        def delete
          execute("unregistervm", @uuid, "--delete", retryable: true)
        end

        def delete_unused_host_only_networks
          networks = []
          execute("list", "hostonlyifs", retryable: true).split("\n").each do |line|
            networks << $1.to_s if line =~ /^Name:\s+(.+?)$/
          end

          execute("list", "vms", retryable: true).split("\n").each do |line|
            if line =~ /^".+?"\s+\{(.+?)\}$/
              begin
                info = execute("showvminfo", $1.to_s, "--machinereadable", retryable: true)
                info.split("\n").each do |inner_line|
                  if inner_line =~ /^hostonlyadapter\d+="(.+?)"$/
                    networks.delete($1.to_s)
                  end
                end
              rescue Vagrant::Errors::VBoxManageError => e
                raise if !e.extra_data[:stderr].include?("VBOX_E_OBJECT_NOT_FOUND")

                # VirtualBox could not find the vm. It may have been deleted
                # by another process after we called 'vboxmanage list vms'? Ignore this error.
              end
            end
          end

          networks.each do |name|
            # First try to remove any DHCP servers attached. We use `raw` because
            # it is okay if this fails. It usually means that a DHCP server was
            # never attached.
            raw("dhcpserver", "remove", "--ifname", name)

            # Delete the actual host only network interface.
            execute("hostonlyif", "remove", name, retryable: true)
          end
        end

        def discard_saved_state
          execute("discardstate", @uuid, retryable: true)
        end

        def enable_adapters(adapters)
          args = []
          adapters.each do |adapter|
            args.concat(["--nic#{adapter[:adapter]}", adapter[:type].to_s])

            if adapter[:bridge]
              args.concat(["--bridgeadapter#{adapter[:adapter]}",
                          adapter[:bridge], "--cableconnected#{adapter[:adapter]}", "on"])
            end

            if adapter[:hostonly]
              args.concat(["--hostonlyadapter#{adapter[:adapter]}",
                          adapter[:hostonly], "--cableconnected#{adapter[:adapter]}", "on"])
            end

            if adapter[:intnet]
              args.concat(["--intnet#{adapter[:adapter]}",
                          adapter[:intnet], "--cableconnected#{adapter[:adapter]}", "on"])
            end

            if adapter[:mac_address]
              args.concat(["--macaddress#{adapter[:adapter]}",
                          adapter[:mac_address]])
            end

            if adapter[:nic_type]
              args.concat(["--nictype#{adapter[:adapter]}", adapter[:nic_type].to_s])
            end
          end

          execute("modifyvm", @uuid, *args, retryable: true)
        end

        def execute_command(command)
          execute(*command)
        end

        def export(path)
          retryable(on: Vagrant::Errors::VBoxManageError, tries: 3, sleep: 1) do
            begin
              execute("export", @uuid, "--output", path.to_s)
            rescue Vagrant::Errors::VBoxManageError => e
              raise if !e.extra_data[:stderr].include?("VERR_E_FILE_ERROR")

              # If the file already exists we'll throw a custom error
              raise Vagrant::Errors::VirtualBoxFileExists,
                stderr: e.extra_data[:stderr]
            end
          end
        end

        def forward_ports(ports)
          args = []
          ports.each do |options|
            pf_builder = [options[:name],
              options[:protocol] || "tcp",
              options[:hostip] || "",
              options[:hostport],
              options[:guestip] || "",
              options[:guestport]]

            args.concat(["--natpf#{options[:adapter] || 1}",
                        pf_builder.join(",")])
          end

          execute("modifyvm", @uuid, *args, retryable: true) if !args.empty?
        end

        def get_machine_id(machine_name)
          output = execute("list", "vms", retryable: true)
          match = /^"#{Regexp.escape(machine_name)}" \{(.+?)\}$/.match(output)
          return match[1].to_s if match
          nil
        end

        def halt
          execute("controlvm", @uuid, "poweroff", retryable: true)
        end

        def import(ovf)
          ovf = Vagrant::Util::Platform.windows_path(ovf)

          output = ""
          total = ""
          last  = 0

          # Dry-run the import to get the suggested name and path
          @logger.debug("Doing dry-run import to determine parallel-safe name...")
          output = execute("import", "-n", ovf)
          result = /Suggested VM name "(.+?)"/.match(output)
          if !result
            raise Vagrant::Errors::VirtualBoxNoName, output: output
          end
          suggested_name = result[1].to_s

          # Append millisecond plus a random to the path in case we're
          # importing the same box elsewhere.
          specified_name = "#{suggested_name}_#{(Time.now.to_f * 1000.0).to_i}_#{rand(100000)}"
          @logger.debug("-- Parallel safe name: #{specified_name}")

          # Build the specified name param list
          name_params = [
            "--vsys", "0",
            "--vmname", specified_name,
          ]

          # Extract the disks list and build the disk target params
          disk_params = []
          disks = output.scan(/(\d+): Hard disk image: source image=.+, target path=(.+),/)
          disks.each do |unit_num, path|
            disk_params << "--vsys"
            disk_params << "0"
            disk_params << "--unit"
            disk_params << unit_num
            disk_params << "--disk"
            if Vagrant::Util::Platform.windows?
              # we use the block form of sub here to ensure that if the specified_name happens to end with a number (which is fairly likely) then
              # we won't end up having the character sequence of a \ followed by a number be interpreted as a back reference.  For example, if
              # specified_name were "abc123", then "\\abc123\\".reverse would be "\\321cba\\", and the \3 would be treated as a back reference by the sub
              disk_params << path.reverse.sub("\\#{suggested_name}\\".reverse) { "\\#{specified_name}\\".reverse }.reverse # Replace only last occurrence
            else
              disk_params << path.reverse.sub("/#{suggested_name}/".reverse, "/#{specified_name}/".reverse).reverse # Replace only last occurrence
            end
          end

          execute("import", ovf , *name_params, *disk_params, retryable: true) do |type, data|
            if type == :stdout
              # Keep track of the stdout so that we can get the VM name
              output << data
            elsif type == :stderr
              # Append the data so we can see the full view
              total << data.gsub("\r", "")

              # Break up the lines. We can't get the progress until we see an "OK"
              lines = total.split("\n")
              if lines.include?("OK.")
                # The progress of the import will be in the last line. Do a greedy
                # regular expression to find what we're looking for.
                match = /.+(\d{2})%/.match(lines.last)
                if match
                  current = match[1].to_i
                  if current > last
                    last = current
                    yield current if block_given?
                  end
                end
              end
            end
          end

          return get_machine_id specified_name
        end

        def max_network_adapters
          8
        end

        def read_forwarded_ports(uuid=nil, active_only=false)
          uuid ||= @uuid

          @logger.debug("read_forward_ports: uuid=#{uuid} active_only=#{active_only}")

          results = []
          current_nic = nil
          info = execute("showvminfo", uuid, "--machinereadable", retryable: true)
          info.split("\n").each do |line|
            # This is how we find the nic that a FP is attached to,
            # since this comes first.
            current_nic = $1.to_i if line =~ /^nic(\d+)=".+?"$/

              # If we care about active VMs only, then we check the state
              # to verify the VM is running.
              if active_only && line =~ /^VMState="(.+?)"$/ && $1.to_s != "running"
                return []
              end

            # Parse out the forwarded port information
            # Forwarding(1)="172.22.8.201tcp32977,tcp,172.22.8.201,32977,,3777"
            # Forwarding(2)="tcp32978,tcp,,32978,,3777"
            if line =~ /^Forwarding.+?="(.+?),.+?,(.*?),(.+?),.*?,(.+?)"$/
              result = [current_nic, $1.to_s, $3.to_i, $4.to_i, $2]
              @logger.debug("  - #{result.inspect}")
              results << result
            end
          end

          results
        end

        def read_bridged_interfaces
          execute("list", "bridgedifs").split("\n\n").collect do |block|
            info = {}

            block.split("\n").each do |line|
              if line =~ /^Name:\s+(.+?)$/
                info[:name] = $1.to_s
              elsif line =~ /^IPAddress:\s+(.+?)$/
                info[:ip] = $1.to_s
              elsif line =~ /^NetworkMask:\s+(.+?)$/
                info[:netmask] = $1.to_s
              elsif line =~ /^Status:\s+(.+?)$/
                info[:status] = $1.to_s
              end
            end

            # Return the info to build up the results
            info
          end
        end

        def read_dhcp_servers
          execute("list", "dhcpservers", retryable: true).split("\n\n").collect do |block|
            info = {}

            block.split("\n").each do |line|
              if network = line[/^NetworkName:\s+HostInterfaceNetworking-(.+?)$/, 1]
                info[:network]      = network
                info[:network_name] = "HostInterfaceNetworking-#{network}"
              elsif ip = line[/^IP:\s+(.+?)$/, 1]
                info[:ip] = ip
              elsif netmask = line[/^NetworkMask:\s+(.+?)$/, 1]
                info[:netmask] = netmask
              elsif lower = line[/^lowerIPAddress:\s+(.+?)$/, 1]
                info[:lower] = lower
              elsif upper = line[/^upperIPAddress:\s+(.+?)$/, 1]
                info[:upper] = upper
              end
            end

            info
          end
        end

        def read_guest_additions_version
          output = execute("guestproperty", "get", @uuid, "/VirtualBox/GuestAdd/Version",
                           retryable: true)
          if output =~ /^Value: (.+?)$/
            # Split the version by _ since some distro versions modify it
            # to look like this: 4.1.2_ubuntu, and the distro part isn't
            # too important.
            value = $1.to_s
            return value.split("_").first
          end

          # If we can't get the guest additions version by guest property, try
          # to get it from the VM info itself.
          info = execute("showvminfo", @uuid, "--machinereadable", retryable: true)
          info.split("\n").each do |line|
            return $1.to_s if line =~ /^GuestAdditionsVersion="(.+?)"$/
          end

          return nil
        end

        def read_guest_ip(adapter_number)
          ip = read_guest_property("/VirtualBox/GuestInfo/Net/#{adapter_number}/V4/IP")
          if !valid_ip_address?(ip)
            raise Vagrant::Errors::VirtualBoxGuestPropertyNotFound,
              guest_property: "/VirtualBox/GuestInfo/Net/#{adapter_number}/V4/IP"
          end

          return ip
        end

        def read_guest_property(property)
          output = execute("guestproperty", "get", @uuid, property)
          if output =~ /^Value: (.+?)$/
            $1.to_s
          else
            raise Vagrant::Errors::VirtualBoxGuestPropertyNotFound, guest_property: property
          end
        end

        def read_host_only_interfaces
          execute("list", "hostonlyifs", retryable: true).split("\n\n").collect do |block|
            info = {}

            block.split("\n").each do |line|
              if line =~ /^Name:\s+(.+?)$/
                info[:name] = $1.to_s
              elsif line =~ /^IPAddress:\s+(.+?)$/
                info[:ip] = $1.to_s
              elsif line =~ /^NetworkMask:\s+(.+?)$/
                info[:netmask] = $1.to_s
              elsif line =~ /^IPV6Address:\s+(.+?)$/
                info[:ipv6] = $1.to_s.strip
              elsif line =~ /^IPV6NetworkMaskPrefixLength:\s+(.+?)$/
                info[:ipv6_prefix] = $1.to_s.strip
              elsif line =~ /^Status:\s+(.+?)$/
                info[:status] = $1.to_s
              end
            end

            info
          end
        end

        def read_mac_address
          info = execute("showvminfo", @uuid, "--machinereadable", retryable: true)
          info.split("\n").each do |line|
            return $1.to_s if line =~ /^macaddress1="(.+?)"$/
          end

          nil
        end

        def read_mac_addresses
          macs = {}
          info = execute("showvminfo", @uuid, "--machinereadable", retryable: true)
          info.split("\n").each do |line|
            if matcher = /^macaddress(\d+)="(.+?)"$/.match(line)
              adapter = matcher[1].to_i
              mac = matcher[2].to_s
              macs[adapter] = mac
            end
          end
          macs
        end

        def read_machine_folder
          execute("list", "systemproperties", retryable: true).split("\n").each do |line|
            if line =~ /^Default machine folder:\s+(.+?)$/i
              return $1.to_s
            end
          end

          nil
        end

        def read_network_interfaces
          nics = {}
          info = execute("showvminfo", @uuid, "--machinereadable", retryable: true)
          info.split("\n").each do |line|
            if line =~ /^nic(\d+)="(.+?)"$/
              adapter = $1.to_i
              type    = $2.to_sym

              nics[adapter] ||= {}
              nics[adapter][:type] = type
            elsif line =~ /^hostonlyadapter(\d+)="(.+?)"$/
              adapter = $1.to_i
              network = $2.to_s

              nics[adapter] ||= {}
              nics[adapter][:hostonly] = network
            elsif line =~ /^bridgeadapter(\d+)="(.+?)"$/
              adapter = $1.to_i
              network = $2.to_s

              nics[adapter] ||= {}
              nics[adapter][:bridge] = network
            end
          end

          nics
        end

        def read_state
          output = execute("showvminfo", @uuid, "--machinereadable", retryable: true)
          if output =~ /^name="<inaccessible>"$/
            return :inaccessible
          elsif output =~ /^VMState="(.+?)"$/
            return $1.to_sym
          end

          nil
        end

        def read_used_ports
          used_ports = Hash.new{|hash, key| hash[key] = Set.new}
          execute("list", "vms", retryable: true).split("\n").each do |line|
            if line =~ /^".+?" \{(.+?)\}$/
              uuid = $1.to_s

              # Ignore our own used ports
              next if uuid == @uuid

              begin
                read_forwarded_ports(uuid, true).each do |_, _, hostport, _, hostip|
                  hostip = '*' if hostip.nil? || hostip.empty?
                  used_ports[hostport].add?(hostip)
                end
              rescue Vagrant::Errors::VBoxManageError => e
                raise if !e.extra_data[:stderr].include?("VBOX_E_OBJECT_NOT_FOUND")

                # VirtualBox could not find the vm. It may have been deleted
                # by another process after we called 'vboxmanage list vms'? Ignore this error.
              end
            end
          end

          used_ports
        end

        def read_vms
          results = {}
          execute("list", "vms", retryable: true).split("\n").each do |line|
            if line =~ /^"(.+?)" \{(.+?)\}$/
              results[$1.to_s] = $2.to_s
            end
          end

          results
        end

        def reconfig_host_only(interface)
          execute("hostonlyif", "ipconfig", interface[:name],
                  "--ipv6", interface[:ipv6], retryable: true)
        end

        def remove_dhcp_server(network_name)
          execute("dhcpserver", "remove", "--netname", network_name, retryable: true)
        end

        def set_mac_address(mac)
          mac = "auto" if !mac
          execute("modifyvm", @uuid, "--macaddress1", mac, retryable: true)
        end

        def set_name(name)
          retryable(on: Vagrant::Errors::VBoxManageError, tries: 3, sleep: 1) do
            begin
              execute("modifyvm", @uuid, "--name", name)
            rescue Vagrant::Errors::VBoxManageError => e
              raise if !e.extra_data[:stderr].include?("VERR_ALREADY_EXISTS")

              # We got VERR_ALREADY_EXISTS. This means that we're renaming to
              # a VM name that already exists. Raise a custom error.
              raise Vagrant::Errors::VirtualBoxNameExists,
                stderr: e.extra_data[:stderr]
            end
          end
        end

        def share_folders(folders)
          is_solaris = begin
                         "SunOS" == read_guest_property("/VirtualBox/GuestInfo/OS/Product")
                       rescue
                         false
                       end
          folders.each do |folder|
            # NOTE: Guest additions on Solaris guests do not properly handle
            # UNC style paths so prevent conversion (See GH-7264)
            if is_solaris
              hostpath = folder[:hostpath]
            else
              hostpath = Vagrant::Util::Platform.windows_path(folder[:hostpath])
            end
            args = ["--name",
              folder[:name],
              "--hostpath",
              hostpath]
            args << "--transient" if folder.key?(:transient) && folder[:transient]

            args << "--automount" if folder.key?(:automount) && folder[:automount]

            if folder[:SharedFoldersEnableSymlinksCreate]
              # Enable symlinks on the shared folder
              execute("setextradata", @uuid, "VBoxInternal2/SharedFoldersEnableSymlinksCreate/#{folder[:name]}", "1", retryable: true)
            end

            # Add the shared folder
            execute("sharedfolder", "add", @uuid, *args, retryable: true)
          end
        end

        def ssh_port(expected_port)
          @logger.debug("Searching for SSH port: #{expected_port.inspect}")

          # Look for the forwarded port only by comparing the guest port
          read_forwarded_ports.each do |_, _, hostport, guestport|
            return hostport if guestport == expected_port
          end

          nil
        end

        def resume
          @logger.debug("Resuming paused VM...")
          execute("controlvm", @uuid, "resume")
        end

        def start(mode)
          command = ["startvm", @uuid, "--type", mode.to_s]
          retryable(on: Vagrant::Errors::VBoxManageError, tries: 3, sleep: 1) do
            r = raw(*command)

            if r.exit_code == 0 || r.stdout =~ /VM ".+?" has been successfully started/
              # Some systems return an exit code 1 for some reason. For that
              # we depend on the output.
              return true
            end

            # If we reached this point then it didn't work out.
            raise Vagrant::Errors::VBoxManageError,
              command: command.inspect,
              stderr: r.stderr
          end
        end

        def suspend
          execute("controlvm", @uuid, "savestate", retryable: true)
        end

        def unshare_folders(names)
          names.each do |name|
            retryable(on: Vagrant::Errors::VBoxManageError, tries: 3, sleep: 1) do
              begin
                execute(
                  "sharedfolder", "remove", @uuid,
                  "--name", name,
                  "--transient")

                execute(
                  "setextradata", @uuid,
                  "VBoxInternal2/SharedFoldersEnableSymlinksCreate/#{name}")
              rescue Vagrant::Errors::VBoxManageError => e
                raise if !e.extra_data[:stderr].include?("VBOX_E_FILE_ERROR")
              end
            end
          end
        end

        def verify!
          # This command sometimes fails if kernel drivers aren't properly loaded
          # so we just run the command and verify that it succeeded.
          execute("list", "hostonlyifs", retryable: true)
        end

        def verify_image(path)
          r = raw("import", path.to_s, "--dry-run")
          return r.exit_code == 0
        end

        def vm_exists?(uuid)
          5.times do |i|
            result = raw("showvminfo", uuid)
            return true if result.exit_code == 0

            # If vboxmanage returned VBOX_E_OBJECT_NOT_FOUND,
            # then the vm truly does not exist. Any other error might be transient
            return false if result.stderr.include?("VBOX_E_OBJECT_NOT_FOUND")

            # Sleep a bit though to give VirtualBox time to fix itself
            sleep 2
          end

          # If we reach this point, it means that we consistently got the
          # failure, do a standard vboxmanage now. This will raise an
          # exception if it fails again.
          execute("showvminfo", uuid)
          return true
        end

        protected

        def valid_ip_address?(ip)
          # Filter out invalid IP addresses
          # GH-4658 VirtualBox can report an IP address of 0.0.0.0 for FreeBSD guests.
          if ip == "0.0.0.0"
            return false
          else
            return true
          end
        end
      end
    end
  end
end
