// Copyright Terence J. Boldt (c)2021-2024
// Use of this source code is governed by an MIT
// license that can be found in the LICENSE file.

// This file provides access to read, write, delete
// and parse directories on a ProDOS drive image

package prodos

import (
	"errors"
	"fmt"
	"io"
	"strings"
	"time"
)

// VolumeHeader from ProDOS
type VolumeHeader struct {
	VolumeName       string
	CreationTime     time.Time
	ActiveFileCount  uint16
	BitmapStartBlock uint16
	TotalBlocks      uint16
	NextBlock        uint16
	EntryLength      uint8
	EntriesPerBlock  uint8
	MinVersion       uint8
	Version          uint8
}

// DirectoryHeader from ProDOS
type DirectoryHeader struct {
	PreviousBlock     uint16
	NextBlock         uint16
	IsSubDirectory    bool
	Name              string
	CreationTime      time.Time
	Version           uint8
	MinVersion        uint8
	Access            uint8
	EntryLength       uint8
	EntriesPerBlock   uint8
	ActiveFileCount   uint16
	StartingBlock     uint16
	ParentBlock       uint16
	ParentEntry       uint16
	ParentEntryLength uint8
}

const (
	// StorageDeleted signifies file is deleted
	StorageDeleted = 0
	// StorageSeedling signifies file is <= 512 bytes
	StorageSeedling = 1
	// StorageSapling signifies file is > 512 bytes and <= 128 KB
	StorageSapling = 2
	// StorageTree signifies file is > 128 KB and <= 16 MB
	StorageTree = 3
	// StoragePascal signifies pascal storage area
	StoragePascal = 4
	// StorageDirectory signifies directory
	StorageDirectory = 13
)

// FileEntry from ProDOS
type FileEntry struct {
	StorageType   uint8
	FileName      string
	FileType      uint8
	CreationTime  time.Time
	KeyPointer    uint16
	Version       uint8
	MinVersion    uint8
	BlocksUsed    uint16
	EndOfFile     uint32
	Access        uint8
	AuxType       uint16
	ModifiedTime  time.Time
	HeaderPointer uint16

	DirectoryBlock  uint16
	DirectoryOffset uint16
}

// ReadDirectory reads the directory information from a specified path
// on a ProDOS image
func ReadDirectory(reader io.ReaderAt, path string) (VolumeHeader, DirectoryHeader, []FileEntry, error) {
	buffer, err := ReadBlock(reader, 2)
	if err != nil {
		return VolumeHeader{}, DirectoryHeader{}, nil, err
	}

	volumeHeader := parseVolumeHeader(buffer)

	if len(path) == 0 {
		path = fmt.Sprintf("/%s", volumeHeader.VolumeName)
	}

	// add volume name if not full path
	if !strings.HasPrefix(path, "/") {
		path = fmt.Sprintf("/%s/%s", volumeHeader.VolumeName, path)
	}

	path = strings.ToUpper(path)
	paths := strings.Split(path, "/")

	directoryHeader, fileEntries, err := getFileEntriesInDirectory(reader, 2, 1, paths)
	if err != nil {
		return VolumeHeader{}, DirectoryHeader{}, nil, err
	}

	return volumeHeader, directoryHeader, fileEntries, nil
}

// CreateDirectory creates a directory information of a specified path
// on a ProDOS image
func CreateDirectory(readerWriter ReaderWriterAt, path string) error {
	if len(path) == 0 {
		return errors.New("cannot create directory without path")
	}

	// add volume name if not full path
	path, err := makeFullPath(path, readerWriter)
	if err != nil {
		return err
	}

	parentPath, newDirectory := GetDirectoryAndFileNameFromPath(path)

	existingFileEntry, _ := GetFileEntry(readerWriter, path)
	if existingFileEntry.StorageType != StorageDeleted {
		return errors.New("directory already exists")
	}

	fileEntry, err := getFreeFileEntryInDirectory(readerWriter, parentPath)
	if err != nil {
		errString := fmt.Sprintf("failed to create directory: %s", err)
		return errors.New(errString)
	}

	// get list of blocks to write file to
	blockList, err := createBlockList(readerWriter, 512)
	if err != nil {
		errString := fmt.Sprintf("failed to create directory: %s", err)
		return errors.New(errString)
	}

	updateVolumeBitmap(readerWriter, blockList)

	fileEntry.FileName = newDirectory
	fileEntry.BlocksUsed = 1
	fileEntry.CreationTime = time.Now()
	fileEntry.ModifiedTime = time.Now()
	fileEntry.AuxType = 0
	fileEntry.EndOfFile = 0x200
	fileEntry.FileType = 0x0F
	fileEntry.KeyPointer = blockList[0]
	fileEntry.Access = 0b11100011
	fileEntry.StorageType = StorageDirectory
	fileEntry.Version = 0x24
	fileEntry.MinVersion = 0x00

	err = writeFileEntry(readerWriter, fileEntry)
	if err != nil {
		return err
	}

	err = incrementFileCount(readerWriter, fileEntry)
	if err != nil {
		errString := fmt.Sprintf("failed to create directory: %s", err)
		return errors.New(errString)
	}

	directoryEntry := DirectoryHeader{
		PreviousBlock:     0,
		NextBlock:         0,
		IsSubDirectory:    true,
		Name:              newDirectory,
		CreationTime:      time.Now(),
		Version:           0x24,
		MinVersion:        0,
		Access:            0xE3,
		EntryLength:       0x27,
		EntriesPerBlock:   0x0D,
		ActiveFileCount:   0,
		StartingBlock:     blockList[0],
		ParentBlock:       fileEntry.DirectoryBlock,
		ParentEntry:       uint16(fileEntry.DirectoryOffset-0x04) / 0x27,
		ParentEntryLength: 0x27,
	}

	err = writeDirectoryHeader(readerWriter, directoryEntry)
	if err != nil {
		errString := fmt.Sprintf("failed to create directory: %s", err)
		return errors.New(errString)
	}

	return nil
}

func makeFullPath(path string, reader io.ReaderAt) (string, error) {
	if !strings.HasPrefix(path, "/") {
		buffer, err := ReadBlock(reader, 0x0002)
		if err != nil {
			return "", err
		}

		volumeHeader := parseVolumeHeader(buffer)
		path = fmt.Sprintf("/%s/%s", volumeHeader.VolumeName, path)
	}
	return path, nil
}

func getFreeFileEntryInDirectory(readerWriter ReaderWriterAt, directory string) (FileEntry, error) {
	_, directoryHeader, _, err := ReadDirectory(readerWriter, directory)
	if err != nil {
		return FileEntry{}, err
	}
	blockNumber := directoryHeader.StartingBlock
	buffer, err := ReadBlock(readerWriter, blockNumber)
	if err != nil {
		return FileEntry{}, err
	}
	entryOffset := uint16(43) // start at offset after header
	entryNumber := 2          // header is essentially the first entry so start at 2

	for {
		if entryNumber > 13 {
			nextBlockNumber := uint16(buffer[2]) + uint16(buffer[3])*256
			// if we ran out of blocks in the directory, expand directory or fail
			if nextBlockNumber == 0 {
				if !directoryHeader.IsSubDirectory {
					return FileEntry{}, errors.New("no free file entries found")
				}
				nextBlockNumber, err = expandDirectory(readerWriter, buffer, blockNumber, directoryHeader)
				if err != nil {
					return FileEntry{}, err
				}
			}
			blockNumber = nextBlockNumber
			// else read the next block in the directory
			buffer, err = ReadBlock(readerWriter, blockNumber)
			if err != nil {
				return FileEntry{}, nil
			}

			entryOffset = 4
			entryNumber = 1
		}
		fileEntry := parseFileEntry(buffer[entryOffset:entryOffset+0x28], blockNumber, entryOffset)

		if fileEntry.StorageType == StorageDeleted {
			fileEntry = FileEntry{}
			fileEntry.DirectoryBlock = blockNumber
			fileEntry.DirectoryOffset = entryOffset
			fileEntry.HeaderPointer = directoryHeader.StartingBlock
			return fileEntry, nil
		}

		entryNumber++
		entryOffset += 39
	}
}

func expandDirectory(readerWriter ReaderWriterAt, buffer []byte, blockNumber uint16, directoryHeader DirectoryHeader) (uint16, error) {
	volumeBitMap, err := ReadVolumeBitmap(readerWriter)
	if err != nil {
		errString := fmt.Sprintf("failed to get volume bitmap to expand directory: %s", err)
		return 0, errors.New(errString)
	}
	blockList := findFreeBlocks(volumeBitMap, 1)
	if len(blockList) != 1 {
		return 0, errors.New("failed to get free block to expand directory")
	}

	nextBlockNumber := blockList[0]
	buffer[0x02] = byte(nextBlockNumber & 0x00FF)
	buffer[0x03] = byte(nextBlockNumber >> 8)
	err = WriteBlock(readerWriter, blockNumber, buffer)
	if err != nil {
		errString := fmt.Sprintf("failed to write block to expand directory: %s", err)
		return 0, errors.New(errString)
	}

	buffer = make([]byte, 0x200)
	buffer[0x00] = byte(blockNumber & 0x00FF)
	buffer[0x01] = byte(blockNumber >> 8)
	err = WriteBlock(readerWriter, nextBlockNumber, buffer)
	if err != nil {
		errString := fmt.Sprintf("failed to write new block to expand directory: %s", err)
		return 0, errors.New(errString)
	}

	updateVolumeBitmap(readerWriter, blockList)

	buffer, err = ReadBlock(readerWriter, directoryHeader.ParentBlock)
	if err != nil {
		errString := fmt.Sprintf("failed to read parent block to expand directory: %s", err)
		return 0, errors.New(errString)
	}
	directoryEntryOffset := directoryHeader.ParentEntry*uint16(directoryHeader.EntryLength) + 0x04
	directoryFileEntry := parseFileEntry(buffer[directoryEntryOffset:directoryEntryOffset+0x28], directoryHeader.ParentBlock, directoryHeader.ParentEntry*uint16(directoryHeader.EntryLength)+0x04)
	directoryFileEntry.BlocksUsed++
	directoryFileEntry.EndOfFile += 0x200
	err = writeFileEntry(readerWriter, directoryFileEntry)
	if err != nil {
		return 0, err
	}

	return nextBlockNumber, nil
}

func getFileEntriesInDirectory(reader io.ReaderAt, blockNumber uint16, currentPath int, paths []string) (DirectoryHeader, []FileEntry, error) {
	buffer, err := ReadBlock(reader, blockNumber)
	if err != nil {
		return DirectoryHeader{}, nil, err
	}

	directoryHeader := parseDirectoryHeader(buffer, blockNumber)

	fileEntries := make([]FileEntry, directoryHeader.ActiveFileCount)
	entryOffset := uint16(43) // start at offset after header
	activeEntries := uint16(0)
	entryNumber := uint8(2) // header is essentially the first entry so start at 2

	nextBlock := directoryHeader.NextBlock

	matchedDirectory := (currentPath == len(paths)-1) && (paths[currentPath] == directoryHeader.Name)

	if !matchedDirectory && (currentPath == len(paths)-1) {
		// path not matched by last path part
		return DirectoryHeader{}, nil, errors.New("path not matched")
	}

	for {
		if entryNumber > 13 {
			entryOffset = 4
			entryNumber = 1
			if blockNumber == 0 {
				return DirectoryHeader{}, nil, nil
			}
			buffer, err = ReadBlock(reader, nextBlock)
			if err != nil {
				return DirectoryHeader{}, nil, err
			}
			nextBlock = uint16(buffer[2]) + uint16(buffer[3])*256
		}
		fileEntry := parseFileEntry(buffer[entryOffset:entryOffset+40], blockNumber, entryOffset)

		if fileEntry.StorageType != StorageDeleted {
			if matchedDirectory && activeEntries == directoryHeader.ActiveFileCount {
				return directoryHeader, fileEntries[0:activeEntries], nil
			}
			if matchedDirectory {
				fileEntries[activeEntries] = fileEntry
			} else if !matchedDirectory && fileEntry.FileType == 15 && paths[currentPath+1] == fileEntry.FileName {
				return getFileEntriesInDirectory(reader, fileEntry.KeyPointer, currentPath+1, paths)
			}
			activeEntries++
		}

		entryNumber++
		entryOffset += 39
	}
}

func parseFileEntry(buffer []byte, blockNumber uint16, entryOffset uint16) FileEntry {
	storageType := buffer[0] >> 4
	fileNameLength := buffer[0] & 15
	fileName := string(buffer[1 : fileNameLength+1])
	fileType := buffer[16]
	startingBlock := uint16(buffer[17]) + uint16(buffer[18])*256
	blocksUsed := uint16(buffer[19]) + uint16(buffer[20])*256
	endOfFile := uint32(buffer[21]) + uint32(buffer[22])*256 + uint32(buffer[23])*65536
	creationTime := DateTimeFromProDOS(buffer[24:28])
	version := buffer[28]
	minVersion := buffer[29]
	access := buffer[30]
	auxType := uint16(buffer[31]) + uint16(buffer[32])*256
	modifiedTime := DateTimeFromProDOS((buffer[33:37]))
	headerPointer := uint16(buffer[0x25]) + uint16(buffer[0x26])*256

	fileEntry := FileEntry{
		StorageType:     storageType,
		FileName:        fileName,
		FileType:        fileType,
		CreationTime:    creationTime,
		Version:         version,
		MinVersion:      minVersion,
		KeyPointer:      startingBlock,
		BlocksUsed:      blocksUsed,
		EndOfFile:       endOfFile,
		Access:          access,
		AuxType:         auxType,
		ModifiedTime:    modifiedTime,
		HeaderPointer:   headerPointer,
		DirectoryBlock:  blockNumber,
		DirectoryOffset: entryOffset,
	}

	return fileEntry
}

func writeFileEntry(writer io.WriterAt, fileEntry FileEntry) error {
	buffer := make([]byte, 39)
	buffer[0] = byte(fileEntry.StorageType)<<4 + byte(len(fileEntry.FileName))
	for i := 0; i < len(fileEntry.FileName); i++ {
		buffer[i+1] = fileEntry.FileName[i]
	}
	buffer[0x10] = byte(fileEntry.FileType)
	buffer[0x11] = byte(fileEntry.KeyPointer & 0xFF)
	buffer[0x12] = byte(fileEntry.KeyPointer >> 8)
	buffer[0x13] = byte(fileEntry.BlocksUsed & 0xFF)
	buffer[0x14] = byte(fileEntry.BlocksUsed >> 8)
	buffer[0x15] = byte(fileEntry.EndOfFile & 0x0000FF)
	buffer[0x16] = byte(fileEntry.EndOfFile & 0x00FF00 >> 8)
	buffer[0x17] = byte(fileEntry.EndOfFile & 0xFF0000 >> 16)
	creationTime := DateTimeToProDOS(fileEntry.CreationTime)
	for i := 0; i < 4; i++ {
		buffer[0x18+i] = creationTime[i]
	}
	buffer[0x1C] = byte(fileEntry.Version)
	buffer[0x1D] = byte(fileEntry.MinVersion)
	buffer[0x1E] = byte(fileEntry.Access)
	buffer[0x1F] = byte(fileEntry.AuxType & 0x00FF)
	buffer[0x20] = byte(fileEntry.AuxType >> 8)
	modifiedTime := DateTimeToProDOS(fileEntry.ModifiedTime)
	for i := 0; i < 4; i++ {
		buffer[0x21+i] = modifiedTime[i]
	}
	buffer[0x25] = byte(fileEntry.HeaderPointer & 0x00FF)
	buffer[0x26] = byte(fileEntry.HeaderPointer >> 8)

	//fmt.Printf("Writing file entry at block: %04X offset: %04X\n", fileEntry.DirectoryBlock, fileEntry.DirectoryOffset)
	_, err := writer.WriteAt(buffer, int64(fileEntry.DirectoryBlock)*512+int64(fileEntry.DirectoryOffset))
	if err != nil {
		return err
	}

	return nil
}

func parseVolumeHeader(buffer []byte) VolumeHeader {
	nextBlock := uint16(buffer[2]) + uint16(buffer[3])*256
	filenameLength := buffer[4] & 15
	volumeName := string(buffer[5 : filenameLength+5])
	creationTime := DateTimeFromProDOS(buffer[28:32])
	version := buffer[32]
	minVersion := buffer[33]
	entryLength := buffer[35]
	entriesPerBlock := buffer[36]
	fileCount := uint16(buffer[37]) + uint16(buffer[38])*256
	bitmapBlock := uint16(buffer[39]) + uint16(buffer[40])*256
	totalBlocks := uint16(buffer[41]) + uint16(buffer[42])*256

	if minVersion > 0 {
		panic("Unsupported ProDOS version")
	}

	volumeHeader := VolumeHeader{
		VolumeName:       volumeName,
		CreationTime:     creationTime,
		ActiveFileCount:  fileCount,
		BitmapStartBlock: bitmapBlock,
		TotalBlocks:      totalBlocks,
		NextBlock:        nextBlock,
		EntriesPerBlock:  entriesPerBlock,
		EntryLength:      entryLength,
		MinVersion:       minVersion,
		Version:          version,
	}
	return volumeHeader
}

func parseDirectoryHeader(buffer []byte, blockNumber uint16) DirectoryHeader {
	previousBlock := uint16(buffer[0x00]) + uint16(buffer[0x01])*256
	nextBlock := uint16(buffer[0x02]) + uint16(buffer[0x03])*256
	isSubDirectory := (buffer[0x04] & 0xF0) == 0xE0
	filenameLength := buffer[0x04] & 0x0F
	name := string(buffer[0x05 : filenameLength+0x05])
	creationTime := DateTimeFromProDOS(buffer[0x1C:0x20])
	version := buffer[0x20]
	minVersion := buffer[0x21]
	access := buffer[0x22]
	entryLength := buffer[0x23]
	entriesPerBlock := buffer[0x24]
	fileCount := uint16(buffer[0x25]) + uint16(buffer[0x26])*256
	parentBlock := uint16(buffer[0x27]) + uint16(buffer[0x28])*256
	parentEntry := uint16(buffer[0x29])
	parentEntryLength := buffer[0x2A]

	directoryEntry := DirectoryHeader{
		PreviousBlock:     previousBlock,
		NextBlock:         nextBlock,
		StartingBlock:     blockNumber,
		IsSubDirectory:    isSubDirectory,
		Name:              name,
		CreationTime:      creationTime,
		Version:           version,
		MinVersion:        minVersion,
		Access:            access,
		EntryLength:       entryLength,
		EntriesPerBlock:   entriesPerBlock,
		ActiveFileCount:   fileCount,
		ParentBlock:       parentBlock,
		ParentEntry:       parentEntry,
		ParentEntryLength: parentEntryLength,
	}

	return directoryEntry
}

func writeDirectoryHeader(readerWriter ReaderWriterAt, directoryHeader DirectoryHeader) error {
	// Reading back the block preserves values including reserved fields
	buffer, err := ReadBlock(readerWriter, directoryHeader.StartingBlock)
	if err != nil {
		return err
	}
	buffer[0x00] = byte(directoryHeader.PreviousBlock & 0x00FF)
	buffer[0x01] = byte(directoryHeader.PreviousBlock >> 8)
	buffer[0x02] = byte(directoryHeader.NextBlock & 0x00FF)
	buffer[0x03] = byte(directoryHeader.NextBlock >> 8)
	if directoryHeader.IsSubDirectory {
		buffer[0x04] = 0xE0
	} else {
		buffer[0x04] = 0xF0
	}
	buffer[0x04] = buffer[0x04] | byte(len(directoryHeader.Name))
	for i := 0; i < len(directoryHeader.Name); i++ {
		buffer[0x05+i] = directoryHeader.Name[i]
	}
	creationTime := DateTimeToProDOS(directoryHeader.CreationTime)
	for i := 0; i < 4; i++ {
		buffer[0x1C+i] = creationTime[i]
	}
	// Without these reserved bytes, reading the directory causes I/O ERROR
	buffer[0x14] = 0x75
	buffer[0x15] = byte(directoryHeader.Version)
	buffer[0x16] = byte(directoryHeader.MinVersion)
	buffer[0x17] = 0xC3
	buffer[0x18] = 0x0D
	buffer[0x19] = 0x27
	buffer[0x1A] = 0x00
	buffer[0x1B] = 0x00

	buffer[0x20] = byte(directoryHeader.Version)
	buffer[0x21] = byte(directoryHeader.MinVersion)
	buffer[0x22] = byte(directoryHeader.Access)
	buffer[0x23] = byte(directoryHeader.EntryLength)
	buffer[0x24] = byte(directoryHeader.EntriesPerBlock)
	buffer[0x25] = byte(directoryHeader.ActiveFileCount & 0x00FF)
	buffer[0x26] = byte(directoryHeader.ActiveFileCount >> 8)
	buffer[0x27] = byte(directoryHeader.ParentBlock & 0x00FF)
	buffer[0x28] = byte(directoryHeader.ParentBlock >> 8)
	buffer[0x29] = byte(directoryHeader.ParentEntry)
	buffer[0x2A] = byte(directoryHeader.ParentEntryLength)
	WriteBlock(readerWriter, directoryHeader.StartingBlock, buffer)

	return nil
}
