package qemu

import (
	"errors"
	"fmt"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"time"

	"github.com/hashicorp/packer/common"
	"github.com/hashicorp/packer/common/bootcommand"
	"github.com/hashicorp/packer/helper/communicator"
	"github.com/hashicorp/packer/helper/config"
	"github.com/hashicorp/packer/helper/multistep"
	"github.com/hashicorp/packer/packer"
	"github.com/hashicorp/packer/template/interpolate"
)

const BuilderId = "transcend.qemu"

var accels = map[string]struct{}{
	"none": {},
	"kvm":  {},
	"tcg":  {},
	"xen":  {},
	"hax":  {},
	"hvf":  {},
	"whpx": {},
}

var netDevice = map[string]bool{
	"ne2k_pci":       true,
	"i82551":         true,
	"i82557b":        true,
	"i82559er":       true,
	"rtl8139":        true,
	"e1000":          true,
	"pcnet":          true,
	"virtio":         true,
	"virtio-net":     true,
	"virtio-net-pci": true,
	"usb-net":        true,
	"i82559a":        true,
	"i82559b":        true,
	"i82559c":        true,
	"i82550":         true,
	"i82562":         true,
	"i82557a":        true,
	"i82557c":        true,
	"i82801":         true,
	"vmxnet3":        true,
	"i82558a":        true,
	"i82558b":        true,
}

var diskInterface = map[string]bool{
	"ide":         true,
	"scsi":        true,
	"virtio":      true,
	"virtio-scsi": true,
}

var diskCache = map[string]bool{
	"writethrough": true,
	"writeback":    true,
	"none":         true,
	"unsafe":       true,
	"directsync":   true,
}

var diskDiscard = map[string]bool{
	"unmap":  true,
	"ignore": true,
}

var diskDZeroes = map[string]bool{
	"unmap": true,
	"on":    true,
	"off":   true,
}

type Builder struct {
	config Config
	runner multistep.Runner
}

type Config struct {
	common.PackerConfig   `mapstructure:",squash"`
	common.HTTPConfig     `mapstructure:",squash"`
	common.ISOConfig      `mapstructure:",squash"`
	bootcommand.VNCConfig `mapstructure:",squash"`
	Comm                  communicator.Config `mapstructure:",squash"`
	common.FloppyConfig   `mapstructure:",squash"`

	ISOSkipCache      bool       `mapstructure:"iso_skip_cache"`
	Accelerator       string     `mapstructure:"accelerator"`
	CpuCount          int        `mapstructure:"cpus"`
	DiskInterface     string     `mapstructure:"disk_interface"`
	DiskSize          uint       `mapstructure:"disk_size"`
	DiskCache         string     `mapstructure:"disk_cache"`
	DiskDiscard       string     `mapstructure:"disk_discard"`
	DetectZeroes      string     `mapstructure:"disk_detect_zeroes"`
	SkipCompaction    bool       `mapstructure:"skip_compaction"`
	DiskCompression   bool       `mapstructure:"disk_compression"`
	Format            string     `mapstructure:"format"`
	Headless          bool       `mapstructure:"headless"`
	DiskImage         bool       `mapstructure:"disk_image"`
	UseBackingFile    bool       `mapstructure:"use_backing_file"`
	MachineType       string     `mapstructure:"machine_type"`
	MemorySize        int        `mapstructure:"memory"`
	NetDevice         string     `mapstructure:"net_device"`
	OutputDir         string     `mapstructure:"output_directory"`
	QemuArgs          [][]string `mapstructure:"qemuargs"`
	QemuBinary        string     `mapstructure:"qemu_binary"`
	ShutdownCommand   string     `mapstructure:"shutdown_command"`
	SSHHostPortMin    uint       `mapstructure:"ssh_host_port_min"`
	SSHHostPortMax    uint       `mapstructure:"ssh_host_port_max"`
	UseDefaultDisplay bool       `mapstructure:"use_default_display"`
	VNCBindAddress    string     `mapstructure:"vnc_bind_address"`
	VNCPortMin        uint       `mapstructure:"vnc_port_min"`
	VNCPortMax        uint       `mapstructure:"vnc_port_max"`
	VMName            string     `mapstructure:"vm_name"`

	// These are deprecated, but we keep them around for BC
	// TODO(@mitchellh): remove
	SSHWaitTimeout time.Duration `mapstructure:"ssh_wait_timeout"`

	// TODO(mitchellh): deprecate
	RunOnce bool `mapstructure:"run_once"`

	RawShutdownTimeout string `mapstructure:"shutdown_timeout"`

	shutdownTimeout time.Duration ``
	ctx             interpolate.Context
}

func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
	err := config.Decode(&b.config, &config.DecodeOpts{
		Interpolate:        true,
		InterpolateContext: &b.config.ctx,
		InterpolateFilter: &interpolate.RenderFilter{
			Exclude: []string{
				"boot_command",
				"qemuargs",
			},
		},
	}, raws...)
	if err != nil {
		return nil, err
	}

	var errs *packer.MultiError
	warnings := make([]string, 0)

	if b.config.DiskSize == 0 {
		b.config.DiskSize = 40960
	}

	if b.config.DiskCache == "" {
		b.config.DiskCache = "writeback"
	}

	if b.config.DiskDiscard == "" {
		b.config.DiskDiscard = "ignore"
	}

	if b.config.DetectZeroes == "" {
		b.config.DetectZeroes = "off"
	}

	if b.config.Accelerator == "" {
		if runtime.GOOS == "windows" {
			b.config.Accelerator = "tcg"
		} else {
			// /dev/kvm is a kernel module that may be loaded if kvm is
			// installed and the host supports VT-x extensions. To make sure
			// this will actually work we need to os.Open() it. If os.Open fails
			// the kernel module was not installed or loaded correctly.
			if fp, err := os.Open("/dev/kvm"); err != nil {
				b.config.Accelerator = "tcg"
			} else {
				fp.Close()
				b.config.Accelerator = "kvm"
			}
		}
		log.Printf("use detected accelerator: %s", b.config.Accelerator)
	} else {
		log.Printf("use specified accelerator: %s", b.config.Accelerator)
	}

	if b.config.MachineType == "" {
		b.config.MachineType = "pc"
	}

	if b.config.OutputDir == "" {
		b.config.OutputDir = fmt.Sprintf("output-%s", b.config.PackerBuildName)
	}

	if b.config.QemuBinary == "" {
		b.config.QemuBinary = "qemu-system-x86_64"
	}

	if b.config.MemorySize < 10 {
		log.Printf("MemorySize %d is too small, using default: 512", b.config.MemorySize)
		b.config.MemorySize = 512
	}

	if b.config.CpuCount < 1 {
		log.Printf("CpuCount %d too small, using default: 1", b.config.CpuCount)
		b.config.CpuCount = 1
	}

	if b.config.SSHHostPortMin == 0 {
		b.config.SSHHostPortMin = 2222
	}

	if b.config.SSHHostPortMax == 0 {
		b.config.SSHHostPortMax = 4444
	}

	if b.config.VNCBindAddress == "" {
		b.config.VNCBindAddress = "127.0.0.1"
	}

	if b.config.VNCPortMin == 0 {
		b.config.VNCPortMin = 5900
	}

	if b.config.VNCPortMax == 0 {
		b.config.VNCPortMax = 6000
	}

	if b.config.VMName == "" {
		b.config.VMName = fmt.Sprintf("packer-%s", b.config.PackerBuildName)
	}

	if b.config.Format == "" {
		b.config.Format = "qcow2"
	}

	errs = packer.MultiErrorAppend(errs, b.config.FloppyConfig.Prepare(&b.config.ctx)...)
	errs = packer.MultiErrorAppend(errs, b.config.VNCConfig.Prepare(&b.config.ctx)...)

	if b.config.NetDevice == "" {
		b.config.NetDevice = "virtio-net"
	}

	if b.config.DiskInterface == "" {
		b.config.DiskInterface = "virtio"
	}

	// TODO: backwards compatibility, write fixer instead
	if b.config.SSHWaitTimeout != 0 {
		b.config.Comm.SSHTimeout = b.config.SSHWaitTimeout
	}

	if b.config.ISOSkipCache {
		b.config.ISOChecksumType = "none"
	}

	isoWarnings, isoErrs := b.config.ISOConfig.Prepare(&b.config.ctx)
	warnings = append(warnings, isoWarnings...)
	errs = packer.MultiErrorAppend(errs, isoErrs...)

	errs = packer.MultiErrorAppend(errs, b.config.HTTPConfig.Prepare(&b.config.ctx)...)
	if es := b.config.Comm.Prepare(&b.config.ctx); len(es) > 0 {
		errs = packer.MultiErrorAppend(errs, es...)
	}

	if !(b.config.Format == "qcow2" || b.config.Format == "raw") {
		errs = packer.MultiErrorAppend(
			errs, errors.New("invalid format, only 'qcow2' or 'raw' are allowed"))
	}

	if b.config.Format != "qcow2" {
		b.config.SkipCompaction = true
		b.config.DiskCompression = false
	}

	if b.config.UseBackingFile && !(b.config.DiskImage && b.config.Format == "qcow2") {
		errs = packer.MultiErrorAppend(
			errs, errors.New("use_backing_file can only be enabled for QCOW2 images and when disk_image is true"))
	}

	if _, ok := accels[b.config.Accelerator]; !ok {
		errs = packer.MultiErrorAppend(
			errs, errors.New("invalid accelerator, only 'kvm', 'tcg', 'xen', 'hax', 'hvf', 'whpx', or 'none' are allowed"))
	}

	if _, ok := netDevice[b.config.NetDevice]; !ok {
		errs = packer.MultiErrorAppend(
			errs, errors.New("unrecognized network device type"))
	}

	if _, ok := diskInterface[b.config.DiskInterface]; !ok {
		errs = packer.MultiErrorAppend(
			errs, errors.New("unrecognized disk interface type"))
	}

	if _, ok := diskCache[b.config.DiskCache]; !ok {
		errs = packer.MultiErrorAppend(
			errs, errors.New("unrecognized disk cache type"))
	}

	if _, ok := diskDiscard[b.config.DiskDiscard]; !ok {
		errs = packer.MultiErrorAppend(
			errs, errors.New("unrecognized disk discard type"))
	}

	if _, ok := diskDZeroes[b.config.DetectZeroes]; !ok {
		errs = packer.MultiErrorAppend(
			errs, errors.New("unrecognized disk detect zeroes setting"))
	}

	if !b.config.PackerForce {
		if _, err := os.Stat(b.config.OutputDir); err == nil {
			errs = packer.MultiErrorAppend(
				errs,
				fmt.Errorf("Output directory '%s' already exists. It must not exist.", b.config.OutputDir))
		}
	}

	if b.config.RawShutdownTimeout == "" {
		b.config.RawShutdownTimeout = "5m"
	}

	b.config.shutdownTimeout, err = time.ParseDuration(b.config.RawShutdownTimeout)
	if err != nil {
		errs = packer.MultiErrorAppend(
			errs, fmt.Errorf("Failed parsing shutdown_timeout: %s", err))
	}

	if b.config.SSHHostPortMin > b.config.SSHHostPortMax {
		errs = packer.MultiErrorAppend(
			errs, errors.New("ssh_host_port_min must be less than ssh_host_port_max"))
	}

	if b.config.VNCPortMin > b.config.VNCPortMax {
		errs = packer.MultiErrorAppend(
			errs, fmt.Errorf("vnc_port_min must be less than vnc_port_max"))
	}

	if b.config.QemuArgs == nil {
		b.config.QemuArgs = make([][]string, 0)
	}

	if errs != nil && len(errs.Errors) > 0 {
		return warnings, errs
	}

	return warnings, nil
}

func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
	// Create the driver that we'll use to communicate with Qemu
	driver, err := b.newDriver(b.config.QemuBinary)
	if err != nil {
		return nil, fmt.Errorf("Failed creating Qemu driver: %s", err)
	}

	steprun := &stepRun{}
	if !b.config.DiskImage {
		steprun.BootDrive = "once=d"
		steprun.Message = "Starting VM, booting from CD-ROM"
	} else {
		steprun.BootDrive = "c"
		steprun.Message = "Starting VM, booting disk image"
	}

	steps := []multistep.Step{}
	if !b.config.ISOSkipCache {
		steps = append(steps, &common.StepDownload{
			Checksum:     b.config.ISOChecksum,
			ChecksumType: b.config.ISOChecksumType,
			Description:  "ISO",
			Extension:    b.config.TargetExtension,
			ResultKey:    "iso_path",
			TargetPath:   b.config.TargetPath,
			Url:          b.config.ISOUrls,
		},
		)
	} else {
		steps = append(steps, &stepSetISO{
			ResultKey: "iso_path",
			Url:       b.config.ISOUrls,
		},
		)
	}

	steps = append(steps, new(stepPrepareOutputDir),
		&common.StepCreateFloppy{
			Files:       b.config.FloppyConfig.FloppyFiles,
			Directories: b.config.FloppyConfig.FloppyDirectories,
		},
		new(stepCreateDisk),
		new(stepCopyDisk),
		new(stepResizeDisk),
		&common.StepHTTPServer{
			HTTPDir:     b.config.HTTPDir,
			HTTPPortMin: b.config.HTTPPortMin,
			HTTPPortMax: b.config.HTTPPortMax,
		},
	)

	if b.config.Comm.Type != "none" {
		steps = append(steps,
			new(stepForwardSSH),
		)
	}

	steps = append(steps,
		new(stepConfigureVNC),
		steprun,
		&stepTypeBootCommand{},
	)

	if b.config.Comm.Type != "none" {
		steps = append(steps,
			&communicator.StepConnect{
				Config:    &b.config.Comm,
				Host:      commHost,
				SSHConfig: b.config.Comm.SSHConfigFunc(),
				SSHPort:   commPort,
				WinRMPort: commPort,
			},
		)
	}

	steps = append(steps,
		new(common.StepProvision),
	)

	steps = append(steps,
		&common.StepCleanupTempKeys{
			Comm: &b.config.Comm,
		},
	)
	steps = append(steps,
		new(stepShutdown),
	)

	steps = append(steps,
		new(stepConvertDisk),
	)

	// Setup the state bag
	state := new(multistep.BasicStateBag)
	state.Put("cache", cache)
	state.Put("config", &b.config)
	state.Put("debug", b.config.PackerDebug)
	state.Put("driver", driver)
	state.Put("hook", hook)
	state.Put("ui", ui)

	// Run
	b.runner = common.NewRunnerWithPauseFn(steps, b.config.PackerConfig, ui, state)
	b.runner.Run(state)

	// If there was an error, return that
	if rawErr, ok := state.GetOk("error"); ok {
		return nil, rawErr.(error)
	}

	// If we were interrupted or cancelled, then just exit.
	if _, ok := state.GetOk(multistep.StateCancelled); ok {
		return nil, errors.New("Build was cancelled.")
	}

	if _, ok := state.GetOk(multistep.StateHalted); ok {
		return nil, errors.New("Build was halted.")
	}

	// Compile the artifact list
	files := make([]string, 0, 5)
	visit := func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}
		if !info.IsDir() {
			files = append(files, path)
		}

		return nil
	}

	if err := filepath.Walk(b.config.OutputDir, visit); err != nil {
		return nil, err
	}

	artifact := &Artifact{
		dir:   b.config.OutputDir,
		f:     files,
		state: make(map[string]interface{}),
	}

	artifact.state["diskName"] = state.Get("disk_filename").(string)
	artifact.state["diskType"] = b.config.Format
	artifact.state["diskSize"] = uint64(b.config.DiskSize)
	artifact.state["domainType"] = b.config.Accelerator

	return artifact, nil
}

func (b *Builder) Cancel() {
	if b.runner != nil {
		log.Println("Cancelling the step runner...")
		b.runner.Cancel()
	}
}

func (b *Builder) newDriver(qemuBinary string) (Driver, error) {
	qemuPath, err := exec.LookPath(qemuBinary)
	if err != nil {
		return nil, err
	}

	qemuImgPath, err := exec.LookPath("qemu-img")
	if err != nil {
		return nil, err
	}

	log.Printf("Qemu path: %s, Qemu Image page: %s", qemuPath, qemuImgPath)
	driver := &QemuDriver{
		QemuPath:    qemuPath,
		QemuImgPath: qemuImgPath,
	}

	if err := driver.Verify(); err != nil {
		return nil, err
	}

	return driver, nil
}
