package config

import (
	"encoding/json"
	"fmt"
)

// The technique for json (de)serialization was explained here:
// http://gregtrowbridge.com/golang-json-serialization-with-interfaces/

type vmComponentKind string

const (
	// Bootloader kinds
	efiBootloader   vmComponentKind = "efiBootloader"
	linuxBootloader vmComponentKind = "linuxBootloader"

	// VirtIO device kinds
	vfNet          vmComponentKind = "virtionet"
	vfVsock        vmComponentKind = "virtiosock"
	vfBlk          vmComponentKind = "virtioblk"
	vfFs           vmComponentKind = "virtiofs"
	vfRng          vmComponentKind = "virtiorng"
	vfSerial       vmComponentKind = "virtioserial"
	vfGpu          vmComponentKind = "virtiogpu"
	vfInput        vmComponentKind = "virtioinput"
	usbMassStorage vmComponentKind = "usbmassstorage"
)

type jsonKind struct {
	Kind vmComponentKind `json:"kind"`
}

func kind(k vmComponentKind) jsonKind {
	return jsonKind{Kind: k}
}

func unmarshalBootloader(rawMsg json.RawMessage) (Bootloader, error) {
	var (
		kind       jsonKind
		bootloader Bootloader
		err        error
	)
	if err := json.Unmarshal(rawMsg, &kind); err != nil {
		return nil, err
	}
	switch kind.Kind {
	case efiBootloader:
		var efi EFIBootloader
		err = json.Unmarshal(rawMsg, &efi)
		if err == nil {
			bootloader = &efi
		}
	case linuxBootloader:
		var linux LinuxBootloader
		err = json.Unmarshal(rawMsg, &linux)
		if err == nil {
			bootloader = &linux
		}
	default:
		err = fmt.Errorf("unknown 'kind' field: '%s'", kind)
	}

	return bootloader, err
}

func unmarshalDevices(rawMsg json.RawMessage) ([]VirtioDevice, error) {
	var (
		rawDevices []*json.RawMessage
		devices    []VirtioDevice
	)

	err := json.Unmarshal(rawMsg, &rawDevices)
	if err != nil {
		return nil, err
	}

	for _, msg := range rawDevices {
		dev, err := unmarshalDevice(*msg)
		if err != nil {
			return nil, err
		}
		devices = append(devices, dev)
	}

	return devices, nil
}

func unmarshalDevice(rawMsg json.RawMessage) (VirtioDevice, error) {
	var (
		kind jsonKind
		dev  VirtioDevice
		err  error
	)
	if err := json.Unmarshal(rawMsg, &kind); err != nil {
		return nil, err
	}
	switch kind.Kind {
	case vfNet:
		var newDevice VirtioNet
		err = json.Unmarshal(rawMsg, &newDevice)
		dev = &newDevice
	case vfVsock:
		var newDevice VirtioVsock
		err = json.Unmarshal(rawMsg, &newDevice)
		dev = &newDevice
	case vfBlk:
		var newDevice VirtioBlk
		err = json.Unmarshal(rawMsg, &newDevice)
		dev = &newDevice
	case vfFs:
		var newDevice VirtioFs
		err = json.Unmarshal(rawMsg, &newDevice)
		dev = &newDevice
	case vfRng:
		var newDevice VirtioRng
		err = json.Unmarshal(rawMsg, &newDevice)
		dev = &newDevice
	case vfSerial:
		var newDevice VirtioSerial
		err = json.Unmarshal(rawMsg, &newDevice)
		dev = &newDevice
	case vfGpu:
		var newDevice VirtioGPU
		err = json.Unmarshal(rawMsg, &newDevice)
		dev = &newDevice
	case vfInput:
		var newDevice VirtioInput
		err = json.Unmarshal(rawMsg, &newDevice)
		dev = &newDevice
	case usbMassStorage:
		var newDevice USBMassStorage
		err = json.Unmarshal(rawMsg, &newDevice)
		dev = &newDevice
	default:
		err = fmt.Errorf("unknown 'kind' field: '%s'", kind)
	}

	if err != nil {
		return nil, err
	}
	return dev, nil
}

// UnmarshalJSON is a custom deserializer for VirtualMachine.  The custom work
// is needed because VirtualMachine uses interfaces in its struct and JSON cannot
// determine which implementation of the interface to deserialize to.
func (vm *VirtualMachine) UnmarshalJSON(b []byte) error {
	var (
		err   error
		input map[string]*json.RawMessage
	)

	if err := json.Unmarshal(b, &input); err != nil {
		return err
	}

	for idx, rawMsg := range input {
		if rawMsg == nil {
			continue
		}
		switch idx {
		case "vcpus":
			err = json.Unmarshal(*rawMsg, &vm.Vcpus)
		case "memoryBytes":
			err = json.Unmarshal(*rawMsg, &vm.MemoryBytes)
		case "bootloader":
			var bootloader Bootloader
			bootloader, err = unmarshalBootloader(*rawMsg)
			if err == nil {
				vm.Bootloader = bootloader
			}
		case "timesync":
			err = json.Unmarshal(*rawMsg, &vm.Timesync)
		case "devices":
			var devices []VirtioDevice
			devices, err = unmarshalDevices(*rawMsg)
			if err == nil {
				vm.Devices = devices
			}
		}

		if err != nil {
			return err
		}
	}
	return nil
}

func (bootloader *EFIBootloader) MarshalJSON() ([]byte, error) {
	type blWithKind struct {
		jsonKind
		EFIBootloader
	}
	return json.Marshal(blWithKind{
		jsonKind:      kind(efiBootloader),
		EFIBootloader: *bootloader,
	})
}

func (bootloader *LinuxBootloader) MarshalJSON() ([]byte, error) {
	type blWithKind struct {
		jsonKind
		LinuxBootloader
	}
	return json.Marshal(blWithKind{
		jsonKind:        kind(linuxBootloader),
		LinuxBootloader: *bootloader,
	})
}

func (dev *VirtioNet) MarshalJSON() ([]byte, error) {
	type devWithKind struct {
		jsonKind
		VirtioNet
	}
	return json.Marshal(devWithKind{
		jsonKind:  kind(vfNet),
		VirtioNet: *dev,
	})
}

func (dev *VirtioVsock) MarshalJSON() ([]byte, error) {
	type devWithKind struct {
		jsonKind
		VirtioVsock
	}
	return json.Marshal(devWithKind{
		jsonKind:    kind(vfVsock),
		VirtioVsock: *dev,
	})
}

func (dev *VirtioBlk) MarshalJSON() ([]byte, error) {
	type devWithKind struct {
		jsonKind
		VirtioBlk
	}
	return json.Marshal(devWithKind{
		jsonKind:  kind(vfBlk),
		VirtioBlk: *dev,
	})
}

func (dev *VirtioFs) MarshalJSON() ([]byte, error) {
	type devWithKind struct {
		jsonKind
		VirtioFs
	}
	return json.Marshal(devWithKind{
		jsonKind: kind(vfFs),
		VirtioFs: *dev,
	})
}

func (dev *VirtioRng) MarshalJSON() ([]byte, error) {
	type devWithKind struct {
		jsonKind
		VirtioRng
	}
	return json.Marshal(devWithKind{
		jsonKind:  kind(vfRng),
		VirtioRng: *dev,
	})
}

func (dev *VirtioSerial) MarshalJSON() ([]byte, error) {
	type devWithKind struct {
		jsonKind
		VirtioSerial
	}
	return json.Marshal(devWithKind{
		jsonKind:     kind(vfSerial),
		VirtioSerial: *dev,
	})
}

func (dev *VirtioGPU) MarshalJSON() ([]byte, error) {
	type devWithKind struct {
		jsonKind
		VirtioGPU
	}
	return json.Marshal(devWithKind{
		jsonKind:  kind(vfGpu),
		VirtioGPU: *dev,
	})
}

func (dev *VirtioInput) MarshalJSON() ([]byte, error) {
	type devWithKind struct {
		jsonKind
		VirtioInput
	}
	return json.Marshal(devWithKind{
		jsonKind:    kind(vfInput),
		VirtioInput: *dev,
	})
}

func (dev *USBMassStorage) MarshalJSON() ([]byte, error) {
	type devWithKind struct {
		jsonKind
		USBMassStorage
	}
	return json.Marshal(devWithKind{
		jsonKind:       kind(usbMassStorage),
		USBMassStorage: *dev,
	})
}
