package sd

import (
	"errors"
	"io"
	"math/bits"
)

var (
	errNegativeOffset = errors.New("sd: negative offset")
)

// Compile time guarantee of interface implementation.
var _ Card = (*SPICard)(nil)
var _ io.ReaderAt = (*BlockDevice)(nil)
var _ io.WriterAt = (*BlockDevice)(nil)

// Card is the interface implemented by SD card drivers such as [SPICard].
// It provides block-aligned I/O over the card's contents. Use [NewBlockDevice]
// to wrap a Card with byte-addressed [io.ReaderAt] and [io.WriterAt] interfaces.
type Card interface {
	// WriteBlocks writes the given data to the card, starting at the given block index.
	// The data must be a multiple of the block size.
	WriteBlocks(data []byte, startBlockIdx int64) (int, error)
	// ReadBlocks reads the given number of blocks from the card, starting at the given block index.
	// The dst buffer must be a multiple of the block size.
	ReadBlocks(dst []byte, startBlockIdx int64) (int, error)
	// EraseBlocks erases blocks starting at startBlockIdx to startBlockIdx+numBlocks.
	EraseBlocks(startBlock, numBlocks int64) error
}

// NewBlockDevice creates a new [BlockDevice] from a Card. blockSize must be a
// power of 2. For an initialized [SPICard], blockSize is typically the CSD's
// [CSD.ReadBlockLen] and numBlocks is [SPICard.NumberOfBlocks].
func NewBlockDevice(card Card, blockSize int, numBlocks int64) (*BlockDevice, error) {
	if card == nil || blockSize <= 0 || numBlocks <= 0 {
		return nil, errors.New("invalid argument(s)")
	}
	blk, err := makeBlockIndexer(blockSize)
	if err != nil {
		return nil, err
	}
	bd := &BlockDevice{
		card:      card,
		blockbuf:  make([]byte, blockSize),
		blk:       blk,
		numblocks: int64(numBlocks),
	}
	return bd, nil
}

// BlockDevice implements the tinyfs.BlockDevice interface for a [Card],
// providing byte-addressed reads and writes at arbitrary offsets by buffering
// non-block-aligned accesses through an internal single-block buffer.
// BlockDevice is not safe for concurrent use.
type BlockDevice struct {
	card      Card
	blockbuf  []byte
	blk       blkIdxer
	numblocks int64
}

// ReadAt implements the [io.ReaderAt] interface for an SD card.
// Reads need not be aligned to block boundaries.
func (bd *BlockDevice) ReadAt(p []byte, off int64) (n int, err error) {
	if off < 0 {
		return 0, errNegativeOffset
	}

	blockIdx := bd.blk.idx(off)
	blockOff := bd.blk.off(off)
	if blockOff != 0 {
		// Non-aligned first block case.
		if _, err = bd.card.ReadBlocks(bd.blockbuf, blockIdx); err != nil {
			return n, err
		}
		n += copy(p, bd.blockbuf[blockOff:])
		p = p[n:]
		blockIdx++
	}

	fullBlocksToRead := bd.blk.idx(int64(len(p)))
	if fullBlocksToRead > 0 {
		// 1 or more full blocks case.
		endOffset := fullBlocksToRead * bd.blk.size()
		ngot, err := bd.card.ReadBlocks(p[:endOffset], blockIdx)
		if err != nil {
			return n + ngot, err
		}
		p = p[endOffset:]
		n += ngot
		blockIdx += fullBlocksToRead
	}

	if len(p) > 0 {
		// Non-aligned last block case.
		if _, err := bd.card.ReadBlocks(bd.blockbuf, blockIdx); err != nil {
			return n, err
		}
		n += copy(p, bd.blockbuf)
	}
	return n, nil
}

// WriteAt implements the [io.WriterAt] interface for an SD card. Writes need
// not be aligned to block boundaries: partial blocks are read, modified and
// written back.
func (bd *BlockDevice) WriteAt(p []byte, off int64) (n int, err error) {
	if off < 0 {
		return 0, errNegativeOffset
	}

	blockIdx := bd.blk.idx(off)
	blockOff := bd.blk.off(off)
	if blockOff != 0 {
		// Non-aligned first block case.
		if _, err := bd.card.ReadBlocks(bd.blockbuf, blockIdx); err != nil {
			return n, err
		}
		nexpect := copy(bd.blockbuf[blockOff:], p)
		ngot, err := bd.card.WriteBlocks(bd.blockbuf, blockIdx)
		if err != nil {
			return n, err
		} else if ngot != len(bd.blockbuf) {
			return n, io.ErrShortWrite
		}
		n += nexpect
		p = p[nexpect:]
		blockIdx++
	}

	fullBlocksToWrite := bd.blk.idx(int64(len(p)))
	if fullBlocksToWrite > 0 {
		// 1 or more full blocks case.
		endOffset := fullBlocksToWrite * bd.blk.size()
		ngot, err := bd.card.WriteBlocks(p[:endOffset], blockIdx)
		n += ngot
		if err != nil {
			return n, err
		} else if ngot != int(endOffset) {
			return n, io.ErrShortWrite
		}
		p = p[ngot:]
		blockIdx += fullBlocksToWrite
	}

	if len(p) > 0 {
		// Non-aligned last block case.
		if _, err := bd.card.ReadBlocks(bd.blockbuf, blockIdx); err != nil {
			return n, err
		}
		copy(bd.blockbuf, p)
		ngot, err := bd.card.WriteBlocks(bd.blockbuf, blockIdx)
		if err != nil {
			return n, err
		} else if ngot != len(bd.blockbuf) {
			return n, io.ErrShortWrite
		}
		n += len(p)
	}
	return n, nil
}

// Size returns the number of bytes in this block device.
func (bd *BlockDevice) Size() int64 {
	return bd.BlockSize() * bd.numblocks
}

// BlockSize returns the size of a block in bytes.
func (bd *BlockDevice) BlockSize() int64 {
	return bd.blk.size()
}

// 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.
func (bd *BlockDevice) EraseBlocks(startEraseBlockIdx, len int64) error {
	return bd.card.EraseBlocks(startEraseBlockIdx, len)
}

// blkIdxer is a helper for calculating block indices and offsets.
type blkIdxer struct {
	blockshift int64
	blockmask  int64
}

// makeBlockIndexer returns a blkIdxer for the given block size,
// which must be a power of 2.
func makeBlockIndexer(blockSize int) (blkIdxer, error) {
	if blockSize <= 0 {
		return blkIdxer{}, errNoblocks
	}
	tz := bits.TrailingZeros(uint(blockSize))
	if blockSize>>tz != 1 {
		return blkIdxer{}, errors.New("blockSize must be a power of 2")
	}
	blk := blkIdxer{
		blockshift: int64(tz),
		blockmask:  (1 << tz) - 1,
	}
	return blk, nil
}

// size returns the size of a block in bytes.
func (blk *blkIdxer) size() int64 {
	return 1 << blk.blockshift
}

// off gets the offset of the byte at byteIdx from the start of its block.
//
//go:inline
func (blk *blkIdxer) off(byteIdx int64) int64 {
	return blk._moduloBlockSize(byteIdx)
}

// idx gets the block index that contains the byte at byteIdx.
//
//go:inline
func (blk *blkIdxer) idx(byteIdx int64) int64 {
	return blk._divideBlockSize(byteIdx)
}

// modulo and divide are defined in terms of bit operations for speed since
// blockSize is a power of 2.

//go:inline
func (blk *blkIdxer) _moduloBlockSize(n int64) int64 { return n & blk.blockmask }

//go:inline
func (blk *blkIdxer) _divideBlockSize(n int64) int64 { return n >> blk.blockshift }
