/*
Copyright (c) 2015-2017 VMware, Inc. All Rights Reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package object

import (
	"errors"
	"fmt"
	"path/filepath"
	"reflect"
	"regexp"
	"sort"
	"strings"

	"github.com/vmware/govmomi/vim25/types"
)

// Type values for use in BootOrder
const (
	DeviceTypeNone     = "-"
	DeviceTypeCdrom    = "cdrom"
	DeviceTypeDisk     = "disk"
	DeviceTypeEthernet = "ethernet"
	DeviceTypeFloppy   = "floppy"
)

// VirtualDeviceList provides helper methods for working with a list of virtual devices.
type VirtualDeviceList []types.BaseVirtualDevice

// SCSIControllerTypes are used for adding a new SCSI controller to a VM.
func SCSIControllerTypes() VirtualDeviceList {
	// Return a mutable list of SCSI controller types, initialized with defaults.
	return VirtualDeviceList([]types.BaseVirtualDevice{
		&types.VirtualLsiLogicController{},
		&types.VirtualBusLogicController{},
		&types.ParaVirtualSCSIController{},
		&types.VirtualLsiLogicSASController{},
	}).Select(func(device types.BaseVirtualDevice) bool {
		c := device.(types.BaseVirtualSCSIController).GetVirtualSCSIController()
		c.SharedBus = types.VirtualSCSISharingNoSharing
		c.BusNumber = -1
		return true
	})
}

// EthernetCardTypes are used for adding a new ethernet card to a VM.
func EthernetCardTypes() VirtualDeviceList {
	return VirtualDeviceList([]types.BaseVirtualDevice{
		&types.VirtualE1000{},
		&types.VirtualE1000e{},
		&types.VirtualVmxnet2{},
		&types.VirtualVmxnet3{},
		&types.VirtualPCNet32{},
		&types.VirtualSriovEthernetCard{},
	}).Select(func(device types.BaseVirtualDevice) bool {
		c := device.(types.BaseVirtualEthernetCard).GetVirtualEthernetCard()
		c.GetVirtualDevice().Key = -1
		return true
	})
}

// Select returns a new list containing all elements of the list for which the given func returns true.
func (l VirtualDeviceList) Select(f func(device types.BaseVirtualDevice) bool) VirtualDeviceList {
	var found VirtualDeviceList

	for _, device := range l {
		if f(device) {
			found = append(found, device)
		}
	}

	return found
}

// SelectByType returns a new list with devices that are equal to or extend the given type.
func (l VirtualDeviceList) SelectByType(deviceType types.BaseVirtualDevice) VirtualDeviceList {
	dtype := reflect.TypeOf(deviceType)
	if dtype == nil {
		return nil
	}
	dname := dtype.Elem().Name()

	return l.Select(func(device types.BaseVirtualDevice) bool {
		t := reflect.TypeOf(device)

		if t == dtype {
			return true
		}

		_, ok := t.Elem().FieldByName(dname)

		return ok
	})
}

// SelectByBackingInfo returns a new list with devices matching the given backing info.
// If the value of backing is nil, any device with a backing of the same type will be returned.
func (l VirtualDeviceList) SelectByBackingInfo(backing types.BaseVirtualDeviceBackingInfo) VirtualDeviceList {
	t := reflect.TypeOf(backing)

	return l.Select(func(device types.BaseVirtualDevice) bool {
		db := device.GetVirtualDevice().Backing
		if db == nil {
			return false
		}

		if reflect.TypeOf(db) != t {
			return false
		}

		if reflect.ValueOf(backing).IsNil() {
			// selecting by backing type
			return true
		}

		switch a := db.(type) {
		case *types.VirtualEthernetCardNetworkBackingInfo:
			b := backing.(*types.VirtualEthernetCardNetworkBackingInfo)
			return a.DeviceName == b.DeviceName
		case *types.VirtualEthernetCardDistributedVirtualPortBackingInfo:
			b := backing.(*types.VirtualEthernetCardDistributedVirtualPortBackingInfo)
			return a.Port.SwitchUuid == b.Port.SwitchUuid &&
				a.Port.PortgroupKey == b.Port.PortgroupKey
		case *types.VirtualDiskFlatVer2BackingInfo:
			b := backing.(*types.VirtualDiskFlatVer2BackingInfo)
			if a.Parent != nil && b.Parent != nil {
				return a.Parent.FileName == b.Parent.FileName
			}
			return a.FileName == b.FileName
		case *types.VirtualSerialPortURIBackingInfo:
			b := backing.(*types.VirtualSerialPortURIBackingInfo)
			return a.ServiceURI == b.ServiceURI
		case types.BaseVirtualDeviceFileBackingInfo:
			b := backing.(types.BaseVirtualDeviceFileBackingInfo)
			return a.GetVirtualDeviceFileBackingInfo().FileName == b.GetVirtualDeviceFileBackingInfo().FileName
		default:
			return false
		}
	})
}

// Find returns the device matching the given name.
func (l VirtualDeviceList) Find(name string) types.BaseVirtualDevice {
	for _, device := range l {
		if l.Name(device) == name {
			return device
		}
	}
	return nil
}

// FindByKey returns the device matching the given key.
func (l VirtualDeviceList) FindByKey(key int32) types.BaseVirtualDevice {
	for _, device := range l {
		if device.GetVirtualDevice().Key == key {
			return device
		}
	}
	return nil
}

// FindIDEController will find the named IDE controller if given, otherwise will pick an available controller.
// An error is returned if the named controller is not found or not an IDE controller.  Or, if name is not
// given and no available controller can be found.
func (l VirtualDeviceList) FindIDEController(name string) (*types.VirtualIDEController, error) {
	if name != "" {
		d := l.Find(name)
		if d == nil {
			return nil, fmt.Errorf("device '%s' not found", name)
		}
		if c, ok := d.(*types.VirtualIDEController); ok {
			return c, nil
		}
		return nil, fmt.Errorf("%s is not an IDE controller", name)
	}

	c := l.PickController((*types.VirtualIDEController)(nil))
	if c == nil {
		return nil, errors.New("no available IDE controller")
	}

	return c.(*types.VirtualIDEController), nil
}

// CreateIDEController creates a new IDE controller.
func (l VirtualDeviceList) CreateIDEController() (types.BaseVirtualDevice, error) {
	ide := &types.VirtualIDEController{}
	ide.Key = l.NewKey()
	return ide, nil
}

// FindSCSIController will find the named SCSI controller if given, otherwise will pick an available controller.
// An error is returned if the named controller is not found or not an SCSI controller.  Or, if name is not
// given and no available controller can be found.
func (l VirtualDeviceList) FindSCSIController(name string) (*types.VirtualSCSIController, error) {
	if name != "" {
		d := l.Find(name)
		if d == nil {
			return nil, fmt.Errorf("device '%s' not found", name)
		}
		if c, ok := d.(types.BaseVirtualSCSIController); ok {
			return c.GetVirtualSCSIController(), nil
		}
		return nil, fmt.Errorf("%s is not an SCSI controller", name)
	}

	c := l.PickController((*types.VirtualSCSIController)(nil))
	if c == nil {
		return nil, errors.New("no available SCSI controller")
	}

	return c.(types.BaseVirtualSCSIController).GetVirtualSCSIController(), nil
}

// CreateSCSIController creates a new SCSI controller of type name if given, otherwise defaults to lsilogic.
func (l VirtualDeviceList) CreateSCSIController(name string) (types.BaseVirtualDevice, error) {
	ctypes := SCSIControllerTypes()

	if name == "" || name == "scsi" {
		name = ctypes.Type(ctypes[0])
	} else if name == "virtualscsi" {
		name = "pvscsi" // ovf VirtualSCSI mapping
	}

	found := ctypes.Select(func(device types.BaseVirtualDevice) bool {
		return l.Type(device) == name
	})

	if len(found) == 0 {
		return nil, fmt.Errorf("unknown SCSI controller type '%s'", name)
	}

	c, ok := found[0].(types.BaseVirtualSCSIController)
	if !ok {
		return nil, fmt.Errorf("invalid SCSI controller type '%s'", name)
	}

	scsi := c.GetVirtualSCSIController()
	scsi.BusNumber = l.newSCSIBusNumber()
	scsi.Key = l.NewKey()
	scsi.ScsiCtlrUnitNumber = 7
	return c.(types.BaseVirtualDevice), nil
}

var scsiBusNumbers = []int{0, 1, 2, 3}

// newSCSIBusNumber returns the bus number to use for adding a new SCSI bus device.
// -1 is returned if there are no bus numbers available.
func (l VirtualDeviceList) newSCSIBusNumber() int32 {
	var used []int

	for _, d := range l.SelectByType((*types.VirtualSCSIController)(nil)) {
		num := d.(types.BaseVirtualSCSIController).GetVirtualSCSIController().BusNumber
		if num >= 0 {
			used = append(used, int(num))
		} // else caller is creating a new vm using SCSIControllerTypes
	}

	sort.Ints(used)

	for i, n := range scsiBusNumbers {
		if i == len(used) || n != used[i] {
			return int32(n)
		}
	}

	return -1
}

// FindNVMEController will find the named NVME controller if given, otherwise will pick an available controller.
// An error is returned if the named controller is not found or not an NVME controller.  Or, if name is not
// given and no available controller can be found.
func (l VirtualDeviceList) FindNVMEController(name string) (*types.VirtualNVMEController, error) {
	if name != "" {
		d := l.Find(name)
		if d == nil {
			return nil, fmt.Errorf("device '%s' not found", name)
		}
		if c, ok := d.(*types.VirtualNVMEController); ok {
			return c, nil
		}
		return nil, fmt.Errorf("%s is not an NVME controller", name)
	}

	c := l.PickController((*types.VirtualNVMEController)(nil))
	if c == nil {
		return nil, errors.New("no available NVME controller")
	}

	return c.(*types.VirtualNVMEController), nil
}

// CreateNVMEController creates a new NVMWE controller.
func (l VirtualDeviceList) CreateNVMEController() (types.BaseVirtualDevice, error) {
	nvme := &types.VirtualNVMEController{}
	nvme.BusNumber = l.newNVMEBusNumber()
	nvme.Key = l.NewKey()

	return nvme, nil
}

var nvmeBusNumbers = []int{0, 1, 2, 3}

// newNVMEBusNumber returns the bus number to use for adding a new NVME bus device.
// -1 is returned if there are no bus numbers available.
func (l VirtualDeviceList) newNVMEBusNumber() int32 {
	var used []int

	for _, d := range l.SelectByType((*types.VirtualNVMEController)(nil)) {
		num := d.(types.BaseVirtualController).GetVirtualController().BusNumber
		if num >= 0 {
			used = append(used, int(num))
		} // else caller is creating a new vm using NVMEControllerTypes
	}

	sort.Ints(used)

	for i, n := range nvmeBusNumbers {
		if i == len(used) || n != used[i] {
			return int32(n)
		}
	}

	return -1
}

// FindDiskController will find an existing ide or scsi disk controller.
func (l VirtualDeviceList) FindDiskController(name string) (types.BaseVirtualController, error) {
	switch {
	case name == "ide":
		return l.FindIDEController("")
	case name == "scsi" || name == "":
		return l.FindSCSIController("")
	case name == "nvme":
		return l.FindNVMEController("")
	default:
		if c, ok := l.Find(name).(types.BaseVirtualController); ok {
			return c, nil
		}
		return nil, fmt.Errorf("%s is not a valid controller", name)
	}
}

// PickController returns a controller of the given type(s).
// If no controllers are found or have no available slots, then nil is returned.
func (l VirtualDeviceList) PickController(kind types.BaseVirtualController) types.BaseVirtualController {
	l = l.SelectByType(kind.(types.BaseVirtualDevice)).Select(func(device types.BaseVirtualDevice) bool {
		num := len(device.(types.BaseVirtualController).GetVirtualController().Device)

		switch device.(type) {
		case types.BaseVirtualSCSIController:
			return num < 15
		case *types.VirtualIDEController:
			return num < 2
		case *types.VirtualNVMEController:
			return num < 8
		default:
			return true
		}
	})

	if len(l) == 0 {
		return nil
	}

	return l[0].(types.BaseVirtualController)
}

// newUnitNumber returns the unit number to use for attaching a new device to the given controller.
func (l VirtualDeviceList) newUnitNumber(c types.BaseVirtualController) int32 {
	units := make([]bool, 30)

	switch sc := c.(type) {
	case types.BaseVirtualSCSIController:
		//  The SCSI controller sits on its own bus
		units[sc.GetVirtualSCSIController().ScsiCtlrUnitNumber] = true
	}

	key := c.GetVirtualController().Key

	for _, device := range l {
		d := device.GetVirtualDevice()

		if d.ControllerKey == key && d.UnitNumber != nil {
			units[int(*d.UnitNumber)] = true
		}
	}

	for unit, used := range units {
		if !used {
			return int32(unit)
		}
	}

	return -1
}

// NewKey returns the key to use for adding a new device to the device list.
// The device list we're working with here may not be complete (e.g. when
// we're only adding new devices), so any positive keys could conflict with device keys
// that are already in use. To avoid this type of conflict, we can use negative keys
// here, which will be resolved to positive keys by vSphere as the reconfiguration is done.
func (l VirtualDeviceList) NewKey() int32 {
	var key int32 = -200

	for _, device := range l {
		d := device.GetVirtualDevice()
		if d.Key < key {
			key = d.Key
		}
	}

	return key - 1
}

// AssignController assigns a device to a controller.
func (l VirtualDeviceList) AssignController(device types.BaseVirtualDevice, c types.BaseVirtualController) {
	d := device.GetVirtualDevice()
	d.ControllerKey = c.GetVirtualController().Key
	d.UnitNumber = new(int32)
	*d.UnitNumber = l.newUnitNumber(c)
	if d.Key == 0 {
		d.Key = -1
	}
}

// CreateDisk creates a new VirtualDisk device which can be added to a VM.
func (l VirtualDeviceList) CreateDisk(c types.BaseVirtualController, ds types.ManagedObjectReference, name string) *types.VirtualDisk {
	// If name is not specified, one will be chosen for you.
	// But if when given, make sure it ends in .vmdk, otherwise it will be treated as a directory.
	if len(name) > 0 && filepath.Ext(name) != ".vmdk" {
		name += ".vmdk"
	}

	device := &types.VirtualDisk{
		VirtualDevice: types.VirtualDevice{
			Backing: &types.VirtualDiskFlatVer2BackingInfo{
				DiskMode:        string(types.VirtualDiskModePersistent),
				ThinProvisioned: types.NewBool(true),
				VirtualDeviceFileBackingInfo: types.VirtualDeviceFileBackingInfo{
					FileName:  name,
					Datastore: &ds,
				},
			},
		},
	}

	l.AssignController(device, c)
	return device
}

// ChildDisk creates a new VirtualDisk device, linked to the given parent disk, which can be added to a VM.
func (l VirtualDeviceList) ChildDisk(parent *types.VirtualDisk) *types.VirtualDisk {
	disk := *parent
	backing := disk.Backing.(*types.VirtualDiskFlatVer2BackingInfo)
	p := new(DatastorePath)
	p.FromString(backing.FileName)
	p.Path = ""

	// Use specified disk as parent backing to a new disk.
	disk.Backing = &types.VirtualDiskFlatVer2BackingInfo{
		VirtualDeviceFileBackingInfo: types.VirtualDeviceFileBackingInfo{
			FileName:  p.String(),
			Datastore: backing.Datastore,
		},
		Parent:          backing,
		DiskMode:        backing.DiskMode,
		ThinProvisioned: backing.ThinProvisioned,
	}

	return &disk
}

func (l VirtualDeviceList) connectivity(device types.BaseVirtualDevice, v bool) error {
	c := device.GetVirtualDevice().Connectable
	if c == nil {
		return fmt.Errorf("%s is not connectable", l.Name(device))
	}

	c.Connected = v
	c.StartConnected = v

	return nil
}

// Connect changes the device to connected, returns an error if the device is not connectable.
func (l VirtualDeviceList) Connect(device types.BaseVirtualDevice) error {
	return l.connectivity(device, true)
}

// Disconnect changes the device to disconnected, returns an error if the device is not connectable.
func (l VirtualDeviceList) Disconnect(device types.BaseVirtualDevice) error {
	return l.connectivity(device, false)
}

// FindCdrom finds a cdrom device with the given name, defaulting to the first cdrom device if any.
func (l VirtualDeviceList) FindCdrom(name string) (*types.VirtualCdrom, error) {
	if name != "" {
		d := l.Find(name)
		if d == nil {
			return nil, fmt.Errorf("device '%s' not found", name)
		}
		if c, ok := d.(*types.VirtualCdrom); ok {
			return c, nil
		}
		return nil, fmt.Errorf("%s is not a cdrom device", name)
	}

	c := l.SelectByType((*types.VirtualCdrom)(nil))
	if len(c) == 0 {
		return nil, errors.New("no cdrom device found")
	}

	return c[0].(*types.VirtualCdrom), nil
}

// CreateCdrom creates a new VirtualCdrom device which can be added to a VM.
func (l VirtualDeviceList) CreateCdrom(c *types.VirtualIDEController) (*types.VirtualCdrom, error) {
	device := &types.VirtualCdrom{}

	l.AssignController(device, c)

	l.setDefaultCdromBacking(device)

	device.Connectable = &types.VirtualDeviceConnectInfo{
		AllowGuestControl: true,
		Connected:         true,
		StartConnected:    true,
	}

	return device, nil
}

// InsertIso changes the cdrom device backing to use the given iso file.
func (l VirtualDeviceList) InsertIso(device *types.VirtualCdrom, iso string) *types.VirtualCdrom {
	device.Backing = &types.VirtualCdromIsoBackingInfo{
		VirtualDeviceFileBackingInfo: types.VirtualDeviceFileBackingInfo{
			FileName: iso,
		},
	}

	return device
}

// EjectIso removes the iso file based backing and replaces with the default cdrom backing.
func (l VirtualDeviceList) EjectIso(device *types.VirtualCdrom) *types.VirtualCdrom {
	l.setDefaultCdromBacking(device)
	return device
}

func (l VirtualDeviceList) setDefaultCdromBacking(device *types.VirtualCdrom) {
	device.Backing = &types.VirtualCdromAtapiBackingInfo{
		VirtualDeviceDeviceBackingInfo: types.VirtualDeviceDeviceBackingInfo{
			DeviceName:    fmt.Sprintf("%s-%d-%d", DeviceTypeCdrom, device.ControllerKey, device.UnitNumber),
			UseAutoDetect: types.NewBool(false),
		},
	}
}

// FindFloppy finds a floppy device with the given name, defaulting to the first floppy device if any.
func (l VirtualDeviceList) FindFloppy(name string) (*types.VirtualFloppy, error) {
	if name != "" {
		d := l.Find(name)
		if d == nil {
			return nil, fmt.Errorf("device '%s' not found", name)
		}
		if c, ok := d.(*types.VirtualFloppy); ok {
			return c, nil
		}
		return nil, fmt.Errorf("%s is not a floppy device", name)
	}

	c := l.SelectByType((*types.VirtualFloppy)(nil))
	if len(c) == 0 {
		return nil, errors.New("no floppy device found")
	}

	return c[0].(*types.VirtualFloppy), nil
}

// CreateFloppy creates a new VirtualFloppy device which can be added to a VM.
func (l VirtualDeviceList) CreateFloppy() (*types.VirtualFloppy, error) {
	device := &types.VirtualFloppy{}

	c := l.PickController((*types.VirtualSIOController)(nil))
	if c == nil {
		return nil, errors.New("no available SIO controller")
	}

	l.AssignController(device, c)

	l.setDefaultFloppyBacking(device)

	device.Connectable = &types.VirtualDeviceConnectInfo{
		AllowGuestControl: true,
		Connected:         true,
		StartConnected:    true,
	}

	return device, nil
}

// InsertImg changes the floppy device backing to use the given img file.
func (l VirtualDeviceList) InsertImg(device *types.VirtualFloppy, img string) *types.VirtualFloppy {
	device.Backing = &types.VirtualFloppyImageBackingInfo{
		VirtualDeviceFileBackingInfo: types.VirtualDeviceFileBackingInfo{
			FileName: img,
		},
	}

	return device
}

// EjectImg removes the img file based backing and replaces with the default floppy backing.
func (l VirtualDeviceList) EjectImg(device *types.VirtualFloppy) *types.VirtualFloppy {
	l.setDefaultFloppyBacking(device)
	return device
}

func (l VirtualDeviceList) setDefaultFloppyBacking(device *types.VirtualFloppy) {
	device.Backing = &types.VirtualFloppyDeviceBackingInfo{
		VirtualDeviceDeviceBackingInfo: types.VirtualDeviceDeviceBackingInfo{
			DeviceName:    fmt.Sprintf("%s-%d", DeviceTypeFloppy, device.UnitNumber),
			UseAutoDetect: types.NewBool(false),
		},
	}
}

// FindSerialPort finds a serial port device with the given name, defaulting to the first serial port device if any.
func (l VirtualDeviceList) FindSerialPort(name string) (*types.VirtualSerialPort, error) {
	if name != "" {
		d := l.Find(name)
		if d == nil {
			return nil, fmt.Errorf("device '%s' not found", name)
		}
		if c, ok := d.(*types.VirtualSerialPort); ok {
			return c, nil
		}
		return nil, fmt.Errorf("%s is not a serial port device", name)
	}

	c := l.SelectByType((*types.VirtualSerialPort)(nil))
	if len(c) == 0 {
		return nil, errors.New("no serial port device found")
	}

	return c[0].(*types.VirtualSerialPort), nil
}

// CreateSerialPort creates a new VirtualSerialPort device which can be added to a VM.
func (l VirtualDeviceList) CreateSerialPort() (*types.VirtualSerialPort, error) {
	device := &types.VirtualSerialPort{
		YieldOnPoll: true,
	}

	c := l.PickController((*types.VirtualSIOController)(nil))
	if c == nil {
		return nil, errors.New("no available SIO controller")
	}

	l.AssignController(device, c)

	l.setDefaultSerialPortBacking(device)

	return device, nil
}

// ConnectSerialPort connects a serial port to a server or client uri.
func (l VirtualDeviceList) ConnectSerialPort(device *types.VirtualSerialPort, uri string, client bool, proxyuri string) *types.VirtualSerialPort {
	if strings.HasPrefix(uri, "[") {
		device.Backing = &types.VirtualSerialPortFileBackingInfo{
			VirtualDeviceFileBackingInfo: types.VirtualDeviceFileBackingInfo{
				FileName: uri,
			},
		}

		return device
	}

	direction := types.VirtualDeviceURIBackingOptionDirectionServer
	if client {
		direction = types.VirtualDeviceURIBackingOptionDirectionClient
	}

	device.Backing = &types.VirtualSerialPortURIBackingInfo{
		VirtualDeviceURIBackingInfo: types.VirtualDeviceURIBackingInfo{
			Direction:  string(direction),
			ServiceURI: uri,
			ProxyURI:   proxyuri,
		},
	}

	return device
}

// DisconnectSerialPort disconnects the serial port backing.
func (l VirtualDeviceList) DisconnectSerialPort(device *types.VirtualSerialPort) *types.VirtualSerialPort {
	l.setDefaultSerialPortBacking(device)
	return device
}

func (l VirtualDeviceList) setDefaultSerialPortBacking(device *types.VirtualSerialPort) {
	device.Backing = &types.VirtualSerialPortURIBackingInfo{
		VirtualDeviceURIBackingInfo: types.VirtualDeviceURIBackingInfo{
			Direction:  "client",
			ServiceURI: "localhost:0",
		},
	}
}

// CreateEthernetCard creates a new VirtualEthernetCard of the given name name and initialized with the given backing.
func (l VirtualDeviceList) CreateEthernetCard(name string, backing types.BaseVirtualDeviceBackingInfo) (types.BaseVirtualDevice, error) {
	ctypes := EthernetCardTypes()

	if name == "" {
		name = ctypes.deviceName(ctypes[0])
	}

	found := ctypes.Select(func(device types.BaseVirtualDevice) bool {
		return l.deviceName(device) == name
	})

	if len(found) == 0 {
		return nil, fmt.Errorf("unknown ethernet card type '%s'", name)
	}

	c, ok := found[0].(types.BaseVirtualEthernetCard)
	if !ok {
		return nil, fmt.Errorf("invalid ethernet card type '%s'", name)
	}

	c.GetVirtualEthernetCard().Backing = backing

	return c.(types.BaseVirtualDevice), nil
}

// PrimaryMacAddress returns the MacAddress field of the primary VirtualEthernetCard
func (l VirtualDeviceList) PrimaryMacAddress() string {
	eth0 := l.Find("ethernet-0")

	if eth0 == nil {
		return ""
	}

	return eth0.(types.BaseVirtualEthernetCard).GetVirtualEthernetCard().MacAddress
}

// convert a BaseVirtualDevice to a BaseVirtualMachineBootOptionsBootableDevice
var bootableDevices = map[string]func(device types.BaseVirtualDevice) types.BaseVirtualMachineBootOptionsBootableDevice{
	DeviceTypeNone: func(types.BaseVirtualDevice) types.BaseVirtualMachineBootOptionsBootableDevice {
		return &types.VirtualMachineBootOptionsBootableDevice{}
	},
	DeviceTypeCdrom: func(types.BaseVirtualDevice) types.BaseVirtualMachineBootOptionsBootableDevice {
		return &types.VirtualMachineBootOptionsBootableCdromDevice{}
	},
	DeviceTypeDisk: func(d types.BaseVirtualDevice) types.BaseVirtualMachineBootOptionsBootableDevice {
		return &types.VirtualMachineBootOptionsBootableDiskDevice{
			DeviceKey: d.GetVirtualDevice().Key,
		}
	},
	DeviceTypeEthernet: func(d types.BaseVirtualDevice) types.BaseVirtualMachineBootOptionsBootableDevice {
		return &types.VirtualMachineBootOptionsBootableEthernetDevice{
			DeviceKey: d.GetVirtualDevice().Key,
		}
	},
	DeviceTypeFloppy: func(types.BaseVirtualDevice) types.BaseVirtualMachineBootOptionsBootableDevice {
		return &types.VirtualMachineBootOptionsBootableFloppyDevice{}
	},
}

// BootOrder returns a list of devices which can be used to set boot order via VirtualMachine.SetBootOptions.
// The order can be any of "ethernet", "cdrom", "floppy" or "disk" or by specific device name.
// A value of "-" will clear the existing boot order on the VC/ESX side.
func (l VirtualDeviceList) BootOrder(order []string) []types.BaseVirtualMachineBootOptionsBootableDevice {
	var devices []types.BaseVirtualMachineBootOptionsBootableDevice

	for _, name := range order {
		if kind, ok := bootableDevices[name]; ok {
			if name == DeviceTypeNone {
				// Not covered in the API docs, nor obvious, but this clears the boot order on the VC/ESX side.
				devices = append(devices, new(types.VirtualMachineBootOptionsBootableDevice))
				continue
			}

			for _, device := range l {
				if l.Type(device) == name {
					devices = append(devices, kind(device))
				}
			}
			continue
		}

		if d := l.Find(name); d != nil {
			if kind, ok := bootableDevices[l.Type(d)]; ok {
				devices = append(devices, kind(d))
			}
		}
	}

	return devices
}

// SelectBootOrder returns an ordered list of devices matching the given bootable device order
func (l VirtualDeviceList) SelectBootOrder(order []types.BaseVirtualMachineBootOptionsBootableDevice) VirtualDeviceList {
	var devices VirtualDeviceList

	for _, bd := range order {
		for _, device := range l {
			if kind, ok := bootableDevices[l.Type(device)]; ok {
				if reflect.DeepEqual(kind(device), bd) {
					devices = append(devices, device)
				}
			}
		}
	}

	return devices
}

// TypeName returns the vmodl type name of the device
func (l VirtualDeviceList) TypeName(device types.BaseVirtualDevice) string {
	dtype := reflect.TypeOf(device)
	if dtype == nil {
		return ""
	}
	return dtype.Elem().Name()
}

var deviceNameRegexp = regexp.MustCompile(`(?:Virtual)?(?:Machine)?(\w+?)(?:Card|EthernetCard|Device|Controller)?$`)

func (l VirtualDeviceList) deviceName(device types.BaseVirtualDevice) string {
	name := "device"
	typeName := l.TypeName(device)

	m := deviceNameRegexp.FindStringSubmatch(typeName)
	if len(m) == 2 {
		name = strings.ToLower(m[1])
	}

	return name
}

// Type returns a human-readable name for the given device
func (l VirtualDeviceList) Type(device types.BaseVirtualDevice) string {
	switch device.(type) {
	case types.BaseVirtualEthernetCard:
		return DeviceTypeEthernet
	case *types.ParaVirtualSCSIController:
		return "pvscsi"
	case *types.VirtualLsiLogicSASController:
		return "lsilogic-sas"
	case *types.VirtualNVMEController:
		return "nvme"
	default:
		return l.deviceName(device)
	}
}

// Name returns a stable, human-readable name for the given device
func (l VirtualDeviceList) Name(device types.BaseVirtualDevice) string {
	var key string
	var UnitNumber int32
	d := device.GetVirtualDevice()
	if d.UnitNumber != nil {
		UnitNumber = *d.UnitNumber
	}

	dtype := l.Type(device)
	switch dtype {
	case DeviceTypeEthernet:
		key = fmt.Sprintf("%d", UnitNumber-7)
	case DeviceTypeDisk:
		key = fmt.Sprintf("%d-%d", d.ControllerKey, UnitNumber)
	default:
		key = fmt.Sprintf("%d", d.Key)
	}

	return fmt.Sprintf("%s-%s", dtype, key)
}

// ConfigSpec creates a virtual machine configuration spec for
// the specified operation, for the list of devices in the device list.
func (l VirtualDeviceList) ConfigSpec(op types.VirtualDeviceConfigSpecOperation) ([]types.BaseVirtualDeviceConfigSpec, error) {
	var fop types.VirtualDeviceConfigSpecFileOperation
	switch op {
	case types.VirtualDeviceConfigSpecOperationAdd:
		fop = types.VirtualDeviceConfigSpecFileOperationCreate
	case types.VirtualDeviceConfigSpecOperationEdit:
		fop = types.VirtualDeviceConfigSpecFileOperationReplace
	case types.VirtualDeviceConfigSpecOperationRemove:
		fop = types.VirtualDeviceConfigSpecFileOperationDestroy
	default:
		panic("unknown op")
	}

	var res []types.BaseVirtualDeviceConfigSpec
	for _, device := range l {
		config := &types.VirtualDeviceConfigSpec{
			Device:    device,
			Operation: op,
		}

		if disk, ok := device.(*types.VirtualDisk); ok {
			config.FileOperation = fop

			// Special case to attach an existing disk
			if op == types.VirtualDeviceConfigSpecOperationAdd && disk.CapacityInKB == 0 {
				childDisk := false
				if b, ok := disk.Backing.(*types.VirtualDiskFlatVer2BackingInfo); ok {
					childDisk = b.Parent != nil
				}

				if !childDisk {
					// Existing disk, clear file operation
					config.FileOperation = ""
				}
			}
		}

		res = append(res, config)
	}

	return res, nil
}
