// Package exceptions handles JavaScript errors nicely for the game.
package exceptions

import (
	"fmt"
	"strings"
	"sync"

	"git.kirsle.net/SketchyMaze/doodle/lib/debugging"
	"git.kirsle.net/SketchyMaze/doodle/pkg/balance"
	"git.kirsle.net/SketchyMaze/doodle/pkg/log"
	"git.kirsle.net/SketchyMaze/doodle/pkg/native"
	"git.kirsle.net/SketchyMaze/doodle/pkg/shmem"
	"git.kirsle.net/go/render"
	"git.kirsle.net/go/render/event"
	"git.kirsle.net/go/ui"
	"github.com/dop251/goja"
)

// The exception catching window is a singleton and appears on top with
// its own supervisor apart from whatever the game is currently doing.
var (
	Supervisor    *ui.Supervisor
	Window        *ui.Window
	Disabled      bool         // don't reopen the window again
	lastException string       // text of last exception
	excLabel      *string      // trimmed exception label text
	mu            sync.RWMutex // thread safety

	// Configurables.
	winSize   = render.NewRect(380, 260)
	excSize   = render.NewRect(360, 140)
	charsWide = 50 // to trim the exception text onto screen
	charsTall = 9
)

func init() {
	l := "No Exception Traceback"
	excLabel = &l
}

// Catch a JavaScript exception and maybe show it to the user.
func Catch(exc string, args ...interface{}) {
	if len(args) > 0 {
		exc = fmt.Sprintf(exc, args...)
	}

	log.Error("[JS] Exception: %s", exc)
	if Disabled {
		return
	}

	Setup()

	width, _ := shmem.CurrentRenderEngine.WindowSize()
	Window.MoveTo(render.Point{
		X: (width / 2) - (Window.Size().W / 2),
		Y: 60,
	})

	Window.Show()
	lastException = exc
	*excLabel = trim(exc)
}

// FormatAndCatch an exception from a JavaScript VM. This is the common function called
// immediately when a scripting-related panic is recovered, and appends stack trace frames
// and formats the message for final Catch display.
func FormatAndCatch(vm *goja.Runtime, exc string, args ...interface{}) {
	// Collect the JavaScript stack frame for debugging.
	var (
		buf       [1000]goja.StackFrame
		jsCallers = []string{}
		sections  = []string{
			fmt.Sprintf(exc, args...),
		}
	)

	if vm != nil {
		frames := vm.CaptureCallStack(1000, buf[:0])
		for i, frame := range frames {
			var position = frame.Position()
			jsCallers = append(jsCallers, fmt.Sprintf("%d. %s at %s line %d column %d", i+1, frame.FuncName(), position.Filename, position.Line, position.Column))
		}

		if len(jsCallers) > 0 {
			sections = append(sections, fmt.Sprintf("JS stack:\n%s", strings.Join(jsCallers, "\n")))
		}
	}

	sections = append(sections, fmt.Sprintf("Go stack:\n%s", debugging.StringifyCallers()))

	Catch(
		strings.Join(sections, "\n\n"),
	)
}

// Setup the global supervisor and window the first time - after the render engine has initialized,
// e.g., when you want the window to show up the first time.
func Setup() {
	mu.Lock()
	defer mu.Unlock()

	log.Info("Setup Exceptions")

	if Supervisor == nil {
		Supervisor = ui.NewSupervisor()
	}

	if Window == nil {
		Window = MakeWindow(Exception{
			Supervisor: Supervisor,
			Engine:     shmem.CurrentRenderEngine,
		})
		Window.Compute(shmem.CurrentRenderEngine)
		Window.Supervise(Supervisor)
	}
}

// Teardown the exception UI, if it was loaded. This is done between Scene transitions in the game
// to reduce memory leaks in case the scenes will flush SDL2 caches or w/e.
func Teardown() {
	log.Error("Teardown Exceptions")
	mu.Lock()
	defer mu.Unlock()

	if Window != nil {
		Window = nil
	}

	if Supervisor != nil {
		Supervisor = nil
	}
}

// Handled returns true if the exception window handles the events this tick, e.g.
// so clicking on its window won't let your mouse click also hit things behind it.
func Handled(ev *event.State) bool {
	if Window != nil && !Window.Hidden() {
		// If they hit the Return/Escape key, dismiss the exception.
		if ev.Enter || ev.Escape {
			ev.Escape = false
			Window.Close()
			return true
		}

		Supervisor.Loop(ev)

		// NOTE: if the window Close handler tears down the Supervisor, the
		// previous call to Supervisor.Loop() may run it and then Supervisor
		// no longer exists.
		if Supervisor != nil && Supervisor.IsPointInWindow(shmem.Cursor) {
			return true
		}
	}
	return false
}

// Loop allows the exception window to appear on game tick.
func Draw(e render.Engine) {
	if Supervisor != nil {
		Supervisor.Present(e)
	}
}

// Exception window to show scripting errors in doodads.
type Exception struct {
	// Settings passed in by doodle
	Supervisor *ui.Supervisor
	Engine     render.Engine
}

// Function to trim the raw exception text so it fits neatly within the label.
// In case it's long, use the Copy button to copy to your clipboard.
func trim(input string) string {
	var lines = []string{}
	for _, line := range strings.Split(input, "\n") {
		if len(lines) >= charsTall {
			lines = lines[:charsTall]
			break
		}

		if len(line) > charsWide {
			// Word wrap it.
			for len(line) > charsWide {
				lines = append(lines, line[:charsWide])
				line = line[charsWide:]
			}
			if len(line) > 0 {
				lines = append(lines, line)
			}
			continue
		}

		lines = append(lines, line)
	}
	return strings.Join(lines, "\n")
}

// MakeWindow initializes the window.
func MakeWindow(cfg Exception) *ui.Window {
	window := ui.NewWindow("Exception")
	window.Configure(ui.Config{
		Width:      winSize.W,
		Height:     winSize.H,
		Background: render.Red.Lighten(128),
	})

	window.Handle(ui.CloseWindow, func(ed ui.EventData) error {
		Teardown()
		return ui.ErrStopPropagation
	})

	header := ui.NewLabel(ui.Label{
		Text: "A JavaScript error has occurred in a doodad:",
		Font: balance.UIFont,
	})
	window.Pack(header, ui.Pack{
		Side:    ui.N,
		Padding: 8,
	})

	text := ui.NewLabel(ui.Label{
		TextVariable: excLabel,
		Font:         balance.ExceptionFont,
	})
	text.Configure(ui.Config{
		BorderSize:  1,
		BorderStyle: ui.BorderSunken,
		Background:  render.White,
		Width:       excSize.W,
		Height:      excSize.H,
	})
	window.Pack(text, ui.Pack{
		Side: ui.N,
	})

	frame := ui.NewFrame("Button frame")
	buttons := []struct {
		label   string
		tooltip string
		f       func()
	}{
		{"Dismiss", "", func() {
			Window.Close()
		}},
		{"Copy", "Copy the full text to clipboard", func() {
			native.CopyToClipboard(lastException)
		}},
		{"Don't show again", "Don't show errors like this again\nuntil your next play session", func() {
			Disabled = true
			Window.Close()
		}},
	}
	for i, button := range buttons {
		button := button

		btn := ui.NewButton(button.label, ui.NewLabel(ui.Label{
			Text: button.label,
			Font: balance.MenuFont,
		}))
		if i == 0 {
			btn.SetStyle(&balance.ButtonPrimary)
		}

		btn.Handle(ui.Click, func(ed ui.EventData) error {
			button.f()
			return nil
		})

		btn.Compute(cfg.Engine)

		// Tooltips?
		if len(button.tooltip) > 0 {
			ui.NewTooltip(btn, ui.Tooltip{
				Text: button.tooltip,
				Edge: ui.Bottom,
			})
		}

		cfg.Supervisor.Add(btn)

		frame.Pack(btn, ui.Pack{
			Side:   ui.W,
			PadX:   4,
			Expand: true,
			Fill:   true,
		})
	}
	window.Pack(frame, ui.Pack{
		Side: ui.N,
		PadY: 12,
	})

	return window
}
