package main

import (
	"encoding/json"
	"fmt"
	"os"
	"os/exec"
	"regexp"
	"strconv"
	"strings"
	"time"

	"github.com/atotto/clipboard"
	"github.com/charmbracelet/vhs/parser"
	"github.com/charmbracelet/vhs/token"
	"github.com/go-rod/rod/lib/input"
)

// Execute executes a command on a running instance of vhs.
func Execute(c parser.Command, v *VHS) error {
	err := CommandFuncs[c.Type](c, v)
	if err != nil {
		return fmt.Errorf("failed to execute command: %w", err)
	}

	if v.recording && v.Options.Test.Output != "" {
		err := v.SaveOutput()
		if err != nil {
			return fmt.Errorf("failed to save output: %w", err)
		}
	}

	return nil
}

// CommandFunc is a function that executes a command on a running
// instance of vhs.
type CommandFunc func(c parser.Command, v *VHS) error

// CommandFuncs maps command types to their executable functions.
var CommandFuncs = map[parser.CommandType]CommandFunc{
	token.BACKSPACE:   ExecuteKey(input.Backspace),
	token.DELETE:      ExecuteKey(input.Delete),
	token.INSERT:      ExecuteKey(input.Insert),
	token.DOWN:        ExecuteKey(input.ArrowDown),
	token.ENTER:       ExecuteKey(input.Enter),
	token.LEFT:        ExecuteKey(input.ArrowLeft),
	token.RIGHT:       ExecuteKey(input.ArrowRight),
	token.SPACE:       ExecuteKey(input.Space),
	token.UP:          ExecuteKey(input.ArrowUp),
	token.TAB:         ExecuteKey(input.Tab),
	token.ESCAPE:      ExecuteKey(input.Escape),
	token.PAGE_UP:     ExecuteKey(input.PageUp),
	token.PAGE_DOWN:   ExecuteKey(input.PageDown),
	token.SCROLL_UP:   ExecuteScroll(-1),
	token.SCROLL_DOWN: ExecuteScroll(1),
	token.HIDE:        ExecuteHide,
	token.REQUIRE:     ExecuteRequire,
	token.SHOW:        ExecuteShow,
	token.SET:         ExecuteSet,
	token.OUTPUT:      ExecuteOutput,
	token.SLEEP:       ExecuteSleep,
	token.TYPE:        ExecuteType,
	token.CTRL:        ExecuteCtrl,
	token.ALT:         ExecuteAlt,
	token.SHIFT:       ExecuteShift,
	token.ILLEGAL:     ExecuteNoop,
	token.SCREENSHOT:  ExecuteScreenshot,
	token.COPY:        ExecuteCopy,
	token.PASTE:       ExecutePaste,
	token.ENV:         ExecuteEnv,
	token.WAIT:        ExecuteWait,
}

// ExecuteNoop is a no-op command that does nothing.
// Generally, this is used for Unknown commands when dealing with
// commands that are not recognized.
func ExecuteNoop(_ parser.Command, _ *VHS) error { return nil }

// ExecuteKey is a higher-order function that returns a CommandFunc to execute
// a key press for a given key. This is so that the logic for key pressing
// (since they are repeatable and delayable) can be re-used.
//
// i.e. ExecuteKey(input.ArrowDown) would return a CommandFunc that executes
// the ArrowDown key press.
func ExecuteKey(k input.Key) CommandFunc {
	return func(c parser.Command, v *VHS) error {
		typingSpeed, err := time.ParseDuration(c.Options)
		if err != nil {
			typingSpeed = v.Options.TypingSpeed
		}
		repeat, err := strconv.Atoi(c.Args)
		if err != nil {
			repeat = 1
		}
		for i := 0; i < repeat; i++ {
			err = v.Page.Keyboard.Type(k)
			if err != nil {
				return fmt.Errorf("failed to type key %c: %w", k, err)
			}
			time.Sleep(typingSpeed)
		}

		return nil
	}
}

// ExecuteScroll returns a command function that scrolls xterm's viewport.
//
// The direction argument is expected to be:
//
//	-1 for ScrollUp
//	+1 for ScrollDown
//
// Each command repeat applies one viewport row movement with the same timing
// semantics as other repeatable key-like commands (Options is the per-step
// delay, Args is repeat count).
//
// A caller that wires an unexpected direction (for example 10) will still call
// xterm's scroll API with that value and produce larger jumps per step.
func ExecuteScroll(direction int) CommandFunc {
	return func(c parser.Command, v *VHS) error {
		typingSpeed, err := time.ParseDuration(c.Options)
		if err != nil {
			typingSpeed = v.Options.TypingSpeed
		}
		repeat, err := strconv.Atoi(c.Args)
		if err != nil {
			repeat = 1
		}

		for i := 0; i < repeat; i++ {
			// ScrollUp/ScrollDown are viewport operations implemented directly via
			// xterm's scroll API.
			_, err = v.Page.Eval(fmt.Sprintf("() => term.scrollLines(%d)", direction))
			if err != nil {
				return fmt.Errorf("failed to scroll viewport: %w", err)
			}
			time.Sleep(typingSpeed)
		}

		return nil
	}
}

// WaitTick is the amount of time to wait between checking for a match.
const WaitTick = 10 * time.Millisecond

// ExecuteWait is a CommandFunc that waits for a regex match for the given amount of time.
func ExecuteWait(c parser.Command, v *VHS) error {
	scope, rxStr, ok := strings.Cut(c.Args, " ")
	rx := v.Options.WaitPattern
	if ok {
		// This is validated on parse so using MustCompile reduces noise.
		rx = regexp.MustCompile(rxStr)
	}

	timeout := v.Options.WaitTimeout
	if c.Options != "" {
		t, err := time.ParseDuration(c.Options)
		if err != nil {
			// Shouldn't be possible due to parse validation.
			return fmt.Errorf("failed to parse duration: %w", err)
		}
		timeout = t
	}

	checkT := time.NewTicker(WaitTick)
	defer checkT.Stop()
	timeoutT := time.NewTimer(timeout)
	defer timeoutT.Stop()

	for {
		var last string
		switch scope {
		case "Line":
			line, err := v.CurrentLine()
			if err != nil {
				return fmt.Errorf("failed to get current line: %w", err)
			}
			last = line

			if rx.MatchString(line) {
				return nil
			}
		case "Screen":
			lines, err := v.Buffer()
			if err != nil {
				return fmt.Errorf("failed to get buffer: %w", err)
			}
			last = strings.Join(lines, "\n")

			if rx.MatchString(last) {
				return nil
			}
		default:
			// Should be impossible due to parse validation, but we don't want to
			// hang if it does happen due to a bug.
			return fmt.Errorf("invalid scope %q", scope)
		}

		select {
		case <-checkT.C:
			continue
		case <-timeoutT.C:
			return fmt.Errorf("timeout waiting for %q to match %s; last value was: %s", c.Args, rx.String(), last)
		}
	}
}

// ExecuteCtrl is a CommandFunc that presses the argument keys and/or modifiers
// with the ctrl key held down on the running instance of vhs.
func ExecuteCtrl(c parser.Command, v *VHS) error {
	// Create key combination by holding ControlLeft
	action := v.Page.KeyActions().Press(input.ControlLeft)
	keys := strings.Split(c.Args, " ")

	for i, key := range keys {
		var inputKey *input.Key

		switch key {
		case "Shift":
			inputKey = &input.ShiftLeft
		case "Alt":
			inputKey = &input.AltLeft
		case "Enter":
			inputKey = &input.Enter
		case "Space":
			inputKey = &input.Space
		case "Backspace":
			inputKey = &input.Backspace
		case "Left":
			inputKey = &input.ArrowLeft
		case "Right":
			inputKey = &input.ArrowRight
		case "Up":
			inputKey = &input.ArrowUp
		case "Down":
			inputKey = &input.ArrowDown
		default:
			r := rune(key[0])
			if k, ok := keymap[r]; ok {
				inputKey = &k
			}
		}

		// Press or hold key in case it's valid
		if inputKey != nil {
			if i != len(keys)-1 {
				action.Press(*inputKey)
			} else {
				// Other keys will remain pressed until the combination reaches the end
				action.Type(*inputKey)
			}
		}
	}

	err := action.Do()
	if err != nil {
		return fmt.Errorf("failed to type key %s: %w", c.Args, err)
	}

	return nil
}

// ExecuteAlt is a CommandFunc that presses the argument key with the alt key
// held down on the running instance of vhs.
func ExecuteAlt(c parser.Command, v *VHS) error {
	err := v.Page.Keyboard.Press(input.AltLeft)
	if err != nil {
		return fmt.Errorf("failed to press Alt key: %w", err)
	}
	if k, ok := token.Keywords[c.Args]; ok { //nolint:nestif
		switch k {
		case token.ENTER:
			err = v.Page.Keyboard.Type(input.Enter)
			if err != nil {
				return fmt.Errorf("failed to type Enter key: %w", err)
			}
		case token.TAB:
			err := v.Page.Keyboard.Type(input.Tab)
			if err != nil {
				return fmt.Errorf("failed to type Tab key: %w", err)
			}
		}
	} else {
		for _, r := range c.Args {
			if k, ok := keymap[r]; ok {
				err = v.Page.Keyboard.Type(k)
				if err != nil {
					return fmt.Errorf("failed to type key %c: %w", r, err)
				}
			}
		}
	}

	err = v.Page.Keyboard.Release(input.AltLeft)
	if err != nil {
		return fmt.Errorf("failed to release Alt key: %w", err)
	}

	return nil
}

// ExecuteShift is a CommandFunc that presses the argument key with the shift
// key held down on the running instance of vhs.
func ExecuteShift(c parser.Command, v *VHS) error {
	err := v.Page.Keyboard.Press(input.ShiftLeft)
	if err != nil {
		return fmt.Errorf("failed to press Shift key: %w", err)
	}

	if k, ok := token.Keywords[c.Args]; ok { //nolint:nestif
		switch k {
		case token.ENTER:
			err = v.Page.Keyboard.Type(input.Enter)
			if err != nil {
				return fmt.Errorf("failed to type Enter key: %w", err)
			}
		case token.TAB:
			err = v.Page.Keyboard.Type(input.Tab)
			if err != nil {
				return fmt.Errorf("failed to type Tab key: %w", err)
			}
		}
	} else {
		for _, r := range c.Args {
			if k, ok := keymap[r]; ok {
				err = v.Page.Keyboard.Type(k)
				if err != nil {
					return fmt.Errorf("failed to type key %c: %w", r, err)
				}
			}
		}
	}

	err = v.Page.Keyboard.Release(input.ShiftLeft)
	if err != nil {
		return fmt.Errorf("failed to release Shift key: %w", err)
	}

	return nil
}

// ExecuteHide is a CommandFunc that starts or stops the recording of the vhs.
func ExecuteHide(_ parser.Command, v *VHS) error {
	v.PauseRecording()
	return nil
}

// ExecuteRequire is a CommandFunc that checks if all the binaries mentioned in the
// Require command are present. If not, it exits with a non-zero error.
func ExecuteRequire(c parser.Command, _ *VHS) error {
	_, err := exec.LookPath(c.Args)
	return err //nolint:wrapcheck
}

// ExecuteShow is a CommandFunc that resumes the recording of the vhs.
func ExecuteShow(_ parser.Command, v *VHS) error {
	v.ResumeRecording()
	return nil
}

// ExecuteSleep sleeps for the desired time specified through the argument of
// the Sleep command.
func ExecuteSleep(c parser.Command, _ *VHS) error {
	dur, err := time.ParseDuration(c.Args)
	if err != nil {
		return fmt.Errorf("failed to parse duration: %w", err)
	}
	time.Sleep(dur)
	return nil
}

// ExecuteType types the argument string on the running instance of vhs.
func ExecuteType(c parser.Command, v *VHS) error {
	typingSpeed := v.Options.TypingSpeed
	if c.Options != "" {
		var err error
		typingSpeed, err = time.ParseDuration(c.Options)
		if err != nil {
			return fmt.Errorf("failed to parse typing speed: %w", err)
		}
	}
	for _, r := range c.Args {
		k, ok := keymap[r]
		if ok {
			err := v.Page.Keyboard.Type(k)
			if err != nil {
				return fmt.Errorf("failed to type key %c: %w", r, err)
			}
		} else {
			err := v.Page.MustElement("textarea").Input(string(r))
			if err != nil {
				return fmt.Errorf("failed to input text: %w", err)
			}

			v.Page.MustWaitIdle()
		}
		time.Sleep(typingSpeed)
	}

	return nil
}

// ExecuteOutput applies the output on the vhs videos.
func ExecuteOutput(c parser.Command, v *VHS) error {
	switch c.Options {
	case ".mp4":
		v.Options.Video.Output.MP4 = c.Args
	case ".test", ".ascii", ".txt":
		v.Options.Test.Output = c.Args
	case ".png":
		v.Options.Video.Output.Frames = c.Args
	case ".webm":
		v.Options.Video.Output.WebM = c.Args
	default:
		v.Options.Video.Output.GIF = c.Args
	}

	return nil
}

// ExecuteCopy copies text to the clipboard.
func ExecuteCopy(c parser.Command, _ *VHS) error {
	return clipboard.WriteAll(c.Args) //nolint:wrapcheck
}

// ExecuteEnv sets env with given key-value pair.
func ExecuteEnv(c parser.Command, _ *VHS) error {
	return os.Setenv(c.Options, c.Args) //nolint:wrapcheck
}

// ExecutePaste pastes text from the clipboard.
func ExecutePaste(_ parser.Command, v *VHS) error {
	clip, err := clipboard.ReadAll()
	if err != nil {
		return fmt.Errorf("failed to read clipboard: %w", err)
	}
	for _, r := range clip {
		k, ok := keymap[r]
		if ok {
			err = v.Page.Keyboard.Type(k)
			if err != nil {
				return fmt.Errorf("failed to type key %c: %w", r, err)
			}
		} else {
			err = v.Page.MustElement("textarea").Input(string(r))
			if err != nil {
				return fmt.Errorf("failed to input text: %w", err)
			}
			v.Page.MustWaitIdle()
		}
	}

	return nil
}

// Settings maps the Set commands to their respective functions.
var Settings = map[string]CommandFunc{
	"FontFamily":    ExecuteSetFontFamily,
	"FontSize":      ExecuteSetFontSize,
	"Framerate":     ExecuteSetFramerate,
	"Height":        ExecuteSetHeight,
	"LetterSpacing": ExecuteSetLetterSpacing,
	"LineHeight":    ExecuteSetLineHeight,
	"PlaybackSpeed": ExecuteSetPlaybackSpeed,
	"Padding":       ExecuteSetPadding,
	"Theme":         ExecuteSetTheme,
	"TypingSpeed":   ExecuteSetTypingSpeed,
	"Width":         ExecuteSetWidth,
	"Shell":         ExecuteSetShell,
	"LoopOffset":    ExecuteLoopOffset,
	"MarginFill":    ExecuteSetMarginFill,
	"Margin":        ExecuteSetMargin,
	"WindowBar":     ExecuteSetWindowBar,
	"WindowBarSize": ExecuteSetWindowBarSize,
	"BorderRadius":  ExecuteSetBorderRadius,
	"WaitPattern":   ExecuteSetWaitPattern,
	"WaitTimeout":   ExecuteSetWaitTimeout,
	"CursorBlink":   ExecuteSetCursorBlink,
}

// ExecuteSet applies the settings on the running vhs specified by the
// option and argument pass to the command.
func ExecuteSet(c parser.Command, v *VHS) error {
	return Settings[c.Options](c, v)
}

// ExecuteSetFontSize applies the font size on the vhs.
func ExecuteSetFontSize(c parser.Command, v *VHS) error {
	fontSize, err := strconv.Atoi(c.Args)
	if err != nil {
		return fmt.Errorf("failed to parse font size: %w", err)
	}
	v.Options.FontSize = fontSize
	_, err = v.Page.Eval(fmt.Sprintf("() => term.options.fontSize = %d", fontSize))
	if err != nil {
		return fmt.Errorf("failed to set font size: %w", err)
	}

	// When changing the font size only the canvas dimensions change which are
	// scaled back during the render to fit the aspect ration and dimensions.
	//
	// We need to call term.fit to ensure that everything is resized properly.
	_, err = v.Page.Eval("term.fit")
	if err != nil {
		return fmt.Errorf("failed to fit terminal: %w", err)
	}

	return nil
}

// ExecuteSetFontFamily applies the font family on the vhs.
func ExecuteSetFontFamily(c parser.Command, v *VHS) error {
	v.Options.FontFamily = c.Args
	_, err := v.Page.Eval(fmt.Sprintf("() => term.options.fontFamily = '%s'", withSymbolsFallback(c.Args)))
	if err != nil {
		return fmt.Errorf("failed to set font family: %w", err)
	}

	return nil
}

// ExecuteSetHeight applies the height on the vhs.
func ExecuteSetHeight(c parser.Command, v *VHS) error {
	height, err := strconv.Atoi(c.Args)
	if err != nil {
		return fmt.Errorf("failed to parse height: %w", err)
	}
	v.Options.Video.Style.Height = height

	return nil
}

// ExecuteSetWidth applies the width on the vhs.
func ExecuteSetWidth(c parser.Command, v *VHS) error {
	width, err := strconv.Atoi(c.Args)
	if err != nil {
		return fmt.Errorf("failed to parse width: %w", err)
	}
	v.Options.Video.Style.Width = width

	return nil
}

// ExecuteSetShell applies the shell on the vhs.
func ExecuteSetShell(c parser.Command, v *VHS) error {
	s, ok := Shells[c.Args]
	if !ok {
		return fmt.Errorf("invalid shell %s", c.Args)
	}

	v.Options.Shell = s
	return nil
}

const (
	bitSize = 64
	base    = 10
)

// ExecuteSetLetterSpacing applies letter spacing (also known as tracking) on
// the vhs.
func ExecuteSetLetterSpacing(c parser.Command, v *VHS) error {
	letterSpacing, err := strconv.ParseFloat(c.Args, bitSize)
	if err != nil {
		return fmt.Errorf("failed to parse letter spacing: %w", err)
	}

	v.Options.LetterSpacing = letterSpacing
	_, err = v.Page.Eval(fmt.Sprintf("() => term.options.letterSpacing = %f", letterSpacing))
	if err != nil {
		return fmt.Errorf("failed to set letter spacing: %w", err)
	}

	return nil
}

// ExecuteSetLineHeight applies the line height on the vhs.
func ExecuteSetLineHeight(c parser.Command, v *VHS) error {
	lineHeight, err := strconv.ParseFloat(c.Args, bitSize)
	if err != nil {
		return fmt.Errorf("failed to parse line height: %w", err)
	}

	v.Options.LineHeight = lineHeight
	_, err = v.Page.Eval(fmt.Sprintf("() => term.options.lineHeight = %f", lineHeight))
	if err != nil {
		return fmt.Errorf("failed to set line height: %w", err)
	}

	return nil
}

// ExecuteSetTheme applies the theme on the vhs.
func ExecuteSetTheme(c parser.Command, v *VHS) error {
	var err error
	v.Options.Theme, err = getTheme(c.Args)
	if err != nil {
		return err
	}

	bts, err := json.Marshal(v.Options.Theme)
	if err != nil {
		return fmt.Errorf("failed to marshal theme: %w", err)
	}

	_, err = v.Page.Eval(fmt.Sprintf("() => term.options.theme = %s", string(bts)))
	if err != nil {
		return fmt.Errorf("failed to set theme: %w", err)
	}

	v.Options.Video.Style.BackgroundColor = v.Options.Theme.Background
	v.Options.Video.Style.WindowBarColor = v.Options.Theme.Background

	return nil
}

// ExecuteSetTypingSpeed applies the default typing speed on the vhs.
func ExecuteSetTypingSpeed(c parser.Command, v *VHS) error {
	typingSpeed, err := time.ParseDuration(c.Args)
	if err != nil {
		return fmt.Errorf("failed to parse typing speed: %w", err)
	}

	v.Options.TypingSpeed = typingSpeed
	return nil
}

// ExecuteSetWaitTimeout applies the default wait timeout on the vhs.
func ExecuteSetWaitTimeout(c parser.Command, v *VHS) error {
	waitTimeout, err := time.ParseDuration(c.Args)
	if err != nil {
		return fmt.Errorf("failed to parse wait timeout: %w", err)
	}
	v.Options.WaitTimeout = waitTimeout
	return nil
}

// ExecuteSetWaitPattern applies the default wait pattern on the vhs.
func ExecuteSetWaitPattern(c parser.Command, v *VHS) error {
	rx, err := regexp.Compile(c.Args)
	if err != nil {
		return fmt.Errorf("failed to compile regexp: %w", err)
	}
	v.Options.WaitPattern = rx
	return nil
}

// ExecuteSetPadding applies the padding on the vhs.
func ExecuteSetPadding(c parser.Command, v *VHS) error {
	padding, err := strconv.Atoi(c.Args)
	if err != nil {
		return fmt.Errorf("failed to parse padding: %w", err)
	}

	v.Options.Video.Style.Padding = padding
	return nil
}

// ExecuteSetFramerate applies the framerate on the vhs.
func ExecuteSetFramerate(c parser.Command, v *VHS) error {
	framerate, err := strconv.ParseInt(c.Args, base, 0)
	if err != nil {
		return fmt.Errorf("failed to parse framerate: %w", err)
	}

	v.Options.Video.Framerate = int(framerate)
	return nil
}

// ExecuteSetPlaybackSpeed applies the playback speed option on the vhs.
func ExecuteSetPlaybackSpeed(c parser.Command, v *VHS) error {
	playbackSpeed, err := strconv.ParseFloat(c.Args, bitSize)
	if err != nil {
		return fmt.Errorf("failed to parse playback speed: %w", err)
	}

	v.Options.Video.PlaybackSpeed = playbackSpeed
	return nil
}

// ExecuteLoopOffset applies the loop offset option on the vhs.
func ExecuteLoopOffset(c parser.Command, v *VHS) error {
	loopOffset, err := strconv.ParseFloat(strings.TrimRight(c.Args, "%"), bitSize)
	if err != nil {
		return fmt.Errorf("failed to parse loop offset: %w", err)
	}

	v.Options.LoopOffset = loopOffset
	return nil
}

// ExecuteSetMarginFill sets vhs margin fill.
func ExecuteSetMarginFill(c parser.Command, v *VHS) error {
	v.Options.Video.Style.MarginFill = c.Args
	return nil
}

// ExecuteSetMargin sets vhs margin size.
func ExecuteSetMargin(c parser.Command, v *VHS) error {
	margin, err := strconv.Atoi(c.Args)
	if err != nil {
		return fmt.Errorf("failed to parse margin: %w", err)
	}

	v.Options.Video.Style.Margin = margin
	return nil
}

// ExecuteSetWindowBar sets window bar type.
func ExecuteSetWindowBar(c parser.Command, v *VHS) error {
	v.Options.Video.Style.WindowBar = c.Args
	return nil
}

// ExecuteSetWindowBarSize sets window bar size.
func ExecuteSetWindowBarSize(c parser.Command, v *VHS) error {
	windowBarSize, err := strconv.Atoi(c.Args)
	if err != nil {
		return fmt.Errorf("failed to parse window bar size: %w", err)
	}

	v.Options.Video.Style.WindowBarSize = windowBarSize
	return nil
}

// ExecuteSetBorderRadius sets corner radius.
func ExecuteSetBorderRadius(c parser.Command, v *VHS) error {
	borderRadius, err := strconv.Atoi(c.Args)
	if err != nil {
		return fmt.Errorf("failed to parse border radius: %w", err)
	}

	v.Options.Video.Style.BorderRadius = borderRadius
	return nil
}

// ExecuteSetCursorBlink sets cursor blinking.
func ExecuteSetCursorBlink(c parser.Command, v *VHS) error {
	var err error
	v.Options.CursorBlink, err = strconv.ParseBool(c.Args)
	if err != nil {
		return fmt.Errorf("failed to parse cursor blink: %w", err)
	}

	return nil
}

// ExecuteScreenshot is a CommandFunc that indicates a new screenshot must be taken.
func ExecuteScreenshot(c parser.Command, v *VHS) error {
	v.ScreenshotNextFrame(c.Args)
	return nil
}

func getTheme(s string) (Theme, error) {
	if strings.TrimSpace(s) == "" {
		return DefaultTheme, nil
	}
	switch s[0] {
	case '{':
		return getJSONTheme(s)
	default:
		return findTheme(s)
	}
}

func getJSONTheme(s string) (Theme, error) {
	var t Theme
	if err := json.Unmarshal([]byte(s), &t); err != nil {
		return DefaultTheme, fmt.Errorf("invalid `Set Theme %q: %w`", s, err)
	}
	return t, nil
}
