// Package to work with VHD images
// See https://technet.microsoft.com/en-us/virtualization/bb676673.aspx
package vhd

import (
	"bytes"
	"encoding/binary"
	"encoding/hex"
	"fmt"
	"math"
	"os"
	"strconv"
	"time"

	"golang.org/x/text/encoding/unicode"
	"golang.org/x/text/transform"
)

const VHD_COOKIE = "636f6e6563746978"     // conectix
const VHD_DYN_COOKIE = "6378737061727365" // cxsparse
const VHD_CREATOR_APP = "676f2d766864"    // go-vhd
const VHD_CREATOR_HOST_OS = "5769326B"    // Win2k
const VHD_BLOCK_SIZE = 2 * 1024 * 1024    // 2MB
const VHD_HEADER_SIZE = 512
const SECTOR_SIZE = 512
const FOURK_SECTOR_SIZE = 4096
const VHD_EXTRA_HEADER_SIZE = 1024

// A VDH file
type VHD struct {
	Footer      VHDHeader
	ExtraHeader VHDExtraHeader
}

// VHD Header
type VHDHeader struct {
	Cookie             [8]byte
	Features           [4]byte
	FileFormatVersion  [4]byte
	DataOffset         [8]byte
	Timestamp          [4]byte
	CreatorApplication [4]byte
	CreatorVersion     [4]byte
	CreatorHostOS      [4]byte
	OriginalSize       [8]byte
	CurrentSize        [8]byte
	DiskGeometry       [4]byte
	DiskType           [4]byte
	Checksum           [4]byte
	UniqueId           [16]byte
	SavedState         [1]byte
	Reserved           [427]byte
}

// VHD extra header, for dynamic and differential disks
type VHDExtraHeader struct {
	Cookie              [8]byte
	DataOffset          [8]byte
	TableOffset         [8]byte
	HeaderVersion       [4]byte
	MaxTableEntries     [4]byte
	BlockSize           [4]byte
	Checksum            [4]byte
	ParentUUID          [16]byte
	ParentTimestamp     [4]byte
	Reserved            [4]byte
	ParentUnicodeName   [512]byte
	ParentLocatorEntry1 [24]byte
	ParentLocatorEntry2 [24]byte
	ParentLocatorEntry3 [24]byte
	ParentLocatorEntry4 [24]byte
	ParentLocatorEntry5 [24]byte
	ParentLocatorEntry6 [24]byte
	ParentLocatorEntry7 [24]byte
	ParentLocatorEntry8 [24]byte
	Reserved2           [256]byte
}

// Options for the CreateSparseVHD function
type VHDOptions struct {
	UUID      string
	Timestamp int64
}

/*
 *  VHDExtraHeader methods
 */

func (header *VHDExtraHeader) CookieString() string {
	return string(header.Cookie[:])
}

// Calculate and add the VHD dynamic/differential header checksum
func (h *VHDExtraHeader) addChecksum() {
	buffer := new(bytes.Buffer)
	binary.Write(buffer, binary.BigEndian, h)
	checksum := 0
	bb := buffer.Bytes()

	for counter := 0; counter < VHD_EXTRA_HEADER_SIZE; counter++ {
		checksum += int(bb[counter])
	}

	binary.BigEndian.PutUint32(h.Checksum[:], uint32(^checksum))
}

/*
 * VHDHeader methods
 */

func (h *VHDHeader) DiskTypeStr() (dt string) {
	switch h.DiskType[3] {
	case 0x00:
		dt = "None"
	case 0x01:
		dt = "Deprecated"
	case 0x02:
		dt = "Fixed"
	case 0x03:
		dt = "Dynamic"
	case 0x04:
		dt = "Differential"
	case 0x05:
		dt = "Reserved"
	case 0x06:
		dt = "Reserved"
	default:
		panic("Invalid disk type detected!")
	}

	return
}

// Return the timestamp of the header
func (h *VHDHeader) TimestampTime() time.Time {
	tstamp := binary.BigEndian.Uint32(h.Timestamp[:])
	return time.Unix(int64(946684800+tstamp), 0)
}

// Calculate and add the VHD header checksum
func (h *VHDHeader) addChecksum() {
	buffer := new(bytes.Buffer)
	binary.Write(buffer, binary.BigEndian, h)
	checksum := 0
	bb := buffer.Bytes()

	for counter := 0; counter < VHD_HEADER_SIZE; counter++ {
		checksum += int(bb[counter])
	}

	binary.BigEndian.PutUint32(h.Checksum[:], uint32(^checksum))
}

func CreateFixedHeader(size uint64, options *VHDOptions) VHDHeader {
	header := VHDHeader{}
	hexToField(VHD_COOKIE, header.Cookie[:])
	hexToField("00000002", header.Features[:])
	hexToField("00010000", header.FileFormatVersion[:])
	hexToField("ffffffffffffffff", header.DataOffset[:])

	// LOL Y2038
	if options.Timestamp != 0 {
		binary.BigEndian.PutUint32(header.Timestamp[:], uint32(options.Timestamp))
	} else {
		t := uint32(time.Now().Unix() - 946684800)
		binary.BigEndian.PutUint32(header.Timestamp[:], t)
	}

	hexToField(VHD_CREATOR_APP, header.CreatorApplication[:])
	hexToField(VHD_CREATOR_HOST_OS, header.CreatorHostOS[:])
	binary.BigEndian.PutUint64(header.OriginalSize[:], size)
	binary.BigEndian.PutUint64(header.CurrentSize[:], size)

	// total sectors = disk size / 512b sector size
	totalSectors := math.Floor(float64(size / 512))
	// [C, H, S]
	geometry := calculateCHS(uint64(totalSectors))
	binary.BigEndian.PutUint16(header.DiskGeometry[:2], uint16(geometry[0]))
	header.DiskGeometry[2] = uint8(geometry[1])
	header.DiskGeometry[3] = uint8(geometry[2])

	hexToField("00000002", header.DiskType[:]) // Fixed 0x00000002
	hexToField("00000000", header.Checksum[:])

	if options.UUID != "" {
		copy(header.UniqueId[:], uuidToBytes(options.UUID))
	} else {
		copy(header.UniqueId[:], uuidgenBytes())
	}

	header.addChecksum()
	return header
}

func RawToFixed(f *os.File, options *VHDOptions) {
	info, err := f.Stat()
	check(err)
	size := uint64(info.Size())
	header := CreateFixedHeader(size, options)
	binary.Write(f, binary.BigEndian, header)
}

func VHDCreateSparse(size uint64, name string, options VHDOptions) VHD {
	header := VHDHeader{}
	hexToField(VHD_COOKIE, header.Cookie[:])
	hexToField("00000002", header.Features[:])
	hexToField("00010000", header.FileFormatVersion[:])
	hexToField("0000000000000200", header.DataOffset[:])

	// LOL Y2038
	if options.Timestamp != 0 {
		binary.BigEndian.PutUint32(header.Timestamp[:], uint32(options.Timestamp))
	} else {
		t := uint32(time.Now().Unix() - 946684800)
		binary.BigEndian.PutUint32(header.Timestamp[:], t)
	}

	hexToField(VHD_CREATOR_APP, header.CreatorApplication[:])
	hexToField(VHD_CREATOR_HOST_OS, header.CreatorHostOS[:])
	binary.BigEndian.PutUint64(header.OriginalSize[:], size)
	binary.BigEndian.PutUint64(header.CurrentSize[:], size)

	// total sectors = disk size / 512b sector size
	totalSectors := math.Floor(float64(size / 512))
	// [C, H, S]
	geometry := calculateCHS(uint64(totalSectors))
	binary.BigEndian.PutUint16(header.DiskGeometry[:2], uint16(geometry[0]))
	header.DiskGeometry[2] = uint8(geometry[1])
	header.DiskGeometry[3] = uint8(geometry[2])

	hexToField("00000003", header.DiskType[:]) // Sparse 0x00000003
	hexToField("00000000", header.Checksum[:])

	if options.UUID != "" {
		copy(header.UniqueId[:], uuidToBytes(options.UUID))
	} else {
		copy(header.UniqueId[:], uuidgenBytes())
	}

	header.addChecksum()

	// Fill the sparse header
	header2 := VHDExtraHeader{}
	hexToField(VHD_DYN_COOKIE, header2.Cookie[:])
	hexToField("ffffffffffffffff", header2.DataOffset[:])
	// header size + sparse header size
	binary.BigEndian.PutUint64(header2.TableOffset[:], uint64(VHD_EXTRA_HEADER_SIZE+VHD_HEADER_SIZE))
	hexToField("00010000", header2.HeaderVersion[:])

	maxTableSize := uint32(size / (VHD_BLOCK_SIZE))
	binary.BigEndian.PutUint32(header2.MaxTableEntries[:], maxTableSize)

	binary.BigEndian.PutUint32(header2.BlockSize[:], VHD_BLOCK_SIZE)
	binary.BigEndian.PutUint32(header2.ParentTimestamp[:], uint32(0))
	header2.addChecksum()

	f, err := os.Create(name)
	check(err)
	defer f.Close()

	binary.Write(f, binary.BigEndian, header)
	binary.Write(f, binary.BigEndian, header2)

	/*
		Write BAT entries
		The BAT is always extended to a sector (4K) boundary
		1536 = 512 + 1024 (the VHD Header + VHD Sparse header size)
	*/
	for count := uint32(0); count < (FOURK_SECTOR_SIZE - 1536); count += 1 {
		f.Write([]byte{0xff})
	}

	/* Windows creates 8K VHDs by default */
	for i := 0; i < (FOURK_SECTOR_SIZE - VHD_HEADER_SIZE); i += 1 {
		f.Write([]byte{0x0})
	}

	binary.Write(f, binary.BigEndian, header)

	return VHD{
		Footer:      header,
		ExtraHeader: header2,
	}
}

/*
 * VHD
 */

func FromFile(f *os.File) (vhd VHD) {
	vhd = VHD{}
	vhd.Footer = readVHDFooter(f)
	vhd.ExtraHeader = readVHDExtraHeader(f)

	return vhd
}

func (vhd *VHD) PrintInfo() {
	fmt.Println("\nVHD footer")
	fmt.Println("==========")
	vhd.PrintFooter()

	if vhd.Footer.DiskType[3] == 0x3 || vhd.Footer.DiskType[3] == 0x04 {
		fmt.Println("\nVHD sparse/differential header")
		fmt.Println("===============================")
		vhd.PrintExtraHeader()
	}
}

func (vhd *VHD) PrintExtraHeader() {
	header := vhd.ExtraHeader

	fmtField("Cookie", fmt.Sprintf("%s (%s)",
		hexs(header.Cookie[:]), header.CookieString()))
	fmtField("Data offset", hexs(header.DataOffset[:]))
	fmtField("Table offset", hexs(header.TableOffset[:]))
	fmtField("Header version", hexs(header.HeaderVersion[:]))
	fmtField("Max table entries", hexs(header.MaxTableEntries[:]))
	fmtField("Block size", hexs(header.BlockSize[:]))
	fmtField("Checksum", hexs(header.Checksum[:]))
	fmtField("Parent UUID", uuid(header.ParentUUID[:]))

	// Seconds since January 1, 1970 12:00:00 AM in UTC/GMT.
	// 946684800 = January 1, 2000 12:00:00 AM in UTC/GMT.
	tstamp := binary.BigEndian.Uint32(header.ParentTimestamp[:])
	t := time.Unix(int64(946684800+tstamp), 0)
	fmtField("Parent timestamp", fmt.Sprintf("%s", t))

	fmtField("Reserved", hexs(header.Reserved[:]))
	parentNameBytes, _, err := transform.Bytes(
		unicode.UTF16(unicode.BigEndian, unicode.IgnoreBOM).NewDecoder(),
		header.ParentUnicodeName[:],
	)
	if err != nil {
		panic(err)
	}
	parentName := string(parentNameBytes)
	fmtField("Parent Name", parentName)
	// Parent locator entries ignored since it's a dynamic disk
	sum := 0
	for _, b := range header.Reserved2 {
		sum += int(b)
	}
	fmtField("Reserved2", strconv.Itoa(sum))
}

func (vhd *VHD) PrintFooter() {
	header := vhd.Footer

	//fmtField("Cookie", string(header.Cookie[:]))
	fmtField("Cookie", fmt.Sprintf("%s (%s)",
		hexs(header.Cookie[:]), string(header.Cookie[:])))
	fmtField("Features", hexs(header.Features[:]))
	fmtField("File format version", hexs(header.FileFormatVersion[:]))

	dataOffset := binary.BigEndian.Uint64(header.DataOffset[:])
	fmtField("Data offset",
		fmt.Sprintf("%s (%d bytes)", hexs(header.DataOffset[:]), dataOffset))

	//// Seconds since January 1, 1970 12:00:00 AM in UTC/GMT.
	//// 946684800 = January 1, 2000 12:00:00 AM in UTC/GMT.
	t := time.Unix(int64(946684800+binary.BigEndian.Uint32(header.Timestamp[:])), 0)
	fmtField("Timestamp", fmt.Sprintf("%s", t))

	fmtField("Creator application", string(header.CreatorApplication[:]))
	fmtField("Creator version", hexs(header.CreatorVersion[:]))
	fmtField("Creator OS", string(header.CreatorHostOS[:]))

	originalSize := binary.BigEndian.Uint64(header.OriginalSize[:])
	fmtField("Original size",
		fmt.Sprintf("%s ( %d bytes )", hexs(header.OriginalSize[:]), originalSize))

	currentSize := binary.BigEndian.Uint64(header.OriginalSize[:])
	fmtField("Current size",
		fmt.Sprintf("%s ( %d bytes )", hexs(header.CurrentSize[:]), currentSize))

	cilinders := int64(binary.BigEndian.Uint16(header.DiskGeometry[:2]))
	heads := int64(header.DiskGeometry[2])
	sectors := int64(header.DiskGeometry[3])
	dsize := cilinders * heads * sectors * 512
	fmtField("Disk geometry",
		fmt.Sprintf("%s (c: %d, h: %d, s: %d) (%d bytes)",
			hexs(header.DiskGeometry[:]),
			cilinders,
			heads,
			sectors,
			dsize))

	fmtField("Disk type",
		fmt.Sprintf("%s (%s)", hexs(header.DiskType[:]), header.DiskTypeStr()))

	fmtField("Checksum", hexs(header.Checksum[:]))
	fmtField("UUID", uuid(header.UniqueId[:]))
	fmtField("Saved state", fmt.Sprintf("%d", header.SavedState[0]))
}

/*
	Utility functions
*/
func calculateCHS(ts uint64) []uint {
	var sectorsPerTrack,
		heads,
		cylinderTimesHeads,
		cylinders float64
	totalSectors := float64(ts)

	ret := make([]uint, 3)

	if totalSectors > 65535*16*255 {
		totalSectors = 65535 * 16 * 255
	}

	if totalSectors >= 65535*16*63 {
		sectorsPerTrack = 255
		heads = 16
		cylinderTimesHeads = math.Floor(totalSectors / sectorsPerTrack)
	} else {
		sectorsPerTrack = 17
		cylinderTimesHeads = math.Floor(totalSectors / sectorsPerTrack)
		heads = math.Floor((cylinderTimesHeads + 1023) / 1024)
		if heads < 4 {
			heads = 4
		}
		if (cylinderTimesHeads >= (heads * 1024)) || heads > 16 {
			sectorsPerTrack = 31
			heads = 16
			cylinderTimesHeads = math.Floor(totalSectors / sectorsPerTrack)
		}
		if cylinderTimesHeads >= (heads * 1024) {
			sectorsPerTrack = 63
			heads = 16
			cylinderTimesHeads = math.Floor(totalSectors / sectorsPerTrack)
		}
	}

	cylinders = cylinderTimesHeads / heads

	// This will floor the values
	ret[0] = uint(cylinders)
	ret[1] = uint(heads)
	ret[2] = uint(sectorsPerTrack)

	return ret
}

func hexToField(hexs string, field []byte) {
	h, err := hex.DecodeString(hexs)
	check(err)

	copy(field, h)
}

// Return the number of blocks in the disk, diskSize in bytes
func getMaxTableEntries(diskSize uint64) uint64 {
	return diskSize * (2 * 1024 * 1024) // block size is 2M
}

func readVHDExtraHeader(f *os.File) (header VHDExtraHeader) {
	buff := make([]byte, 1024)
	_, err := f.ReadAt(buff, 512)
	check(err)

	binary.Read(bytes.NewBuffer(buff[:]), binary.BigEndian, &header)

	return header
}

func readVHDFooter(f *os.File) (header VHDHeader) {
	info, err := f.Stat()
	check(err)

	buff := make([]byte, 512)
	_, err = f.ReadAt(buff, info.Size()-512)
	check(err)

	binary.Read(bytes.NewBuffer(buff[:]), binary.BigEndian, &header)

	return header
}

func readVHDHeader(f *os.File) (header VHDHeader) {
	buff := make([]byte, 512)
	_, err := f.ReadAt(buff, 0)
	check(err)

	binary.Read(bytes.NewBuffer(buff[:]), binary.BigEndian, &header)

	return header
}
