package izapple2

import (
	"fmt"
	"os"
	"strconv"
	"strings"

	"github.com/ivanizag/iz6502"
)

func configure(configuration *configuration) (*Apple2, error) {

	if configuration.getFlag(confShowConfig) {
		configuration.dump()
		os.Exit(0)
	}

	var a Apple2
	a.Name = configuration.get(confName)
	a.mmu = newMemoryManager(&a)
	a.video = newVideo(&a)
	a.io = newIoC0Page(&a)
	a.commandChannel = make(chan command, 100)

	// Configure the board
	board := configuration.get(confBoard)
	a.board = board

	err := setupCharactedGenerator(&a, board, configuration.get(confCharRom))
	if err != nil {
		return nil, err
	}

	addApple2SoftSwitches(a.io)
	switch board {
	case "2plus":
		a.mmu.initMainRAM()
	case "2e":
		a.isApple2e = true
		a.mmu.initMainRAM()
		a.mmu.initExtendedRAM(1)
		a.hasLowerCase = true
		addApple2ESoftSwitches(a.io)
	case "base64a":
		a.mmu.initMainRAM()
		a.hasLowerCase = true
		addBase64aSoftSwitches(a.io)
	case "basis108":
		memBasis108 := newMemoryRangeBasis108()
		videoBasis108 := newVideoBasis108(&a, memBasis108)
		a.mmu.initCustomRAM(memBasis108)
		a.video = videoBasis108
		a.hasLowerCase = true
		addBasis108SoftSwitches(a.io, memBasis108, videoBasis108, a.cg)
	default:
		return nil, fmt.Errorf("board %s not supported it must be '2plus', '2e', 'base64a', 'basis108", board)
	}

	cpu := configuration.get(confCpu)
	switch cpu {
	case "6502":
		a.cpu = iz6502.NewNMOS6502(a.mmu)
	case "65c02":
		a.cpu = iz6502.NewCMOS65c02(a.mmu)
	}

	err = a.loadRom(configuration.get(confRom))
	if err != nil {
		return nil, err
	}

	a.setProfiling(configuration.getFlag(confProfile))
	a.SetForceCaps(configuration.getFlag(confForceCaps))

	err = a.setClockSpeed(configuration.get(confSpeed))
	if err != nil {
		return nil, err
	}

	// Add cards on the slots
	for i := 0; i < 8; i++ {
		cardConfig := configuration.get(fmt.Sprintf("s%v", i))
		if cardConfig != "" {
			_, err := setupCard(&a, i, cardConfig)
			if err != nil {
				return nil, err
			}
		}
	}

	// Add mods
	mods := strings.Split(configuration.get(confMods), ",")
	for _, mod := range mods {
		switch strings.TrimSpace(mod) {
		// case "shift":
		//	 setupShiftedKeyboard(a)
		case "four-colors":
			// This removes the mod to have 6 colors sent by Wozniak to Byte
			// magazine. See: https://archive.org/details/byte-magazine-1979-06/page/n67/mode/2up?view=theater
			a.isFourColors = true
		}
	}

	// Add optional accesories including the aux slot
	ramWorksSize := configuration.get(confRamworks)
	if ramWorksSize != "" && ramWorksSize != "none" {
		err = setupRAMWorksCard(&a, ramWorksSize)
		if err != nil {
			return nil, err
		}
	}

	if configuration.getFlag(confRgb) {
		setupRGBCard(&a)
	}

	nsc := configuration.get(confNsc)
	if nsc != "none" && nsc != "" {
		err = setupNoSlotClock(&a, nsc)
		if err != nil {
			return nil, err
		}
	}

	if configuration.getFlag(confRomx) {
		err := setupRomX(&a)
		if err != nil {
			return nil, err
		}
	}

	err = setupTracers(&a, configuration.get(confTrace))
	if err != nil {
		return nil, err
	}

	return &a, nil
}

func (a *Apple2) setClockSpeed(speed string) error {
	if speed == "full" {
		a.cycleDurationNs = 0
	} else if speed == "ntsc" {
		a.cycleDurationNs = 1000.0 / CPUClockMhz
	} else if speed == "pal" {
		a.cycleDurationNs = 1000.0 / cpuClockEuroMhz
	} else {
		clockMhz, err := strconv.ParseFloat(speed, 64)
		if err != nil {
			return fmt.Errorf("invalid clock speed: %s", speed)
		}
		a.cycleDurationNs = 1000.0 / clockMhz
	}
	return nil
}

func (a *Apple2) setProfiling(value bool) {
	a.profile = value
}

// SetForceCaps allows the caps state to be toggled at runtime
func (a *Apple2) SetForceCaps(value bool) {
	a.forceCaps = value
}

func (a *Apple2) loadRom(filename string) error {
	if filename == "<custom>" {
		switch a.board {
		case "base64a":
			return loadBase64aRom(a)
		case "basis108":
			return loadBasis108Rom(a)
		default:
			return fmt.Errorf("no custom ROM defined for board %s", a.board)
		}
	}

	data, _, err := LoadResource(filename)
	if err != nil {
		return err
	}

	size := len(data)

	romBase := 0x10000 - size
	a.mmu.physicalROM = newMemoryRangeROM(uint16(romBase), data, "Main ROM")
	return nil
}

const pagedRomChipWindowSize = 0x800                                  // 2 KB
const pagedRomChipCount = 6                                           // There has to be six ROM chips
const pagedRomWindowSize = pagedRomChipWindowSize * pagedRomChipCount // To cover 0xd000 to 0xffff
func loadMultiPageRom(a *Apple2, filenames []string) error {
	if len(filenames) != pagedRomChipCount {
		return fmt.Errorf("expected %d ROM files, got %d", pagedRomChipCount, len(filenames))
	}

	// Load the 6 PROM dumps
	proms := make([][]uint8, pagedRomChipCount)
	banks := 1
	for i, filename := range filenames {
		var err error
		proms[i], _, err = LoadResource(filename)
		if err != nil {
			return err
		}
		pages := len(proms[i]) / pagedRomChipWindowSize
		if pages > banks {
			banks = pages
		}
	}

	// Init the array of banks
	romBanksBytes := make([][]uint8, banks)
	for bank := range romBanksBytes {
		romBanksBytes[bank] = make([]uint8, 0, pagedRomWindowSize)
	}

	// Distribute the per chip banks on the full rom banks
	for _, romData := range proms {
		for bank := range romBanksBytes {
			start := (bank * pagedRomChipWindowSize) % len(romData)
			romBanksBytes[bank] = append(romBanksBytes[bank], romData[start:start+pagedRomChipWindowSize]...)
		}
	}

	// Create paged ROM
	romData := make([]uint8, 0, pagedRomWindowSize*banks)
	for _, bank := range romBanksBytes {
		romData = append(romData, bank...)
	}
	rom := newMemoryRangePagedROM(0xd000, romData, "Multipage main ROM", uint8(banks))

	// Start with first bank active
	rom.setPage(0)

	a.mmu.physicalROM = rom
	return nil
}

// CreateConfiguredApple is a device independent main. Video, keyboard and speaker won't be defined
func CreateConfiguredApple() (*Apple2, error) {
	// Get configuration from defaults and the command line
	configuration, filename, err := getConfigurationFromCommandLine()
	if err != nil {
		return nil, err
	}

	if filename != "" {
		// Try loading as diskette
		_, err := LoadDiskette(filename)
		isDiskette := err == nil
		if isDiskette {
			// Let's force a DiskII with the diskette in slot 6
			configuration.set(confS6, fmt.Sprintf("diskii,disk1=\"%s\"", filename))
		} else {
			// Let's force a Smartport card with a block device in slot 7
			configuration.set(confS7, fmt.Sprintf("smartport,image1=\"%s\"", filename))
		}
	}

	a, err := configure(configuration)
	if err != nil {
		return nil, err
	}
	return a, nil
}
