package i6502

import (
	"fmt"
)

/*
The AddressBus contains a list of all attached memory components,
like Ram, Rom and IO. It takes care of mapping the global 16-bit
address space of the Cpu to the relative memory addressing of
each component.
*/
type AddressBus struct {
	addressables []*addressable // Different components
}

type addressable struct {
	memory Memory // Actual memory
	start  uint16 // First address in address space
	end    uint16 // Last address in address space
}

func (a *addressable) String() string {
	return fmt.Sprintf("\t0x%04X-%04X\n", a.start, a.end)
}

// Creates a new, empty 16-bit AddressBus
func NewAddressBus() (*AddressBus, error) {
	return &AddressBus{addressables: make([]*addressable, 0)}, nil
}

// Returns a string with details about the AddressBus and attached memory
func (a *AddressBus) String() string {
	output := "Address Bus:\n"

	for _, addressable := range a.addressables {
		output += addressable.String()
	}

	return output
}

/*
Attach the given Memory at the specified memory offset.

To attach 16kB ROM at 0xC000-FFFF, you simple attach the Rom at
address 0xC000, the Size of the Memory determines the end-address.

    rom, _ := i6502.NewRom(0x4000)
    bus.Attach(rom, 0xC000)
*/
func (a *AddressBus) Attach(memory Memory, offset uint16) {
	start := offset
	end := offset + memory.Size() - 1
	addressable := addressable{memory: memory, start: start, end: end}

	a.addressables = append(a.addressables, &addressable)
}

/*
Read an 8-bit value from Memory attached at the 16-bit address.

This will panic if you try to read from an address that has no Memory attached.
*/
func (a *AddressBus) ReadByte(address uint16) byte {
	addressable, err := a.addressableForAddress(address)
	if err != nil {
		panic(err)
	}

	return addressable.memory.ReadByte(address - addressable.start)
}

/*
Convenience method to quickly read a 16-bit value from address and address + 1.

Note that we first read the LOW byte from address and then the HIGH byte from address + 1.
*/
func (a *AddressBus) Read16(address uint16) uint16 {
	lo := uint16(a.ReadByte(address))
	hi := uint16(a.ReadByte(address + 1))

	return (hi << 8) | lo
}

/*
Write an 8-bit value to the Memory at the 16-bit address.

This will panic if you try to write to an address that has no Memory attached or
Memory that is read-only, like Rom.
*/
func (a *AddressBus) WriteByte(address uint16, data byte) {
	addressable, err := a.addressableForAddress(address)
	if err != nil {
		panic(err)
	}

	addressable.memory.WriteByte(address-addressable.start, data)
}

/*
Convenience method to quickly write a 16-bit value to address and address + 1.

Note that the LOW byte will be stored in address and the high byte in address + 1.
*/
func (a *AddressBus) Write16(address uint16, data uint16) {
	a.WriteByte(address, byte(data))
	a.WriteByte(address+1, byte(data>>8))
}

// Returns the addressable for the specified address, or an error if no addressable exists.
func (a *AddressBus) addressableForAddress(address uint16) (*addressable, error) {
	for _, addressable := range a.addressables {
		if addressable.start <= address && addressable.end >= address {
			return addressable, nil
		}
	}

	return nil, fmt.Errorf("No addressable memory found at 0x%04X", address)
}
