package msc

import (
	"errors"
	"machine/usb/msc/csw"
	"machine/usb/msc/scsi"
)

var invalidWriteError = errors.New("invalid write offset or length")

func (m *msc) scsiCmdReadWrite(cmd scsi.Cmd) {
	status := m.validateScsiReadWrite(cmd)
	if status != csw.StatusPassed {
		m.sendScsiError(status, scsi.SenseIllegalRequest, scsi.SenseCodeInvalidCmdOpCode)
	} else if m.totalBytes > 0 {
		if cmd.CmdType() == scsi.CmdRead {
			m.scsiRead(cmd)
		} else {
			// WRITE(10) and UNMAP commands don't take any action until the data stage begins
		}
	} else {
		// Zero byte transfer. No practical use case
		m.state = mscStateStatus
	}
}

// Validate SCSI READ(10) and WRITE(10) commands
func (m *msc) validateScsiReadWrite(cmd scsi.Cmd) csw.Status {
	blockCount := cmd.BlockCount()
	// CBW wrapper transfer length
	if m.totalBytes == 0 {
		// If the SCSI command's block count doesn't loosely match the wrapper's transfer length something's wrong
		if blockCount > 0 {
			return csw.StatusPhaseError
		}
		// Zero length transfer. No practical use case, but explicitly not an error according to the spec
		return csw.StatusPassed
	}
	if (cmd.CmdType() == scsi.CmdRead && m.cbw.isOut()) || (cmd.CmdType() == scsi.CmdWrite && m.cbw.isIn()) {
		// If the command is READ(10) and the data direction is from host to device that's a problem
		// 6.7.3 The Thirteen Cases - Case 10 (Ho <> Di)
		// If the command is WRITE(10) and the data direction is from device to host that's also a problem
		// 6.7.2 The Thirteen Cases - Case 8 (Hi <> Do)
		// https://usb.org/sites/default/files/usbmassbulk_10.pdf
		return csw.StatusPhaseError
	}
	if blockCount == 0 {
		// We already checked for zero length transfer above, so this is a problem
		// 6.7.2 The Thirteen Cases - Case 4 (Hi > Dn)
		// https://usb.org/sites/default/files/usbmassbulk_10.pdf
		return csw.StatusFailed
	}
	if m.totalBytes/blockCount == 0 {
		// Block size shouldn't be small enough to round to zero
		// 6.7.2 The Thirteen Cases - Case 7 (Hi < Di) READ(10) or
		// 6.7.3 The Thirteen Cases - Case 13 (Ho < Do) WRITE(10)
		// https://usb.org/sites/default/files/usbmassbulk_10.pdf
		return csw.StatusPhaseError
	}
	return csw.StatusPassed
}

func (m *msc) usbToRawOffset(lba, offset uint32) (int64, int64) {
	// Convert the emulated block address to the underlying hardware block's start and offset
	rawLBA := (lba*m.blockSizeUSB + offset) / m.blockSizeRaw
	rawBlockOffset := int64((lba*m.blockSizeUSB + offset) % m.blockSizeRaw)
	return int64(m.blockOffset + rawLBA*m.blockSizeRaw), rawBlockOffset
}

func (m *msc) readBlock(b []byte, lba, offset uint32) (n int, err error) {
	// Convert the emulated block address to the underlying hardware block's start and offset
	blockStart, blockOffset := m.usbToRawOffset(lba, offset)

	// Read a full block from the underlying device into the block cache
	n, err = m.dev.ReadAt(m.blockCache, blockStart)
	n -= int(blockOffset)
	if n > len(b) {
		n = len(b)
	}

	copy(b, m.blockCache[blockOffset:])

	return n, err
}

func (m *msc) writeBlock(b []byte, lba, offset uint32) (n int, err error) {
	// Convert the emulated block address to the underlying hardware block's start and offset
	blockStart, blockOffset := m.usbToRawOffset(lba, offset)

	if blockOffset != 0 || len(b) != int(m.blockSizeRaw) {
		return 0, invalidWriteError
	}

	// Write the full block to the underlying device
	n, err = m.dev.WriteAt(b, blockStart)
	n -= int(blockOffset)
	if n > len(b) {
		n = len(b)
	}

	return n, err
}

func (m *msc) scsiRead(cmd scsi.Cmd) {
	// Make sure we don't exceed the buffer size
	readEnd := m.totalBytes - m.sentBytes
	if readEnd > m.maxPacketSize {
		readEnd = m.maxPacketSize
	}
	// Resize the buffer to fit the read size
	m.resetBuffer(int(readEnd))

	// Read data from the emulated block device
	n, err := m.readBlock(m.buf[:readEnd], cmd.LBA(), m.sentBytes)
	if err != nil || n == 0 {
		m.sendScsiError(csw.StatusFailed, scsi.SenseNotReady, scsi.SenseCodeMediumNotPresent)
		return
	}

	m.queuedBytes = readEnd
	m.sendUSBPacket(m.buf)
}

func (m *msc) scsiWrite(cmd scsi.Cmd, b []byte) {
	if m.readOnly {
		m.sendScsiError(csw.StatusFailed, scsi.SenseDataProtect, scsi.SenseCodeWriteProtected)
		return
	}

	// Write data to the block device
	n, err := m.writeBlock(b, cmd.LBA(), m.sentBytes)
	if err != nil || n < len(b) {
		m.sentBytes += uint32(n)
		m.sendScsiError(csw.StatusFailed, scsi.SenseNotReady, scsi.SenseCodeMediumNotPresent)
	} else {
		m.sentBytes += uint32(len(b))
	}

	if m.sentBytes >= m.totalBytes {
		// Data transfer is complete, send CSW
		m.state = mscStateStatus
		m.run([]byte{}, true)
	}
}
