//go:build stm32f4 || stm32l4 || stm32wlx

package machine

import (
	"device/stm32"

	"bytes"
	"unsafe"
)

// compile-time check for ensuring we fulfill BlockDevice interface
var _ BlockDevice = flashBlockDevice{}

var Flash flashBlockDevice

type flashBlockDevice struct {
}

// ReadAt reads the given number of bytes from the block device.
func (f flashBlockDevice) ReadAt(p []byte, off int64) (n int, err error) {
	if FlashDataStart()+uintptr(off)+uintptr(len(p)) > FlashDataEnd() {
		return 0, errFlashCannotReadPastEOF
	}

	data := unsafe.Slice((*byte)(unsafe.Pointer(FlashDataStart()+uintptr(off))), len(p))
	copy(p, data)

	return len(p), nil
}

// WriteAt writes the given number of bytes to the block device.
// Only double-word (64 bits) length data can be programmed. See rm0461 page 78.
// If the length of p is not long enough it will be padded with 0xFF bytes.
// This method assumes that the destination is already erased.
func (f flashBlockDevice) WriteAt(p []byte, off int64) (n int, err error) {
	if FlashDataStart()+uintptr(off)+uintptr(len(p)) > FlashDataEnd() {
		return 0, errFlashCannotWritePastEOF
	}

	unlockFlash()
	defer lockFlash()

	return writeFlashData(FlashDataStart()+uintptr(off), f.pad(p))
}

// Size returns the number of bytes in this block device.
func (f flashBlockDevice) Size() int64 {
	return int64(FlashDataEnd() - FlashDataStart())
}

// WriteBlockSize returns the block size in which data can be written to
// memory. It can be used by a client to optimize writes, non-aligned writes
// should always work correctly.
func (f flashBlockDevice) WriteBlockSize() int64 {
	return writeBlockSize
}

func eraseBlockSize() int64 {
	return eraseBlockSizeValue
}

// EraseBlockSize returns the smallest erasable area on this particular chip
// in bytes. This is used for the block size in EraseBlocks.
// It must be a power of two, and may be as small as 1. A typical size is 4096.
// TODO: correctly handle processors that have differently sized blocks
// in different areas of memory like the STM32F40x and STM32F1x.
func (f flashBlockDevice) EraseBlockSize() int64 {
	return eraseBlockSize()
}

// EraseBlocks erases the given number of blocks. An implementation may
// transparently coalesce ranges of blocks into larger bundles if the chip
// supports this. The start and len parameters are in block numbers, use
// EraseBlockSize to map addresses to blocks.
// Note that block 0 should map to the address of FlashDataStart().
func (f flashBlockDevice) EraseBlocks(start, len int64) error {
	var address uintptr = uintptr(start*f.EraseBlockSize()) + FlashDataStart()
	blk := int64(address-uintptr(memoryStart)) / f.EraseBlockSize()

	unlockFlash()
	defer lockFlash()

	for i := blk; i < blk+len; i++ {
		if err := eraseBlock(uint32(i)); err != nil {
			return err
		}
	}

	return nil
}

// pad data if needed so it is long enough for correct byte alignment on writes.
func (f flashBlockDevice) pad(p []byte) []byte {
	overflow := int64(len(p)) % f.WriteBlockSize()
	if overflow == 0 {
		return p
	}

	padding := bytes.Repeat([]byte{0xff}, int(f.WriteBlockSize()-overflow))
	return append(p, padding...)
}

const memoryStart = 0x08000000

func unlockFlash() {
	// keys as described rm0461 page 76
	var fkey1 uint32 = 0x45670123
	var fkey2 uint32 = 0xCDEF89AB

	// Wait for the flash memory not to be busy
	for stm32.FLASH.GetSR_BSY() != 0 {
	}

	// Check if the controller is unlocked already
	if stm32.FLASH.GetCR_LOCK() != 0 {
		// Write the first key
		stm32.FLASH.SetKEYR(fkey1)
		// Write the second key
		stm32.FLASH.SetKEYR(fkey2)
	}
}

func lockFlash() {
	stm32.FLASH.SetCR_LOCK(1)
}
