package main

import (
	"bytes"
	"fmt"
	"os"
	"strings"
	"time"

	_ "embed"

	"github.com/BertoldVdb/ms-tools/mshal"
)

type DumpROM struct {
	Filename string `arg help:"File to write dump to."`
}

type dumpCodeParams struct {
	addrMailbox int
	addrTemp    int
	addrTempLen int
	addrLoad    int
	addrHook    int
	valueHook   byte
}

func (d *DumpROM) Run(c *Context) error {
	var p dumpCodeParams

	devType := c.hal.GetDeviceType()
	if strings.Contains(devType, "MS2106") {
		p.addrMailbox = 0xCF10
		p.addrTemp = 0xCD00
		p.addrTempLen = 256
		p.addrLoad = 0xC4A0
		p.addrHook = 9
		p.valueHook = 0x96
	} else if strings.Contains(devType, "MS2107") {
		p.addrMailbox = 0xD000
		p.addrTemp = 0xD100
		p.addrTempLen = 256
		p.addrLoad = 0xC800
		p.addrHook = 8
		p.valueHook = 1
	} else if strings.Contains(devType, "MS2109") {
		p.addrMailbox = 0xCBF0
		p.addrTemp = 0xD300
		p.addrTempLen = 256
		p.addrLoad = 0xCC20
		p.addrHook = 4
		p.valueHook = 1 << 2
	} else if strings.Contains(devType, "MS2130") {
		p.addrMailbox = 0x7C00
		p.addrTemp = 0x7D00
		p.addrTempLen = 256
		p.addrHook = 0x7D00
		p.addrHook = 8
		p.valueHook = 1
	} else {
		return mshal.ErrorUnknownDevice
	}

	code, err := d.work(c.hal, p)
	if err != nil {
		return err
	}

	return os.WriteFile(d.Filename, code, 0644)
}

//go:embed asm/dumprom.bin
var dumpBlobBase []byte

func (d *DumpROM) work(ms *mshal.HAL, p dumpCodeParams) ([]byte, error) {
	dumpBlob := bytes.Replace(dumpBlobBase, []byte{0xDE, 0xAD}, []byte{byte(p.addrMailbox >> 8), byte(p.addrMailbox)}, -1)

	tmpBufLen := 1 + int(0xFF-byte(p.addrTemp))
	if tmpBufLen > p.addrTempLen {
		tmpBufLen = p.addrTempLen
	}
	if tmpBufLen > 255 {
		tmpBufLen = 255
	}

	config := ms.MemoryRegionGet(mshal.MemoryRegionUserConfig)
	configOld := make([]byte, config.GetLength())
	configNew := make([]byte, config.GetLength())

	/* Read orig hooks */
	if _, err := config.Access(false, 0, configOld); err != nil {
		return nil, err
	}

	/* Disable all userhooks */
	if _, err := config.Access(true, 0, configNew); err != nil {
		return nil, err
	}

	/* Disable reading */
	xdata := ms.MemoryRegionGet(mshal.MemoryRegionRAM)
	if err := mshal.WriteByte(xdata, p.addrMailbox, 0); err != nil {
		return nil, err
	}

	/* Read original code */
	orig := make([]byte, len(dumpBlob))
	_, err := xdata.Access(false, p.addrLoad, orig)
	if err != nil {
		return nil, nil
	}

	/* Ensure CPU left the affected area */
	time.Sleep(time.Second)

	/* Write new code */
	if p.addrLoad > 0 {
		if _, err := xdata.Access(true, p.addrLoad, dumpBlob); err != nil {
			return nil, err
		}
	}

	/* Enable USB/Periodic hook */
	if err := mshal.WriteByte(config, p.addrHook, p.valueHook); err != nil {
		return nil, err
	}

	buf := make([]byte, 65536)

	addr := 0
	lastAddr := addr + len(buf)
	index := 0

	for {
		config := []byte{byte(addr >> 8), byte(addr), byte(p.addrTemp >> 8), byte(p.addrTemp)}
		remaining := lastAddr - addr
		if remaining == 0 {
			break
		}
		if remaining > tmpBufLen {
			remaining = tmpBufLen
		}

		if _, err := xdata.Access(true, p.addrMailbox+1, config); err != nil {
			return nil, err
		}

		if err := mshal.WriteByte(xdata, p.addrMailbox, byte(remaining)); err != nil {
			return nil, err
		}

		timeout := time.Now().Add(500 * time.Millisecond)
		for {
			ack, err := mshal.ReadByte(xdata, p.addrMailbox)
			if err != nil {
				return nil, err
			}

			if ack != 0 {
				if time.Now().After(timeout) {
					return nil, mshal.ErrorPatchFailed
				}
			} else {
				break
			}

			time.Sleep(20 * time.Millisecond)
		}

		_, err = xdata.Access(false, p.addrTemp, buf[index:(index+remaining)])
		if err != nil {
			return nil, err
		}

		addr += remaining
		index += remaining
		fmt.Printf("Dumping code: %d bytes read.\n", addr)
	}

	/* Disable USB hook */
	if err := mshal.WriteByte(config, p.addrHook, 0); err != nil {
		return nil, err
	}

	/* Ensure CPU left code */
	time.Sleep(25 * time.Millisecond)

	/* Put original code back */
	if p.addrLoad > 0 {
		/* Remove overwritten code from dump */
		buf = bytes.ReplaceAll(buf, dumpBlob, orig)

		if _, err := xdata.Access(true, p.addrLoad, orig); err != nil {
			return nil, err
		}
	}

	/* Re-enable old hooks */
	_, err = config.Access(true, 0, configOld)
	return buf, err
}
