package disk

import (
	"bytes"
	"regexp"
	"strings"
)

const RDOS_CATALOG_TRACK = 0x01
const RDOS_CATALOG_LENGTH = 0xB
const RDOS_ENTRY_LENGTH = 0x20
const RDOS_NAME_LENGTH = 0x18

var RDOS_SIGNATURE = []byte{
	byte('R' + 0x80),
	byte('D' + 0x80),
	byte('O' + 0x80),
	byte('S' + 0x80),
	byte(' ' + 0x80),
}

var RDOS_SIGNATURE_32 = []byte{
	byte('R' + 0x80),
	byte('D' + 0x80),
	byte('O' + 0x80),
	byte('S' + 0x80),
	byte(' ' + 0x80),
	byte('2' + 0x80),
}

var RDOS_SIGNATURE_33 = []byte{
	byte('R' + 0x80),
	byte('D' + 0x80),
	byte('O' + 0x80),
	byte('S' + 0x80),
	byte(' ' + 0x80),
	byte('3' + 0x80),
}

type RDOSFormatSpec struct {
	SectorStride  int
	SectorMax     int
	CatalogTrack  int
	CatalogSector int
	Ordering      SectorOrder
}

type RDOSFormat int

const (
	RDOS_Unknown RDOSFormat = iota
	RDOS_3
	RDOS_32
	RDOS_33
)

func (f RDOSFormat) String() string {

	switch f {
	case RDOS_3:
		return "RDOS3"
	case RDOS_32:
		return "RDOS32"
	case RDOS_33:
		return "RDOS33"
	}

	return "Unknown"

}

func (f RDOSFormat) Spec() *RDOSFormatSpec {

	switch f {
	case RDOS_32:
		return &RDOSFormatSpec{
			SectorStride:  16,
			SectorMax:     13,
			CatalogTrack:  1,
			CatalogSector: 0,
			Ordering:      SectorOrderDOS33,
		}
	case RDOS_3:
		return &RDOSFormatSpec{
			SectorStride:  16,
			SectorMax:     13,
			CatalogTrack:  1,
			CatalogSector: 0,
			Ordering:      SectorOrderDOS33,
		}
	case RDOS_33:
		return &RDOSFormatSpec{
			SectorStride:  16,
			SectorMax:     16,
			CatalogTrack:  1,
			CatalogSector: 12,
			Ordering:      SectorOrderProDOS,
		}
	}
	return nil

}

func (dsk *DSKWrapper) IsRDOS() (bool, RDOSFormat) {

	// It needs to be either 140K or 113K
	if len(dsk.Data) != STD_DISK_BYTES && len(dsk.Data) != STD_DISK_BYTES_OLD {
		return false, RDOS_Unknown
	}

	sectorStride := (len(dsk.Data) / STD_TRACKS_PER_DISK) / 256

	idbytes := dsk.Data[sectorStride*256 : sectorStride*256+6]

	if bytes.Compare(idbytes, RDOS_SIGNATURE_32) == 0 && sectorStride == 13 {
		return true, RDOS_32
	}

	if bytes.Compare(idbytes, RDOS_SIGNATURE_32) == 0 && sectorStride == 16 {
		return true, RDOS_3
	}

	if bytes.Compare(idbytes, RDOS_SIGNATURE_33) == 0 && sectorStride == 16 {
		return true, RDOS_33
	}

	return false, RDOS_Unknown

}

type RDOSFileDescriptor struct {
	data [RDOS_ENTRY_LENGTH]byte
}

func (fd *RDOSFileDescriptor) SetData(in []byte) {
	for i, b := range in {
		if i < RDOS_ENTRY_LENGTH {
			fd.data[i] = b
		}
	}
}

func (fd *RDOSFileDescriptor) IsDeleted() bool {

	return fd.data[24] == 0xa0 || fd.data[0] == 0x80

}

func (fd *RDOSFileDescriptor) IsUnused() bool {

	return fd.data[24] == 0x00

}

func (fd *RDOSFileDescriptor) IsLocked() bool {
	return true
}

type RDOSFileType int

const (
	FileType_RDOS_Unknown RDOSFileType = iota
	FileType_RDOS_AppleSoft
	FileType_RDOS_Binary
	FileType_RDOS_Text
)

var RDOSTypeMap = map[RDOSFileType][2]string{
	FileType_RDOS_Unknown:   [2]string{"UNK", "Unknown"},
	FileType_RDOS_AppleSoft: [2]string{"BAS", "Applesoft Basic Program"},
	FileType_RDOS_Binary:    [2]string{"BIN", "Binary File"},
	FileType_RDOS_Text:      [2]string{"TXT", "ASCII Text"},
}

func (ft RDOSFileType) String() string {
	info, ok := RDOSTypeMap[ft]
	if ok {
		return info[1]
	}
	return "Unknown"
}

func (ft RDOSFileType) Ext() string {
	info, ok := RDOSTypeMap[ft]
	if ok {
		return info[0]
	}
	return "UNK"
}

func RDOSFileTypeFromExt(ext string) RDOSFileType {
	for ft, info := range RDOSTypeMap {
		if strings.ToUpper(ext) == info[0] {
			return ft
		}
	}
	return 0x00
}

func (fd *RDOSFileDescriptor) Type() RDOSFileType {

	switch rune(fd.data[24]) {
	case 'A' + 0x80:
		return FileType_RDOS_AppleSoft
	case 'B' + 0x80:
		return FileType_RDOS_Binary
	case 'T' + 0x80:
		return FileType_RDOS_Text
	}

	return FileType_RDOS_Unknown

}

func (fd *RDOSFileDescriptor) Name() string {

	str := ""
	for i := 0; i < RDOS_NAME_LENGTH; i++ {

		ch := rune(fd.data[i] & 127)
		if ch == 0 {
			break
		}
		str += string(ch)

	}

	str = strings.TrimRight(str, " ")
	switch fd.Type() {
	case FileType_RDOS_AppleSoft:
		str += ".bas"
	case FileType_RDOS_Binary:
		str += ".bin"
	case FileType_RDOS_Text:
		str += ".txt"
	}

	return str

}

func (fd *RDOSFileDescriptor) NameUnadorned() string {

	str := ""
	for i := 0; i < RDOS_NAME_LENGTH; i++ {

		ch := rune(fd.data[i] & 127)
		if ch == 0 {
			break
		}
		str += string(ch)

	}

	return strings.TrimSpace(str)

}

func (fd RDOSFileDescriptor) NumSectors() int {
	return int(fd.data[25])
}

func (fd RDOSFileDescriptor) LoadAddress() int {
	return int(fd.data[26]) + 256*int(fd.data[27])
}

func (fd RDOSFileDescriptor) StartSector() int {
	return int(fd.data[30]) + 256*int(fd.data[31])
}

func (fd RDOSFileDescriptor) Length() int {
	return int(fd.data[28]) + 256*int(fd.data[29])
}

func (dsk *DSKWrapper) RDOSGetCatalog(pattern string) ([]*RDOSFileDescriptor, error) {

	pattern = strings.Replace(pattern, ".", "[.]", -1)
	pattern = strings.Replace(pattern, "*", ".*", -1)
	pattern = strings.Replace(pattern, "?", ".", -1)

	rx := regexp.MustCompile("(?i)" + pattern)

	var files = make([]*RDOSFileDescriptor, 0)

	d := make([]byte, 0)

	for s := 0; s < RDOS_CATALOG_LENGTH; s++ {
		dsk.SetTrack(1)
		dsk.SetSector(s)
		chunk := dsk.Read()
		d = append(d, chunk...)
	}

	var dirPtr int
	for i := 0; i < RDOS_CATALOG_LENGTH*RDOS_ENTRY_LENGTH; i++ {
		entry := &RDOSFileDescriptor{}
		entry.SetData(d[dirPtr : dirPtr+RDOS_ENTRY_LENGTH])

		dirPtr += RDOS_ENTRY_LENGTH

		if entry.IsUnused() {
			break
		}

		if !entry.IsDeleted() && rx.MatchString(entry.NameUnadorned()) {
			files = append(files, entry)
		}

	}

	return files, nil

}

func (dsk *DSKWrapper) RDOSUsedBitmap() ([]bool, error) {

	spt := dsk.RDOSFormat.Spec().SectorMax
	activeSectors := spt * 35

	used := make([]bool, activeSectors)

	files, err := dsk.RDOSGetCatalog("*")
	if err != nil {
		return used, err
	}

	for _, file := range files {

		length := file.NumSectors()
		start := file.StartSector()
		if start+length > activeSectors {
			continue // file is bad
		}

		for block := start; block < start+length; block++ {
			used[block] = true
		}

	}

	return used, nil

}

func (dsk *DSKWrapper) RDOSReadFile(file *RDOSFileDescriptor) ([]byte, error) {

	spt := dsk.RDOSFormat.Spec().SectorMax
	activeSectors := spt * 35

	length := file.NumSectors()
	start := file.StartSector()

	// If file is damaged return nothing
	if start+length > activeSectors {
		return []byte(nil), nil
	}

	block := start
	data := make([]byte, 0)
	for block < start+length && len(data) < file.Length() {

		track := block / spt
		sector := block % spt

		dsk.SetTrack(track)
		dsk.SetSector(sector)

		chunk := dsk.Read()
		needed := file.Length() - len(data)
		if needed >= 256 {
			data = append(data, chunk...)
		} else {
			data = append(data, chunk[:needed]...)
		}

		block++

	}

	return data, nil

}
