package blefs

import (
	"io"
	"io/fs"
	"path/filepath"
	"time"
)

// File represents a file on the BLE filesystem
type File struct {
	fs            *FS
	path          string
	offset        uint32
	offsetCh      chan uint32
	length        uint32
	amtLeft       uint32
	amtTferd      uint32
	isReadOnly    bool
	isWriteOnly   bool
	offsetChanged bool
}

// Open opens a file and returns it as an fs.File to
// satisfy the fs.FS interface
func (blefs *FS) Open(path string) (*File, error) {
	if blefs.readOpen {
		return nil, ErrReadOpen
	}
	blefs.readOpen = true
	// Make a read file request. This opens the file for reading.
	err := blefs.request(
		FSCmdReadFile,
		true,
		uint16(len(path)),
		uint32(0),
		uint32(blefs.maxData()),
		path,
	)
	if err != nil {
		return nil, err
	}
	return &File{
		fs:          blefs,
		path:        path,
		length:      0,
		offset:      0,
		offsetCh:    make(chan uint32, 5),
		isReadOnly:  true,
		isWriteOnly: false,
	}, nil
}

// Create makes a new file on the BLE file system and returns it.
func (blefs *FS) Create(path string, size uint32) (*File, error) {
	if blefs.writeOpen {
		return nil, ErrWriteOpen
	}
	blefs.writeOpen = true
	// Make a write file request. This will create and open a file for writing.
	err := blefs.request(
		FSCmdWriteFile,
		true,
		uint16(len(path)),
		uint32(0),
		uint64(time.Now().UnixNano()),
		size,
		path,
	)
	if err != nil {
		return nil, err
	}
	return &File{
		fs:          blefs,
		path:        path,
		length:      size,
		amtLeft:     size,
		offset:      0,
		offsetCh:    make(chan uint32, 5),
		isReadOnly:  false,
		isWriteOnly: true,
	}, nil
}

// Size returns the total size of the opened file
func (file *File) Size() uint32 {
	return file.length
}

// Progress returns a channel that receives the amount
// of bytes sent as they are sent
func (file *File) Progress() <-chan uint32 {
	return file.offsetCh
}

// Read reads data from a file into b
func (fl *File) Read(b []byte) (n int, err error) {
	// If file is write only (opened by FS.Create())
	if fl.isWriteOnly {
		return 0, ErrFileWriteOnly
	}

	// If offset has been changed (Seek() called)
	if fl.offsetChanged {
		// Create new read file request with the specified offset to restart reading
		err := fl.fs.request(
			FSCmdReadFile,
			true,
			uint16(len(fl.path)),
			fl.offset,
			uint32(fl.fs.maxData()),
			fl.path,
		)
		if err != nil {
			return 0, err
		}
		// Reset offsetChanged
		fl.offsetChanged = false
	}

	// Get length of b. This will be the maximum amount that can be read.
	maxLen := uint32(len(b))
	if maxLen == 0 {
		return 0, nil
	}
	var buf []byte
	for {
		// If amount transfered equals max length
		if fl.amtTferd == maxLen {
			// Reset amount transfered
			fl.amtTferd = 0
			// Copy buffer contents to b
			copy(b, buf)
			// Return max length with no error
			return int(maxLen), nil
		}
		// Create new empty fileReadResponse
		resp := fileReadResponse{}
		// Upon receiving 0x11 (FSResponseReadFile)
		err := fl.fs.on(FSResponseReadFile, func(data []byte) error {
			// Read binary data into struct
			err := decode(
				data,
				&resp.status,
				&resp.padding,
				&resp.offset,
				&resp.length,
				&resp.chunkLen,
				&resp.data,
			)
			if err != nil {
				return err
			}
			// If status is not ok
			if resp.status != FSStatusOk {
				return FSError{resp.status}
			}
			return nil
		})
		if err != nil {
			return 0, err
		}
		// If entire file transferred, break
		if fl.offset == resp.length {
			break
		}

		// Append data returned in response to buffer
		buf = append(buf, resp.data...)
		// Set file length
		fl.length = resp.length
		// Add returned chunk length to offset and amount transferred
		fl.offset += resp.chunkLen
		fl.offsetCh <- fl.offset
		fl.amtTferd += resp.chunkLen

		// Calculate amount of bytes to be sent in next request
		chunkLen := min(fl.length-fl.offset, uint32(fl.fs.maxData()))
		// If after transferring, there will be more data than max length
		if fl.amtTferd+chunkLen > maxLen {
			// Set chunk length to amount left to fill max length
			chunkLen = maxLen - fl.amtTferd
		}
		// Make data request. This will return more data from the file.
		fl.fs.request(
			FSCmdDataReq,
			false,
			byte(FSStatusOk),
			padding(2),
			fl.offset,
			chunkLen,
		)
	}
	close(fl.offsetCh)
	// Copy buffer contents to b
	copied := copy(b, buf)
	// Return amount of bytes copied with EOF error
	return copied, io.EOF
}

// Write writes data from b into a file on the BLE filesysyem
func (fl *File) Write(b []byte) (n int, err error) {
	maxLen := uint32(cap(b))
	// If file is read only (opened by FS.Open())
	if fl.isReadOnly {
		return 0, ErrFileReadOnly
	}

	// If offset has been changed (Seek() called)
	if fl.offsetChanged {
		// Create new write file request with the specified offset to restart writing
		err := fl.fs.request(
			FSCmdWriteFile,
			true,
			uint16(len(fl.path)),
			fl.offset,
			uint64(time.Now().UnixNano()),
			fl.length,
			fl.path,
		)
		if err != nil {
			return 0, err
		}
		// Reset offsetChanged
		fl.offsetChanged = false
	}

	for {
		// If amount transfered equals max length
		if fl.amtTferd == maxLen {
			// Reset amount transfered
			fl.amtTferd = 0
			// Return max length with no error
			return int(maxLen), nil
		}

		// Create new empty fileWriteResponse
		resp := fileWriteResponse{}
		// Upon receiving 0x21 (FSResponseWriteFile)
		err := fl.fs.on(FSResponseWriteFile, func(data []byte) error {
			// Read binary data into struct
			err := decode(
				data,
				&resp.status,
				&resp.padding,
				&resp.offset,
				&resp.modtime,
				&resp.free,
			)
			if err != nil {
				return err
			}
			// If status is not ok
			if resp.status != FSStatusOk {
				return FSError{resp.status}
			}
			return nil
		})

		if err != nil {
			return 0, err
		}
		// If no free space left in current file, break
		if resp.free == 0 {
			break
		}

		// Calculate amount of bytes to be transferred in next request
		chunkLen := min(fl.length-fl.offset, uint32(fl.fs.maxData()))
		// If after transferring, there will be more data than max length
		if fl.amtTferd+chunkLen > maxLen {
			// Set chunk length to amount left to fill max length
			chunkLen = maxLen - fl.amtTferd
		}
		// Get data from b
		chunk := b[fl.amtTferd : fl.amtTferd+chunkLen]
		// Create transfer request. This will transfer the chunk to the file.
		fl.fs.request(
			FSCmdTransfer,
			false,
			byte(FSStatusOk),
			padding(2),
			fl.offset,
			chunkLen,
			chunk,
		)
		// Add chunk length to offset and amount transferred
		fl.offset += chunkLen
		fl.offsetCh <- fl.offset
		fl.amtTferd += chunkLen
	}

	close(fl.offsetCh)
	return int(fl.offset), nil
}

// WriteString converts the string to []byte and calls Write()
func (fl *File) WriteString(s string) (n int, err error) {
	return fl.Write([]byte(s))
}

// Seek changes the offset of the file being read/written.
// This can only be done once in between reads/writes.
func (fl *File) Seek(offset int64, whence int) (int64, error) {
	if fl.offsetChanged {
		return int64(fl.offset), ErrOffsetChanged
	}
	var newOffset int64
	switch whence {
	case io.SeekCurrent:
		newOffset = int64(fl.offset) + offset
	case io.SeekStart:
		newOffset = offset
	case io.SeekEnd:
		newOffset = int64(fl.length) + offset
	default:
		newOffset = int64(fl.offset)
	}
	if newOffset < 0 || uint32(newOffset) > fl.length {
		return int64(fl.offset), ErrInvalidOffset
	}
	fl.offset = uint32(newOffset)
	fl.offsetChanged = true
	return int64(newOffset), nil
}

// Close must be called before opening another file
func (fl *File) Close() error {
	if fl.isReadOnly {
		fl.fs.readOpen = false
	} else if fl.isWriteOnly {
		fl.fs.writeOpen = false
	}

	return nil
}

// Stat does a RedDir() and finds the current file in the output
func (fl *File) Stat() (fs.FileInfo, error) {
	// Get directory in filepath
	dir := filepath.Dir(fl.path)
	// Read directory
	dirEntries, err := fl.fs.ReadDir(dir)
	if err != nil {
		return nil, err
	}
	for _, entry := range dirEntries {
		// If file name is base name of path
		if entry.Name() == filepath.Base(fl.path) {
			// Return file info
			return entry.Info()
		}
	}
	return nil, ErrFileNotExists
}

// fileReadResponse represents a response for a read request
type fileReadResponse struct {
	status   int8
	padding  [2]byte
	offset   uint32
	length   uint32
	chunkLen uint32
	data     []byte
}

// fileWriteResponse represents a response for a write request
type fileWriteResponse struct {
	status  int8
	padding [2]byte
	offset  uint32
	modtime uint64
	free    uint32
}

// min returns the smaller uint32 out of two given
func min(o, t uint32) uint32 {
	if t < o {
		return t
	}
	return o
}