package fat

import (
	"bytes"
	"encoding/binary"
	"errors"
	"fmt"
	"math"
	"strings"
	"time"
	"unicode/utf16"

	"github.com/mitchellh/go-fs"
)

type DirectoryAttr uint8

const (
	AttrReadOnly  DirectoryAttr = 0x01
	AttrHidden                  = 0x02
	AttrSystem                  = 0x04
	AttrVolumeId                = 0x08
	AttrDirectory               = 0x10
	AttrArchive                 = 0x20
	AttrLongName                = AttrReadOnly | AttrHidden | AttrSystem | AttrVolumeId
)

// The size in bytes of a single directory entry.
const DirectoryEntrySize = 32

// Mask applied to the ord of the last long entry.
const LastLongEntryMask = 0x40

// DirectoryCluster represents a cluster on the disk that contains
// entries/contents.
type DirectoryCluster struct {
	entries      []*DirectoryClusterEntry
	fat16Root    bool
	startCluster uint32
}

// DirectoryClusterEntry is a single 32-byte entry that is part of the
// chain of entries in a directory cluster.
type DirectoryClusterEntry struct {
	name       string
	ext        string
	attr       DirectoryAttr
	createTime time.Time
	accessTime time.Time
	writeTime  time.Time
	cluster    uint32
	fileSize   uint32
	deleted    bool

	longOrd      uint8
	longName     string
	longChecksum uint8
}

func DecodeDirectoryCluster(startCluster uint32, device fs.BlockDevice, fat *FAT) (*DirectoryCluster, error) {
	bs := fat.bs
	chain := fat.Chain(startCluster)
	data := make([]byte, uint32(len(chain))*bs.BytesPerCluster())
	for i, clusterNumber := range chain {
		dataOffset := uint32(i) * bs.BytesPerCluster()
		devOffset := int64(bs.ClusterOffset(int(clusterNumber)))
		chainData := data[dataOffset : dataOffset+bs.BytesPerCluster()]

		if _, err := device.ReadAt(chainData, devOffset); err != nil {
			return nil, err
		}
	}

	result, err := decodeDirectoryCluster(data, bs)
	if err != nil {
		return nil, err
	}

	result.startCluster = startCluster
	return result, nil
}

// DecodeFAT16RootDirectory decodes the FAT16 root directory structure
// from the device.
func DecodeFAT16RootDirectoryCluster(device fs.BlockDevice, bs *BootSectorCommon) (*DirectoryCluster, error) {
	data := make([]byte, DirectoryEntrySize*bs.RootEntryCount)
	if _, err := device.ReadAt(data, int64(bs.RootDirOffset())); err != nil {
		return nil, err
	}

	result, err := decodeDirectoryCluster(data, bs)
	if err != nil {
		return nil, err
	}

	result.fat16Root = true
	return result, nil
}

func decodeDirectoryCluster(data []byte, bs *BootSectorCommon) (*DirectoryCluster, error) {
	entries := make([]*DirectoryClusterEntry, 0, bs.RootEntryCount)
	for i := uint16(0); i < uint16(len(data)/DirectoryEntrySize); i++ {
		offset := i * DirectoryEntrySize
		entryData := data[offset : offset+DirectoryEntrySize]
		if entryData[0] == 0 {
			break
		}

		entry, err := DecodeDirectoryClusterEntry(entryData)
		if err != nil {
			return nil, err
		}

		entries = append(entries, entry)
	}

	result := &DirectoryCluster{
		entries: entries,
	}

	return result, nil
}

func NewDirectoryCluster(start uint32, parent uint32, t time.Time) *DirectoryCluster {
	cluster := new(DirectoryCluster)
	cluster.startCluster = start

	// Create the "." and ".." entries
	cluster.entries = []*DirectoryClusterEntry{
		{
			accessTime: t,
			attr:       AttrDirectory,
			cluster:    start,
			createTime: t,
			name:       ".",
			writeTime:  t,
		},
		{
			accessTime: t,
			attr:       AttrDirectory,
			cluster:    parent,
			createTime: t,
			name:       "..",
			writeTime:  t,
		},
	}

	return cluster
}

// NewFat16RootDirectory creates a new DirectoryCluster that is meant only
// to be the root directory of a FAT12/FAT16 filesystem.
func NewFat16RootDirectoryCluster(bs *BootSectorCommon, label string) (*DirectoryCluster, error) {
	if bs.RootEntryCount == 0 {
		return nil, errors.New("root entry count is 0 in boot sector")
	}

	result := &DirectoryCluster{
		entries: make([]*DirectoryClusterEntry, 1, bs.RootEntryCount),
	}

	// Create the volume ID entry
	result.entries[0] = &DirectoryClusterEntry{
		attr:    AttrVolumeId,
		name:    label,
		cluster: 0,
	}

	return result, nil
}

// Bytes returns the on-disk byte data for this directory structure.
func (d *DirectoryCluster) Bytes() []byte {
	result := make([]byte, cap(d.entries)*DirectoryEntrySize)

	for i, entry := range d.entries {
		offset := i * DirectoryEntrySize
		entryBytes := entry.Bytes()
		copy(result[offset:offset+DirectoryEntrySize], entryBytes)
	}

	return result
}

// WriteToDevice writes the cluster to the device.
func (d *DirectoryCluster) WriteToDevice(device fs.BlockDevice, fat *FAT) error {
	if d.fat16Root {
		// Write the cluster to the FAT16 root directory location
		offset := int64(fat.bs.RootDirOffset())
		if _, err := device.WriteAt(d.Bytes(), offset); err != nil {
			return err
		}
	} else {
		chain := &ClusterChain{
			device:       device,
			fat:          fat,
			startCluster: d.startCluster,
		}

		if _, err := chain.Write(d.Bytes()); err != nil {
			return err
		}
	}

	return nil
}

// Bytes returns the on-disk byte data for this directory entry.
func (d *DirectoryClusterEntry) Bytes() []byte {
	var result [DirectoryEntrySize]byte

	if d.longName != "" {
		runes := bytes.Runes([]byte(d.longName))

		// The name must be zero-terminated then padded with 0xFF
		// up to 13 characters
		if len(runes) < 13 {
			runes = append(runes, 0)
			for len(runes) < 13 {
				runes = append(runes, 0xFFFF)
			}
		}

		// LDIR_Ord
		result[0] = d.longOrd

		// LDIR_Name1
		for i := 0; i < int(math.Min(float64(len(runes)), 5)); i++ {
			offset := 1 + (i * 2)
			data := result[offset : offset+2]
			binary.LittleEndian.PutUint16(data, uint16(runes[i]))
		}

		// LDIR_Attr
		result[11] = byte(AttrLongName)

		// LDIR_Type
		result[12] = 0

		// LDIR_Chksum
		result[13] = d.longChecksum

		// LDIR_Name2
		for i := 0; i < 6; i++ {
			offset := 14 + (i * 2)
			data := result[offset : offset+2]
			binary.LittleEndian.PutUint16(data, uint16(runes[i+5]))
		}

		// LDIR_FstClusLO
		result[26] = 0
		result[27] = 0

		// LDIR_Name3
		for i := 0; i < 2; i++ {
			offset := 28 + (i * 2)
			data := result[offset : offset+2]
			binary.LittleEndian.PutUint16(data, uint16(runes[i+11]))
		}
	} else {
		// DIR_Name
		var simpleName string
		if d.name == "." || d.name == ".." {
			simpleName = d.name
		} else {
			simpleName = fmt.Sprintf("%s.%s", d.name, d.ext)
		}
		copy(result[0:11], shortNameEntryValue(simpleName))

		// DIR_Attr
		result[11] = byte(d.attr)

		// DIR_CrtTime
		crtDate, crtTime, crtTenths := encodeDOSTime(d.createTime)
		result[13] = crtTenths
		binary.LittleEndian.PutUint16(result[14:16], crtTime)
		binary.LittleEndian.PutUint16(result[16:18], crtDate)

		// DIR_LstAccDate
		accDate, _, _ := encodeDOSTime(d.accessTime)
		binary.LittleEndian.PutUint16(result[18:20], accDate)

		// DIR_FstClusHI
		binary.LittleEndian.PutUint16(result[20:22], uint16(d.cluster>>16))

		// DIR_WrtTime and DIR_WrtDate
		wrtDate, wrtTime, _ := encodeDOSTime(d.writeTime)
		binary.LittleEndian.PutUint16(result[22:24], wrtTime)
		binary.LittleEndian.PutUint16(result[24:26], wrtDate)

		// DIR_FstClusLO
		binary.LittleEndian.PutUint16(result[26:28], uint16(d.cluster&0xFFFF))

		// DIR_FileSize
		binary.LittleEndian.PutUint32(result[28:32], d.fileSize)
	}

	return result[:]
}

// IsLong returns true if this is a long entry.
func (d *DirectoryClusterEntry) IsLong() bool {
	return (d.attr & AttrLongName) == AttrLongName
}

func (d *DirectoryClusterEntry) IsVolumeId() bool {
	return (d.attr & AttrVolumeId) == AttrVolumeId
}

// DecodeDirectoryClusterEntry decodes a single directory entry in the
// Directory structure.
func DecodeDirectoryClusterEntry(data []byte) (*DirectoryClusterEntry, error) {
	var result DirectoryClusterEntry

	// Do the attributes so we can determine if we're dealing with long names
	result.attr = DirectoryAttr(data[11])
	if (result.attr & AttrLongName) == AttrLongName {
		result.longOrd = data[0]

		chars := make([]uint16, 13)
		for i := 0; i < 5; i++ {
			offset := 1 + (i * 2)
			chars[i] = binary.LittleEndian.Uint16(data[offset : offset+2])
		}

		for i := 0; i < 6; i++ {
			offset := 14 + (i * 2)
			chars[i+5] = binary.LittleEndian.Uint16(data[offset : offset+2])
		}

		for i := 0; i < 2; i++ {
			offset := 28 + (i * 2)
			chars[i+11] = binary.LittleEndian.Uint16(data[offset : offset+2])
		}

		result.longName = string(utf16.Decode(chars))
		result.longChecksum = data[13]
	} else {
		result.deleted = data[0] == 0xE5

		// Basic attributes
		if data[0] == 0x05 {
			data[0] = 0xE5
		}

		result.name = strings.TrimRight(string(data[0:8]), " ")
		result.ext = strings.TrimRight(string(data[8:11]), " ")

		// Creation time
		createTimeTenths := data[13]
		createTimeWord := binary.LittleEndian.Uint16(data[14:16])
		createDateWord := binary.LittleEndian.Uint16(data[16:18])
		result.createTime = decodeDOSTime(createDateWord, createTimeWord, createTimeTenths)

		// Access time
		accessDateWord := binary.LittleEndian.Uint16(data[18:20])
		result.accessTime = decodeDOSTime(accessDateWord, 0, 0)

		// Write time
		writeTimeWord := binary.LittleEndian.Uint16(data[22:24])
		writeDateWord := binary.LittleEndian.Uint16(data[24:26])
		result.writeTime = decodeDOSTime(writeDateWord, writeTimeWord, 0)

		// Cluster
		result.cluster = uint32(binary.LittleEndian.Uint16(data[20:22]))
		result.cluster <<= 4
		result.cluster |= uint32(binary.LittleEndian.Uint16(data[26:28]))

		// File size
		result.fileSize = binary.LittleEndian.Uint32(data[28:32])
	}

	return &result, nil
}

// NewLongDirectoryClusterEntry returns the series of directory cluster
// entries that need to be written for a long directory entry. This list
// of entries does NOT contain the short name entry.
func NewLongDirectoryClusterEntry(name string, shortName string) ([]*DirectoryClusterEntry, error) {
	// Split up the shortName properly
	checksum := checksumShortName(shortNameEntryValue(shortName))

	// Calcualte the number of entries we'll actually need to store
	// the long name.
	numLongEntries := len(name) / 13
	if len(name)%13 != 0 {
		numLongEntries++
	}

	entries := make([]*DirectoryClusterEntry, numLongEntries)
	for i := 0; i < numLongEntries; i++ {
		entries[i] = new(DirectoryClusterEntry)
		entry := entries[i]
		entry.attr = AttrLongName
		entry.longOrd = uint8(numLongEntries - i)

		if i == 0 {
			entry.longOrd |= LastLongEntryMask
		}

		// Calculate the offsets of the string for this entry
		j := (numLongEntries - i - 1) * 13
		k := j + 13
		if k > len(name) {
			k = len(name)
		}

		entry.longChecksum = checksum
		entry.longName = name[j:k]
	}

	return entries, nil
}

func decodeDOSTime(date, dosTime uint16, tenths uint8) time.Time {
	return time.Date(
		1980+int(date>>9),
		time.Month((date>>5)&0x0F),
		int(date&0x1F),
		int(dosTime>>11),
		int((dosTime>>5)&0x3F),
		int((dosTime&0x1F)*2),
		int(tenths)*10*int(time.Millisecond),
		time.Local)
}

func encodeDOSTime(t time.Time) (uint16, uint16, uint8) {
	var date uint16 = uint16((t.Year() - 1980) << 9)
	date |= uint16(t.Month()) << 5
	date += uint16(t.Day() & 0xFF)

	var time uint16 = uint16(t.Hour() << 11)
	time |= uint16(t.Minute() << 5)
	time += uint16(t.Second() / 2)

	var tenths uint8
	// TODO(mitchellh): Do tenths

	return date, time, tenths
}
