package blefs

import (
	"bytes"
	"encoding/binary"
	"errors"
	"time"

	"github.com/muka/go-bluetooth/bluez"
	"github.com/muka/go-bluetooth/bluez/profile/gatt"
)

var (
	ErrFSUnexpectedResponse = errors.New("unexpected response returned by filesystem")
	ErrFSResponseTimeout    = errors.New("timed out waiting for response")
	ErrFSError              = errors.New("error reported by filesystem")
)

const (
	FSStatusOk    = 0x01
	FSStatusError = 0x02
)

// Filesystem command
const (
	FSCmdReadFile  = 0x10
	FSCmdDataReq   = 0x12
	FSCmdWriteFile = 0x20
	FSCmdTransfer  = 0x22
	FSCmdDelete    = 0x30
	FSCmdMkdir     = 0x40
	FSCmdListDir   = 0x50
	FSCmdMove      = 0x60
)

// Filesystem response
const (
	FSResponseReadFile  = 0x11
	FSResponseWriteFile = 0x21
	FSResponseDelete    = 0x31
	FSResponseMkdir     = 0x41
	FSResponseListDir   = 0x51
	FSResponseMove      = 0x61
)

// btOptsCmd cause a write command rather than a wrire request
var btOptsCmd = map[string]interface{}{"type": "command"}

// FS implements the fs.FS interface for the Adafruit BLE FS protocol
type FS struct {
	transferChar   *gatt.GattCharacteristic1
	transferRespCh <-chan *bluez.PropertyChanged
	readOpen       bool
	writeOpen      bool
}

// New creates a new fs given the transfer characteristic
func New(transfer *gatt.GattCharacteristic1) (*FS, error) {
	// Create new FS instance
	out := &FS{transferChar: transfer}

	// Start notifications on transfer characteristic
	err := out.transferChar.StartNotify()
	if err != nil {
		return nil, err
	}
	// Watch properties of transfer characteristic
	ch, err := out.transferChar.WatchProperties()
	if err != nil {
		return nil, err
	}
	// Create buffered channel for propery change events
	bufCh := make(chan *bluez.PropertyChanged, 10)
	go func() {
		// Relay all messages from original channel to buffered
		for val := range ch {
			bufCh <- val
		}
	}()
	// Set transfer response channel to buffered channel
	out.transferRespCh = bufCh
	return out, nil
}

func (blefs *FS) Close() error {
	return blefs.transferChar.StopNotify()
}

// request makes a request on the transfer characteristic
func (blefs *FS) request(cmd byte, padding bool, data ...interface{}) error {
	// Encode data as binary
	dataBin, err := encode(data...)
	if err != nil {
		return err
	}
	bin := []byte{cmd}
	if padding {
		bin = append(bin, 0x00)
	}
	// Append encoded data to command with one byte of padding
	bin = append(bin, dataBin...)
	// Write value to characteristic
	err = blefs.transferChar.WriteValue(bin, btOptsCmd)
	if err != nil {
		return err
	}
	return nil
}

// on waits for the given command to be received on
// the control point characteristic, then runs the callback.
func (blefs *FS) on(resp byte, onCmdCb func(data []byte) error) error {
	// Use for loop in case of invalid property
	for {
		select {
		case propChanged := <-blefs.transferRespCh:
			// If property was invalid
			if propChanged.Name != "Value" {
				// Keep waiting
				continue
			}
			// Assert propery value as byte slice
			data := propChanged.Value.([]byte)
			// If command has prefix of given command
			if data[0] == resp {
				// Return callback with data after command
				return onCmdCb(data[1:])
			}
			return ErrFSUnexpectedResponse
		case <-time.After(time.Minute):
			return ErrFSResponseTimeout
		}
	}
}

// encode encodes go values to binary
func encode(data ...interface{}) ([]byte, error) {
	// Create new buffer
	buf := &bytes.Buffer{}
	// For every data element
	for _, elem := range data {
		switch val := elem.(type) {
		case string:
			// Write string to buffer
			if _, err := buf.WriteString(val); err != nil {
				return nil, err
			}
		case []byte:
			// Write bytes to buffer
			if _, err := buf.Write(val); err != nil {
				return nil, err
			}
		default:
			// Encode and write value as little endian binary
			if err := binary.Write(buf, binary.LittleEndian, val); err != nil {
				return nil, err
			}
		}
	}
	// Return bytes from buffer
	return buf.Bytes(), nil
}

// decode reads binary into pointers given in vals
func decode(data []byte, vals ...interface{}) error {
	offset := 0
	for _, elem := range vals {
		// If at end of data, stop
		if offset == len(data) {
			break
		}
		switch val := elem.(type) {
		case *string:
			// Set val to string starting from offset
			*val = string(data[offset:])
			// Add string length to offset
			offset += len(data) - offset
		case *[]byte:
			// Set val to byte slice starting from offset
			*val = data[offset:]
			// Add slice length to offset
			offset += len(data) - offset
		default:
			// Create new reader for data starting from offset
			reader := bytes.NewReader(data[offset:])
			// Read binary into value pointer
			err := binary.Read(reader, binary.LittleEndian, val)
			if err != nil {
				return err
			}
			// Add size of value to offset
			offset += binary.Size(val)
		}
	}
	return nil
}

// maxData returns MTU-20. This is the maximum amount of data
// to send in a packet. Subtracting 20 ensures that the MTU
// is never exceeded.
func (blefs *FS) maxData() uint16 {
	mtu := blefs.transferChar.Properties.MTU
	// If MTU is zero, the current version of BlueZ likely
	// doesn't support the MTU property, so assume 256.
	if mtu == 0 {
		mtu = 256
	}
	return mtu - 20
}

// padding returns a slice of len amount of 0x00.
func padding(len int) []byte {
	return make([]byte, len)
}

// bytesHuman returns a human-readable string for
// the amount of bytes inputted.
func bytesHuman(b uint32) (float64, string) {
	const unit = 1000
	// Set possible units prefixes (PineTime flash is 4MB)
	units := [2]rune{'k', 'M'}
	// If amount of bytes is less than smallest unit
	if b < unit {
		// Return unchanged with unit "B"
		return float64(b), "B"
	}

	div, exp := uint32(unit), 0
	// Get decimal values and unit prefix index
	for n := b / unit; n >= unit; n /= unit {
		div *= unit
		exp++
	}

	// Create string for full unit
	unitStr := string([]rune{units[exp], 'B'})

	// Return decimal with unit string
	return float64(b) / float64(div), unitStr
}