// This file is part of Gopher2600.
//
// Gopher2600 is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Gopher2600 is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Gopher2600.  If not, see <https://www.gnu.org/licenses/>.

package comparison

import (
	"fmt"
	"image"
	"image/color"
	"strings"
	"sync/atomic"

	"github.com/jetsetilly/gopher2600/cartridgeloader"
	"github.com/jetsetilly/gopher2600/debugger/govern"
	"github.com/jetsetilly/gopher2600/environment"
	"github.com/jetsetilly/gopher2600/hardware"
	"github.com/jetsetilly/gopher2600/hardware/riot/ports"
	"github.com/jetsetilly/gopher2600/hardware/television"
	"github.com/jetsetilly/gopher2600/hardware/television/signal"
	"github.com/jetsetilly/gopher2600/hardware/television/specification"
	"github.com/jetsetilly/gopher2600/notifications"
)

// Comparison type runs a parallel emulation with the intention of comparing
// the output with the driver emulation.
type Comparison struct {
	VCS *hardware.VCS

	frameInfo television.FrameInfo

	img         *image.RGBA
	cropImg     *image.RGBA
	diffImg     *image.RGBA
	cropDiffImg *image.RGBA

	isEmulating   atomic.Value
	emulationQuit chan bool

	Render     chan *image.RGBA
	DiffRender chan *image.RGBA

	// pixel renderer implementation for the "driver" emulation. ie. the
	// emulation we'll be comparing against
	driver driver
}

const comparisonEnv = environment.Label("comparison")

// NewComparison is the preferred method of initialisation for the Comparison type.
func NewComparison(driverVCS *hardware.VCS) (*Comparison, error) {
	cmp := &Comparison{
		emulationQuit: make(chan bool, 1),
		Render:        make(chan *image.RGBA, 1),
		DiffRender:    make(chan *image.RGBA, 1),
	}

	// set isEmulating atomic as a boolean
	cmp.isEmulating.Store(false)

	// create a new television. this will be used during the initialisation of
	// the VCS and not referred to directly again
	tv, err := television.NewTelevision("AUTO")
	if err != nil {
		return nil, fmt.Errorf("comparison: %w", err)
	}
	tv.AddPixelRenderer(cmp)
	tv.SetFPSCap(true)

	// create a new VCS emulation
	cmp.VCS, err = hardware.NewVCS(tv, cmp, driverVCS.Env.Prefs)
	if err != nil {
		return nil, fmt.Errorf("comparison: %w", err)
	}
	cmp.VCS.Env.Label = comparisonEnv

	cmp.img = image.NewRGBA(image.Rect(0, 0, specification.ClksScanline, specification.AbsoluteMaxScanlines))
	cmp.diffImg = image.NewRGBA(image.Rect(0, 0, specification.ClksScanline, specification.AbsoluteMaxScanlines))

	// start with a NTSC television as default
	cmp.Resize(television.NewFrameInfo(specification.SpecNTSC))
	cmp.Reset()

	// create driver
	cmp.driver = newDriver(driverVCS.TV)
	driverVCS.TV.AddPixelRenderer(&cmp.driver)

	// synchronise RIOT ports
	sync := make(chan ports.TimedInputEvent, 32)
	err = cmp.VCS.Input.AttachPassenger(sync)
	if err != nil {
		return nil, fmt.Errorf("comparison: %w", err)
	}
	err = driverVCS.Input.AttachDriver(sync)
	if err != nil {
		return nil, fmt.Errorf("comparison: %w", err)
	}

	return cmp, nil
}

func (cmp *Comparison) String() string {
	cart := cmp.VCS.Mem.Cart
	s := strings.Builder{}
	s.WriteString(fmt.Sprintf("%s (%s cartridge)", cart.ShortName, cart.ID()))
	if cc := cart.GetContainer(); cc != nil {
		s.WriteString(fmt.Sprintf(" [in %s]", cc.ContainerID()))
	}
	return s.String()
}

// IsEmulating returns true if the comparison emulator is working. Useful for
// testing whether the cartridgeloader was an emulatable file.
func (cmp *Comparison) IsEmulating() bool {
	return cmp.isEmulating.Load().(bool)
}

// Quit ends the running comparison emulation.
func (cmp *Comparison) Quit() {
	// very important that we remove the pixel renderer from the main
	// emulation's television that we added as part of the newDriver()
	// function. if we don't then the renderer will continue firing and get
	// jammed waiting on a channel that has been abandone
	cmp.driver.tv.RemovePixelRenderer(&cmp.driver)

	// send quit signal to the comparison emulation
	cmp.emulationQuit <- true
}

// Notify implements the notifications.Notify interface
func (cmp *Comparison) Notify(notice notifications.Notice) error {
	switch notice {
	case notifications.NotifySuperchargerFastload:
		// the supercharger ROM will eventually start execution from the PC
		// address given in the supercharger file

		// CPU execution has been interrupted. update state of CPU
		cmp.VCS.CPU.Interrupted = true

		// the interrupted CPU means it never got a chance to
		// finalise the result. we force that here by simply
		// setting the Final flag to true.
		cmp.VCS.CPU.LastResult.Final = true

		// call function to complete tape loading procedure
		fastload := cmp.VCS.Mem.Cart.GetSuperchargerFastLoad()
		err := fastload.Fastload(cmp.VCS.CPU, cmp.VCS.Mem.RAM, cmp.VCS.RIOT.Timer)
		if err != nil {
			return err
		}
	case notifications.NotifySuperchargerSoundloadEnded:
		return cmp.VCS.TV.Reset(true)
	}

	return nil
}

// CreateFromLoader will cause images to be generated from a running emulation
// initialised with the specified cartridge loader.
func (cmp *Comparison) CreateFromLoader(cartload cartridgeloader.Loader) error {
	if cmp.IsEmulating() {
		return fmt.Errorf("comparison: emulation already running")
	}

	go func() {
		defer func() {
			cmp.driver.quit <- nil
			cmp.isEmulating.Store(false)
			cartload.Close()
		}()

		// not using setup system to attach cartridge. maybe we should?
		err := cmp.VCS.AttachCartridge(cartload, true)
		if err != nil {
			cmp.driver.quit <- err
			return
		}

		// if we get to this point then we can be reasonably sure that the
		// cartridgeloader is emulatable
		cmp.isEmulating.Store(true)

		// we need the main emulation to be exactly one frame ahead of the
		// comparison emulation. the best way of doing this is to not even
		// start the comparison emulation immediately

		select {
		case <-cmp.driver.sync:
		case <-cmp.emulationQuit:
			return
		}

		select {
		case cmp.driver.ack <- true:
		case <-cmp.emulationQuit:
			return
		}

		err = cmp.VCS.Run(func() (govern.State, error) {
			select {
			case <-cmp.emulationQuit:
				return govern.Ending, nil
			default:
			}

			return govern.Running, nil
		})
		if err != nil {
			cmp.driver.quit <- err
			return
		}
	}()

	return nil
}

// Resize implements the television.PixelRenderer interface.
func (cmp *Comparison) Resize(frameInfo television.FrameInfo) error {
	cmp.frameInfo = frameInfo
	crop := image.Rect(
		specification.ClksHBlank, frameInfo.VisibleTop,
		specification.ClksHBlank+specification.ClksVisible, frameInfo.VisibleBottom,
	)

	cmp.cropImg = cmp.img.SubImage(crop).(*image.RGBA)
	cmp.cropDiffImg = cmp.diffImg.SubImage(crop).(*image.RGBA)

	return nil
}

// NewFrame implements the television.PixelRenderer interface.
func (cmp *Comparison) NewFrame(frameInfo television.FrameInfo) error {
	cmp.frameInfo = frameInfo

	img := *cmp.cropImg
	img.Pix = make([]uint8, len(cmp.cropImg.Pix))
	copy(img.Pix, cmp.cropImg.Pix)

	select {
	case cmp.Render <- &img:
	default:
	}

	select {
	case <-cmp.driver.sync:
	case <-cmp.emulationQuit:
		return nil
	}

	select {
	case cmp.driver.ack <- true:
	case <-cmp.emulationQuit:
		return nil
	}

	return cmp.diff()
}

// NewScanline implements the television.PixelRenderer interface.
func (cmp *Comparison) NewScanline(scanline int) error {
	return nil
}

func (cmp *Comparison) diff() error {
	if !cmp.frameInfo.Stable || !cmp.driver.frameInfo.Stable {
		return nil
	}

	if cmp.frameInfo.FrameNum > cmp.driver.frameInfo.FrameNum {
		return fmt.Errorf("comparison: comparison emulation is running AHEAD of the driver emulation")
	} else if cmp.frameInfo.FrameNum < cmp.driver.frameInfo.FrameNum-1 {
		return fmt.Errorf("comparison: comparison emulation is running BEHIND of the driver emulation")
	}

	var drvImg *image.RGBA
	if cmp.driver.swapIdx {
		drvImg = cmp.driver.cropImg[0]
	} else {
		drvImg = cmp.driver.cropImg[1]
	}

	if len(cmp.cropImg.Pix) != len(drvImg.Pix) {
		return fmt.Errorf("comparison: frames are different sizes")
	}

	for i := 0; i < len(cmp.cropImg.Pix); i += 4 {
		a := cmp.cropImg.Pix[i : i+3 : i+3]
		b := drvImg.Pix[i : i+3 : i+3]
		c := cmp.cropDiffImg.Pix[i : i+4 : i+4]
		if a[0] != b[0] || a[1] != b[1] || a[2] != b[2] {
			c[0] = 0xff
			c[1] = 0xff
			c[2] = 0xff
		} else {
			c[0] = 0x00
			c[1] = 0x00
			c[2] = 0x00
		}
		c[3] = 0xff
	}

	cmp.DiffRender <- cmp.cropDiffImg

	return nil
}

// SetPixels implements the television.PixelRenderer interface.
func (cmp *Comparison) SetPixels(sig []signal.SignalAttributes, last int) error {
	var col color.RGBA
	var offset int

	for i := range sig {
		// handle VBLANK by setting pixels to black
		if sig[i]&signal.VBlank == signal.VBlank {
			col = color.RGBA{R: 0, G: 0, B: 0}
		} else {
			px := signal.ColorSignal((sig[i] & signal.Color) >> signal.ColorShift)
			col = cmp.frameInfo.Spec.GetColor(px)
		}

		// small cap improves performance, see https://golang.org/issue/27857
		s := cmp.img.Pix[offset : offset+3 : offset+3]
		s[0] = col.R
		s[1] = col.G
		s[2] = col.B

		offset += 4
	}
	return nil
}

// Reset implements the television.PixelRenderer interface.
func (cmp *Comparison) Reset() {
	// clear pixels. setting the alpha channel so we don't have to later (the
	// alpha channel never changes)
	for y := 0; y < cmp.img.Bounds().Size().Y; y++ {
		for x := 0; x < cmp.img.Bounds().Size().X; x++ {
			cmp.img.SetRGBA(x, y, color.RGBA{0, 0, 0, 255})
		}
	}

	for y := 0; y < cmp.diffImg.Bounds().Size().Y; y++ {
		for x := 0; x < cmp.diffImg.Bounds().Size().X; x++ {
			cmp.diffImg.SetRGBA(x, y, color.RGBA{0, 0, 0, 255})
		}
	}

	img := *cmp.cropImg
	img.Pix = make([]uint8, len(cmp.cropImg.Pix))
	copy(img.Pix, cmp.cropImg.Pix)

	select {
	case cmp.Render <- &img:
	default:
	}
}

// EndRendering implements the television.PixelRenderer interface.
func (cmp *Comparison) EndRendering() error {
	return nil
}
