package main

import (
	"context"
	"fmt"
	"io"
	"log"
	"os"

	"github.com/charmbracelet/vhs/lexer"
	"github.com/charmbracelet/vhs/parser"
	"github.com/charmbracelet/vhs/token"
	"github.com/go-rod/rod"
)

// EvaluatorOption is a function that can be used to modify the VHS instance.
type EvaluatorOption func(*VHS)

// Evaluate takes as input a tape string, an output writer, and an output file
// and evaluates all the commands within the tape string and produces a GIF.
func Evaluate(ctx context.Context, tape string, out io.Writer, opts ...EvaluatorOption) []error {
	l := lexer.New(tape)
	p := parser.New(l)

	cmds := p.Parse()
	errs := p.Errors()
	if len(errs) != 0 || len(cmds) == 0 {
		return []error{InvalidSyntaxError{errs}}
	}

	v := New()
	for _, cmd := range cmds {
		if cmd.Type == token.SET && cmd.Options == "Shell" || cmd.Type == token.ENV {
			err := Execute(cmd, &v)
			if err != nil {
				return []error{err}
			}
		}
	}

	// Start things up
	if err := v.Start(); err != nil {
		return []error{err}
	}
	defer func() { _ = v.close() }()

	// Let's wait until we can access the window.term variable.
	//
	// This is necessary because some SET commands modify the terminal.
	err := v.Page.Wait(rod.Eval("() => window.term != undefined"))
	if err != nil {
		return []error{err}
	}

	var offset int
	for i, cmd := range cmds {
		if cmd.Type == token.SET || cmd.Type == token.OUTPUT || cmd.Type == token.REQUIRE {
			_, _ = fmt.Fprintln(out, Highlight(cmd, false))
			if cmd.Options != "Shell" {
				err := Execute(cmd, &v)
				if err != nil {
					return []error{err}
				}
			}
		} else {
			offset = i
			break
		}
	}

	// Make sure image is big enough to fit padding, bar, and margins
	video := v.Options.Video
	minWidth := double(video.Style.Padding) + double(video.Style.Margin)
	minHeight := double(video.Style.Padding) + double(video.Style.Margin)
	if video.Style.WindowBar != "" {
		minHeight += video.Style.WindowBarSize
	}
	if video.Style.Height < minHeight || video.Style.Width < minWidth {
		//nolint:staticcheck
		v.Errors = append(
			v.Errors,
			fmt.Errorf(
				"Dimensions must be at least %d x %d",
				minWidth, minHeight,
			),
		)
	}

	if len(v.Errors) > 0 {
		return v.Errors
	}

	// Setup the terminal session so we can start executing commands.
	v.Setup()

	// If the first command (after Settings and Outputs) is a Hide command, we can
	// begin executing the commands before we start recording to avoid capturing
	// any unwanted frames.
	if cmds[offset].Type == token.HIDE {
		for i, cmd := range cmds[offset:] {
			if cmd.Type == token.SHOW {
				offset += i
				break
			}
			_, _ = fmt.Fprintln(out, Highlight(cmd, true))
			err := Execute(cmd, &v)
			if err != nil {
				return []error{err}
			}
		}
	}

	// Begin recording frames as we are now in a recording state.
	ctx, cancel := context.WithCancel(ctx) //nolint:gosec
	ch := v.Record(ctx)

	// Clean up temporary files at the end.
	defer func() {
		if v.Options.Video.Output.Frames != "" {
			// Move the frames to the output directory.
			_ = os.Rename(v.Options.Video.Input, v.Options.Video.Output.Frames)
		}

		_ = v.Cleanup()
	}()

	teardown := func() {
		// Stop recording frames.
		cancel()
		// Read from channel to ensure recorder is done.
		<-ch
	}

	// Log errors from the recording process.
	go func() {
		for err := range ch {
			log.Print(err.Error())
		}
	}()

	for _, cmd := range cmds[offset:] {
		if ctx.Err() != nil {
			teardown()
			return []error{ctx.Err()}
		}

		// When changing the FontFamily, FontSize, LineHeight, Padding
		// The xterm.js canvas changes dimensions and causes FFMPEG to not work
		// correctly (specifically) with palettegen.
		// It will be possible to change settings on the fly in the future, but
		// it is currently not as it does not result in a proper render of the
		// GIF as the frame sequence will change dimensions. This is fixable.
		//
		// We should remove if isSetting statement.
		isSetting := cmd.Type == token.SET && cmd.Options != "TypingSpeed"

		if isSetting {
			fmt.Println(ErrorStyle.Render(fmt.Sprintf("WARN: 'Set %s %s' has been ignored. Move the directive to the top of the file.\nLearn more: https://github.com/charmbracelet/vhs#settings", cmd.Options, cmd.Args)))
		}
		if isSetting || cmd.Type == token.REQUIRE {
			_, _ = fmt.Fprintln(out, Highlight(cmd, true))
			continue
		}
		_, _ = fmt.Fprintln(out, Highlight(cmd, !v.recording || cmd.Type == token.SHOW || cmd.Type == token.HIDE || isSetting))
		err := Execute(cmd, &v)
		if err != nil {
			teardown()
			return []error{err}
		}
	}

	// If running as an SSH server, the output file is a temporary file
	// to use for the output.
	//
	// We need to set the GIF file path before it is created but after all of
	// the settings and commands are executed. This is done in `serve.go`.
	//
	// Since the GIF creation is deferred, setting the output file here will
	// achieve what we want.
	for _, opt := range opts {
		opt(&v)
	}

	teardown()
	if err := v.Render(); err != nil {
		return []error{err}
	}
	return nil
}
