import time
import struct
from machine import Pin, SoftSPI
import uasyncio as asyncio

# Pins
MOSI   = 19
MISO   = 21
SCK    =  5
MIDI   =  8
SD_CS  = 14 # A6
MP3_CS = 32 # A7 NB: This is the command chip. Not sure why it says "MP3" on the silk.
DREQ   = 15 # A8
XDCS   = 33 # A9 NB: This is the data chip. Feed encoded sound files directly to it.

WRITE  = 0x02
READ   = 0x03

SCI_MODE     = 0x00
SCI_STATUS   = 0x01
SCI_CLOCKF   = 0x03
SCI_AUDATA   = 0x05
SCI_WRAM     = 0x06
SCI_WRAMADDR = 0x07
SCI_HDAT0    = 0x08
SCI_HDAT1    = 0x09
SCI_VOL      = 0x0b

SM_DIFF          = 1
SM_RESET         = 1 <<  2
SM_CANCEL        = 1 <<  3
SM_EARSPEAKER_LO = 1 <<  4
SM_EARSPEAKER_HI = 1 <<  7
SM_DACT          = 1 <<  8
SM_SDIORD        = 1 <<  9
SM_LINE1         = 1 << 14
SM_CLK_RANGE     = 1 << 15

ADDR_VERSION = 0x1e02
ADDR_FILL    = 0x1e06

class Vs1053:
    PLAY_BUFFER = 512 * 25

    def __init__(self):
        self.dreq = Pin(DREQ, mode=Pin.IN)
        self.xdcs = Pin(XDCS, mode=Pin.OUT)
        self.xcs = Pin(MP3_CS, mode=Pin.OUT)
        self.spi = SoftSPI(sck=SCK, mosi=MOSI, miso=MISO)
        self.reset()
        # Don't select any chips.
        self.xdcs(1)
        self.xcs(1)

    def wait(self, seconds_ms=1):
        while not self.dreq.value():
            time.sleep_ms(seconds_ms)

    def write_command(self, bytes_):
        # Select the command chip and clock out bytes_ on the SPI.
        self.xcs(0)
        self.spi.write(bytes_)
        self.xcs(1)

    def write_data(self, bytes_):
        # Select the data chip and clock out bytes_ on the SPI.
        self.xdcs(0)
        self.spi.write(bytes_)
        self.xdcs(1)

    def read_register(self, address):
        self.xcs(0)
        self.spi.write(struct.pack('BB', READ, address))
        register = struct.unpack('>H', self.spi.read(2))[0]
        self.xcs(1)
        return register

    def write_register(self, address, short):
        self.write_command(struct.pack('>BBH', WRITE, address, short))
        self.wait()

    def get_status_bit(self, bit):
        return self.read_register(SCI_STATUS) & bit and 1

    def set_status_bit(self, bit):
        self.write_register(SCI_STATUS, self.read_register(SCI_STATUS) | bit)

    def get_mode_bit(self, bit):
        return self.read_register(SCI_MODE) & bit and 1

    def set_mode_bit(self, bit):
        self.write_register(SCI_MODE, self.read_register(SCI_MODE) | bit)

    def clear_mode_bit(self, bit):
        self.write_register(SCI_MODE, self.read_register(SCI_MODE) ^ bit)

    def get_fill_byte(self):
        return bytes([self.read_ram_word(ADDR_FILL) & 0xff])

    def set_volume(self, volume_short):
        # volume_short is a 16-bit value representing volume attenuation from full volume (DS 9.6.11).
        # High nibble is left channel, low is right.
        self.write_register(SCI_VOL, volume_short)

    def read_ram_word(self, address):
        self.write_register(SCI_WRAMADDR, address)
        return self.read_register(SCI_WRAM)

    async def play(self, f):
        while True:
            bytes_ = f.read(32)
            if bytes_ == b'':
                break
            while self.dreq.value() == 0x00:
                await asyncio.sleep(0.01)
            self.write_data(bytes_)
        self.stop()

    def stop(self):
        """ Datasheet 10.5.1 steps 2-7. """
        fill_byte = self.get_fill_byte()
        self.write_data(fill_byte * 2052)
        self.set_mode_bit(SM_CANCEL)
        bytes_sent = 0
        while bytes_sent <= 2048:
            self.write_data(fill_byte * 32)
            bytes_sent += 32
            if not self.get_mode_bit(SM_CANCEL):
                break
        else:
            # Causes an audible pop.  Hope we don't get here.
            self.reset()

    def reset(self):
        # DS 10.3
        self.write_register(SCI_STATUS, self.read_register(SCI_STATUS) | SM_RESET)
        time.sleep_us(5)
        self.wait()

        # Set clock multiplier (SC_MULT) to XTALIx2.0 (DS 9.6.4).
        # Without this, sound quality is horrible and audibly slow.
        self.write_register(SCI_CLOCKF, 0x2000) # Critical.
