//go:generate struct-markdown

package common

import (
	"fmt"
	"path/filepath"
	"runtime"
	"strings"

	"github.com/hashicorp/packer/packer-plugin-sdk/template/interpolate"
)

type HWConfig struct {
	// The number of cpus to use when building the VM.
	CpuCount int `mapstructure:"cpus" required:"false"`
	// The amount of memory to use when building the VM in megabytes.
	MemorySize int `mapstructure:"memory" required:"false"`
	// The number of cores per socket to use when building the VM. This
	// corresponds to the cpuid.coresPerSocket option in the .vmx file.
	CoreCount int `mapstructure:"cores" required:"false"`
	// This is the network type that the virtual machine will be created with.
	// This can be one of the generic values that map to a device such as
	// hostonly, nat, or bridged. If the network is not one of these values,
	// then it is assumed to be a VMware network device. (VMnet0..x)
	Network string `mapstructure:"network" required:"false"`
	// This is the ethernet adapter type the the virtual machine will be
	// created with. By default the `e1000` network adapter type will be used
	// by Packer. For more information, please consult [Choosing a network
	// adapter for your virtual
	// machine](https://kb.vmware.com/s/article/1001805) for desktop VMware
	// clients. For ESXi, refer to the proper ESXi documentation.
	NetworkAdapterType string `mapstructure:"network_adapter_type" required:"false"`
	// The custom name of the network. Sets the vmx value "ethernet0.networkName"
	NetworkName string `mapstructure:"network_name" required:"false"`
	// Specify whether to enable VMware's virtual soundcard device when
	// building the VM. Defaults to false.
	Sound bool `mapstructure:"sound" required:"false"`
	// Enable VMware's USB bus when building the guest VM. Defaults to false.
	// To enable usage of the XHCI bus for USB 3 (5 Gbit/s), one can use the
	// vmx_data option to enable it by specifying true for the usb_xhci.present
	// property.
	USB bool `mapstructure:"usb" required:"false"`
	// This specifies a serial port to add to the VM. It has a format of
	// `Type:option1,option2,...`. The field `Type` can be one of the following
	// values: `FILE`, `DEVICE`, `PIPE`, `AUTO`, or `NONE`.
	//
	// * `FILE:path(,yield)` - Specifies the path to the local file to be used
	//   as the serial port.
	//
	//   * `yield` (bool) - This is an optional boolean that specifies
	//     whether the vm should yield the cpu when polling the port. By
	//     default, the builder will assume this as `FALSE`.
	//
	// * `DEVICE:path(,yield)` - Specifies the path to the local device to be
	//   used as the serial port. If `path` is empty, then default to the first
	//   serial port.
	//
	//   * `yield` (bool) - This is an optional boolean that specifies
	//     whether the vm should yield the cpu when polling the port. By
	//     default, the builder will assume this as `FALSE`.
	//
	// * `PIPE:path,endpoint,host(,yield)` - Specifies to use the named-pipe
	//   "path" as a serial port. This has a few options that determine how the
	//   VM should use the named-pipe.
	//
	//   * `endpoint` (string) - Chooses the type of the VM-end, which can be
	//     either a `client` or `server`.
	//
	//   * `host` (string)     - Chooses the type of the host-end, which can
	//     be either `app` (application) or `vm` (another virtual-machine).
	//
	//   * `yield` (bool)      - This is an optional boolean that specifies
	//     whether the vm should yield the cpu when polling the port. By
	//     default, the builder will assume this as `FALSE`.
	//
	// * `AUTO:(yield)` - Specifies to use auto-detection to determine the
	//   serial port to use. This has one option to determine how the VM should
	//   support the serial port.
	//
	//   * `yield` (bool) - This is an optional boolean that specifies
	//     whether the vm should yield the cpu when polling the port. By
	//     default, the builder will assume this as `FALSE`.
	//
	// * `NONE` - Specifies to not use a serial port. (default)
	//
	Serial string `mapstructure:"serial" required:"false"`
	// This specifies a parallel port to add to the VM. It has the format of
	// `Type:option1,option2,...`. Type can be one of the following values:
	// `FILE`, `DEVICE`, `AUTO`, or `NONE`.
	//
	// * `FILE:path` 		- Specifies the path to the local file to be used
	//   for the parallel port.
	//
	// * `DEVICE:path`	 	- Specifies the path to the local device to be used
	//   for the parallel port.
	//
	// * `AUTO:direction`   - Specifies to use auto-detection to determine the
	//   parallel port. Direction can be `BI` to specify bidirectional
	//   communication or `UNI` to specify unidirectional communication.
	//
	// * `NONE` 			- Specifies to not use a parallel port. (default)
	Parallel string `mapstructure:"parallel" required:"false"`
}

func (c *HWConfig) Prepare(ctx *interpolate.Context) []error {
	var errs []error

	// Hardware and cpu options
	if c.CpuCount < 0 {
		errs = append(errs, fmt.Errorf("An invalid number of cpus was specified (cpus < 0): %d", c.CpuCount))
	}

	if c.MemorySize < 0 {
		errs = append(errs, fmt.Errorf("An invalid amount of memory was specified (memory < 0): %d", c.MemorySize))
	}

	// Hardware and cpu options
	if c.CoreCount < 0 {
		errs = append(errs, fmt.Errorf("An invalid number of cores was specified (cores < 0): %d", c.CoreCount))
	}

	// Peripherals
	if !c.Sound {
		c.Sound = false
	}

	if !c.USB {
		c.USB = false
	}

	if c.Parallel == "" {
		c.Parallel = "none"
	}

	if c.Serial == "" {
		c.Serial = "none"
	}

	return errs
}

/* parallel port */
type ParallelUnion struct {
	Union  interface{}
	File   *ParallelPortFile
	Device *ParallelPortDevice
	Auto   *ParallelPortAuto
}
type ParallelPortFile struct {
	Filename string
}
type ParallelPortDevice struct {
	Bidirectional string
	Devicename    string
}
type ParallelPortAuto struct {
	Bidirectional string
}

func (c *HWConfig) HasParallel() bool {
	return c.Parallel != ""
}

func (c *HWConfig) ReadParallel() (*ParallelUnion, error) {
	input := strings.SplitN(c.Parallel, ":", 2)
	if len(input) < 1 {
		return nil, fmt.Errorf("Unexpected format for parallel port: %s", c.Parallel)
	}

	var formatType, formatOptions string
	formatType = input[0]
	if len(input) == 2 {
		formatOptions = input[1]
	} else {
		formatOptions = ""
	}

	switch strings.ToUpper(formatType) {
	case "FILE":
		res := &ParallelPortFile{Filename: filepath.FromSlash(formatOptions)}
		return &ParallelUnion{Union: res, File: res}, nil
	case "DEVICE":
		comp := strings.Split(formatOptions, ",")
		if len(comp) < 1 || len(comp) > 2 {
			return nil, fmt.Errorf("Unexpected format for parallel port: %s", c.Parallel)
		}
		res := new(ParallelPortDevice)
		res.Bidirectional = "FALSE"
		res.Devicename = filepath.FromSlash(comp[0])
		if len(comp) > 1 {
			switch strings.ToUpper(comp[1]) {
			case "BI":
				res.Bidirectional = "TRUE"
			case "UNI":
				res.Bidirectional = "FALSE"
			default:
				return nil, fmt.Errorf("Unknown direction %s specified for parallel port: %s", strings.ToUpper(comp[1]), c.Parallel)
			}
		}
		return &ParallelUnion{Union: res, Device: res}, nil

	case "AUTO":
		res := new(ParallelPortAuto)
		switch strings.ToUpper(formatOptions) {
		case "":
			fallthrough
		case "UNI":
			res.Bidirectional = "FALSE"
		case "BI":
			res.Bidirectional = "TRUE"
		default:
			return nil, fmt.Errorf("Unknown direction %s specified for parallel port: %s", strings.ToUpper(formatOptions), c.Parallel)
		}
		return &ParallelUnion{Union: res, Auto: res}, nil

	case "NONE":
		return &ParallelUnion{Union: nil}, nil
	}

	return nil, fmt.Errorf("Unexpected format for parallel port: %s", c.Parallel)
}

/* serial conversions */
type SerialConfigPipe struct {
	Filename string
	Endpoint string
	Host     string
	Yield    string
}

type SerialConfigFile struct {
	Filename string
	Yield    string
}

type SerialConfigDevice struct {
	Devicename string
	Yield      string
}

type SerialConfigAuto struct {
	Devicename string
	Yield      string
}

type SerialUnion struct {
	Union  interface{}
	Pipe   *SerialConfigPipe
	File   *SerialConfigFile
	Device *SerialConfigDevice
	Auto   *SerialConfigAuto
}

func (c *HWConfig) HasSerial() bool {
	return c.Serial != ""
}

func (c *HWConfig) ReadSerial() (*SerialUnion, error) {
	var defaultSerialPort string
	if runtime.GOOS == "windows" {
		defaultSerialPort = "COM1"
	} else {
		defaultSerialPort = "/dev/ttyS0"
	}
	input := strings.SplitN(c.Serial, ":", 2)
	if len(input) < 1 {
		return nil, fmt.Errorf("Unexpected format for serial port: %s", c.Serial)
	}

	var formatType, formatOptions string
	formatType = input[0]
	if len(input) == 2 {
		formatOptions = input[1]
	} else {
		formatOptions = ""
	}

	switch strings.ToUpper(formatType) {
	case "PIPE":
		comp := strings.Split(formatOptions, ",")
		if len(comp) < 3 || len(comp) > 4 {
			return nil, fmt.Errorf("Unexpected format for serial port pipe: %s", c.Serial)
		}
		if res := strings.ToLower(comp[1]); res != "client" && res != "server" {
			return nil, fmt.Errorf("Unexpected format for endpoint in serial port pipe: %s -> %s", c.Serial, res)
		}
		if res := strings.ToLower(comp[2]); res != "app" && res != "vm" {
			return nil, fmt.Errorf("Unexpected format for host in serial port pipe: %s -> %s", c.Serial, res)
		}
		res := &SerialConfigPipe{
			Filename: comp[0],
			Endpoint: comp[1],
			Host:     map[string]string{"app": "TRUE", "vm": "FALSE"}[strings.ToLower(comp[2])],
			Yield:    "FALSE",
		}
		if len(comp) == 4 {
			res.Yield = strings.ToUpper(comp[3])
		}
		if res.Yield != "TRUE" && res.Yield != "FALSE" {
			return nil, fmt.Errorf("Unexpected format for yield in serial port pipe: %s -> %s", c.Serial, res.Yield)
		}
		return &SerialUnion{Union: res, Pipe: res}, nil

	case "FILE":
		comp := strings.Split(formatOptions, ",")
		if len(comp) > 2 {
			return nil, fmt.Errorf("Unexpected format for serial port file: %s", c.Serial)
		}

		res := &SerialConfigFile{Yield: "FALSE"}
		res.Filename = filepath.FromSlash(comp[0])
		res.Yield = "FALSE"
		if len(comp) > 1 {
			res.Yield = strings.ToUpper(comp[1])
		}
		if res.Yield != "TRUE" && res.Yield != "FALSE" {
			return nil, fmt.Errorf("Unexpected format for yield in serial port file: %s -> %s", c.Serial, res.Yield)
		}

		return &SerialUnion{Union: res, File: res}, nil

	case "DEVICE":
		comp := strings.Split(formatOptions, ",")
		if len(comp) > 2 {
			return nil, fmt.Errorf("Unexpected format for serial port device: %s", c.Serial)
		}
		res := new(SerialConfigDevice)
		// set serial port defaults
		res.Devicename = defaultSerialPort
		res.Yield = "FALSE"
		// Read actual values from component, if set.
		if len(comp) == 1 {
			filepath.FromSlash(comp[0])
		} else if len(comp) == 2 {
			res.Devicename = filepath.FromSlash(comp[0])
			res.Yield = strings.ToUpper(comp[1])
		}

		if res.Yield != "TRUE" && res.Yield != "FALSE" {
			return nil, fmt.Errorf("Unexpected format for yield in serial port device: %s -> %s", c.Serial, res.Yield)
		}

		return &SerialUnion{Union: res, Device: res}, nil

	case "AUTO":
		res := new(SerialConfigAuto)
		res.Devicename = defaultSerialPort

		if len(formatOptions) > 0 {
			res.Yield = strings.ToUpper(formatOptions)
		} else {
			res.Yield = "FALSE"
		}

		if res.Yield != "TRUE" && res.Yield != "FALSE" {
			return nil, fmt.Errorf("Unexpected format for yield in serial port auto: %s -> %s", c.Serial, res.Yield)
		}

		return &SerialUnion{Union: res, Auto: res}, nil

	case "NONE":
		return &SerialUnion{Union: nil}, nil

	default:
		return nil, fmt.Errorf("Unknown serial type %s: %s", strings.ToUpper(formatType), c.Serial)
	}
}
