package main

import (
	"context"
	_ "embed"
	"fmt"
	"math"
	"okemu/config"
	"okemu/debug"
	"okemu/debug/dzrp"
	//"okemu/debug/listener"
	"okemu/logger"
	"okemu/okean240"
	"okemu/z80/dis"
	"os"
	"runtime"
	"runtime/pprof"
	"sync/atomic"
	"time"

	"fyne.io/fyne/v2"
	"fyne.io/fyne/v2/canvas"
	"fyne.io/fyne/v2/widget"
	"github.com/loov/hrtime"
	log "github.com/sirupsen/logrus"
)

var Version = "v1.0.0"
var BuildTime = "2026-03-01"

////go:embed hex/m80.hex
//var serialBytes []byte

//go:embed bin/PIP.COM
var ramBytes []byte

////go:embed bin/LINK.COM
//var ramBytes2 []byte

var needReset = false

var fullSpeed atomic.Bool

func main() {

	fmt.Printf("Starting Ocean-240.2 emulator %s build at %s\n", Version, BuildTime)

	f, err := os.Create("okemu.prof")
	if err != nil {
		log.Warn("Can not create prof file", err)
	}
	defer func(f *os.File) {
		err := f.Close()
		if err != nil {
			log.Warn("Can not close prof file", err)
		}
	}(f)
	if err := pprof.StartCPUProfile(f); err != nil {
		log.Warn("Can not start CPU profiling", err)
	}
	defer pprof.StopCPUProfile()

	// Create a context that can be cancelled
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	// base log init
	logger.InitLogging()

	// load config yml file
	config.LoadConfig()

	conf := config.GetConfig()

	// Reconfigure logging by config values
	// logger.ReconfigureLogging(conf)

	debugger := debug.NewDebugger()
	computer := okean240.NewComputer(conf, debugger)

	//computer.SetSerialBytes(serialBytes)

	computer.AutoLoadFloppy()

	disassm := dis.NewDisassembler(computer)

	w, raster, label := mainWindow(computer, conf)

	dezog := dzrp.NewDZRP(conf, debugger, disassm, computer)

	go emulator(ctx, computer, dezog)
	go timerClock(ctx, computer)
	go screen(ctx, computer, raster, label)

	if conf.Debugger.Enabled {
		//go listener.SetupTcpHandler(conf, debugger, disassm, computer)
		go dezog.SetupTcpHandler()
	}

	(*w).ShowAndRun()
	computer.AutoSaveFloppy()
}

func screen(ctx context.Context, computer *okean240.ComputerType, raster *canvas.Raster, label *widget.Label) {
	ticker := time.NewTicker(20 * time.Millisecond)
	defer ticker.Stop()

	frame := 0
	var pre uint64 = 0
	var preTim uint64 = 0
	var cpuFreq float64 = 0
	var timerFreq float64 = 0
	timeStart := hrtime.Now()

	for {
		select {
		case <-ticker.C:
			frame++
			// redraw screen here
			fyne.Do(func() {
				// status for every 50 frames
				if frame%50 == 0 {
					timeElapsed := hrtime.Since(timeStart)
					period := float64(timeElapsed.Nanoseconds()) / 1_000_000.0

					//cpuFreq = math.Round(float64(computer.Cycles()-pre)/period) / 1000.0
					cpuFreq = math.Round(float64(cpuTicks.Load()-pre)/period) / 1000.0
					timerFreq = math.Round(float64(timerTicks.Load()-preTim)/period) / 1000.0
					label.SetText(formatLabel(computer, cpuFreq, timerFreq))

					adjustPeriods(cpuFreq, timerFreq)

					//log.Debugf("Cpu clk period: %d, Timer clock period: %d, period: %1.3f", cpuClkPeriod.Load(), timerClkPeriod.Load(), period)
					//pre = computer.Cycles()
					pre = cpuTicks.Load()
					preTim = timerTicks.Load()
					timeStart = hrtime.Now()
				}
				raster.Refresh()
			})
		case <-ctx.Done():
			return
		}
	}
}

// adjustPeriods Adjust periods for CPU and Timer clock frequencies
func adjustPeriods(cpuFreq float64, timerFreq float64) {
	// adjust cpu clock if not full speed
	if !fullSpeed.Load() {
		if cpuFreq > okean240.CPUFrequencyHi && cpuClkPeriod.Load() < defaultCpuClkPeriod+defaultCpuClkPeriod/2 {
			cpuClkPeriod.Add(1)
		} else if cpuFreq < okean240.CPUFrequencyLow && cpuClkPeriod.Load() > 3 {
			cpuClkPeriod.Add(-1)
		}
	}
	// adjust timerClock clock
	if timerFreq > okean240.TimerFrequencyHi && timerClkPeriod.Load() < defaultTimerClkPeriod+defaultTimerClkPeriod/2 {
		timerClkPeriod.Add(1)
	} else if timerFreq < okean240.TimerFrequencyLow && timerClkPeriod.Load() > 3 {
		timerClkPeriod.Add(-1)
	}
}

func formatLabel(computer *okean240.ComputerType, freq float64, freqTim float64) string {
	return fmt.Sprintf("Screen size: %dx%d | Fcpu: %1.3fMHz | Ftmr: %1.3fMHz | Debugger: %s", computer.ScreenWidth(), computer.ScreenHeight(), freq, freqTim, computer.DebuggerState())
}

var timerTicks atomic.Uint64

const defaultTimerClkPeriod = 430 // = 1_000_000_000 / 1_607_900 // period in nanos for 1.5MHz frequency
const defaultCpuClkPeriod = 311   // = 1_000_000_000 / 2_770_000   // period in nanos for 2.5MHz frequency

var timerClkPeriod atomic.Int64 // = 1_000_000_000 / 1_607_900 // period in nanos for 1.5MHz frequency
var cpuClkPeriod atomic.Int64   // = 1_000_000_000 / 2_770_000   // period in nanos for 2.5MHz frequency

//var timerTicker *time.Ticker

//var cpuTicker *time.Ticker

func timerClock(ctx context.Context, computer *okean240.ComputerType) {
	timerClkPeriod.Store(defaultTimerClkPeriod)
	timeStart := hrtime.Now()
	for {
		elapsed := hrtime.Since(timeStart)
		if int64(elapsed) > timerClkPeriod.Load() {
			timeStart = hrtime.Now()
			computer.TimerClk()
			timerTicks.Add(1)
			runtime.Gosched()
		}
	}

}

var cpuTicks atomic.Uint64

func emulator(ctx context.Context, computer *okean240.ComputerType, dezog *dzrp.DZRP) {
	cpuClkPeriod.Store(defaultCpuClkPeriod)
	//cpuTicker = time.NewTicker(time.Duration(cpuClkPeriod.Load()) * time.Nanosecond)
	//defer cpuTicker.Stop()

	cpuTicks.Store(0) // := uint64(0)
	nextTick := uint64(0)

	var bp uint16
	var bpType byte
	timeStart := hrtime.Now()

	for {
		//select {
		//case <-cpuTicker.C:
		// CPU
		elapsed := hrtime.Since(timeStart)
		if int64(elapsed) >= cpuClkPeriod.Load() {
			timeStart = hrtime.Now()
			bp = 0
			bpType = 0

			// 2.5MHz frequency
			cpuTicks.Add(1)
			if fullSpeed.Load() {
				// Max frequency
				_, bp, bpType = computer.Do()
			} else if cpuTicks.Load() >= nextTick {
				var t uint32
				t, bp, bpType = computer.Do()
				nextTick = cpuTicks.Load() + uint64(t)
				runtime.Gosched()
			}

			// Breakpoint hit
			if bp > 0 || bpType != 0 {
				dezog.BreakpointHit(bp, bpType)
			}
			if needReset {
				computer.Reset()
				needReset = false
			}
		}
		//case <-ctx.Done():
		//	return
		//}
	}

}
