package vm

import (
	"bytes"
	"fmt"
	"io"
	"reflect"
	"regexp"
	"strings"
	"text/tabwriter"

	"github.com/expr-lang/expr/ast"
	"github.com/expr-lang/expr/builtin"
	"github.com/expr-lang/expr/file"
	"github.com/expr-lang/expr/vm/runtime"
)

// Program represents a compiled expression.
type Program struct {
	Bytecode  []Opcode
	Arguments []int
	Constants []any

	source    file.Source
	node      ast.Node
	locations []file.Location
	variables int
	functions []Function
	debugInfo map[string]string
	span      *Span
}

// NewProgram returns a new Program. It's used by the compiler.
func NewProgram(
	source file.Source,
	node ast.Node,
	locations []file.Location,
	variables int,
	constants []any,
	bytecode []Opcode,
	arguments []int,
	functions []Function,
	debugInfo map[string]string,
	span *Span,
) *Program {
	return &Program{
		source:    source,
		node:      node,
		locations: locations,
		variables: variables,
		Constants: constants,
		Bytecode:  bytecode,
		Arguments: arguments,
		functions: functions,
		debugInfo: debugInfo,
		span:      span,
	}
}

// Source returns origin file.Source.
func (program *Program) Source() file.Source {
	return program.source
}

// Node returns origin ast.Node.
func (program *Program) Node() ast.Node {
	return program.node
}

// Locations returns a slice of bytecode's locations.
func (program *Program) Locations() []file.Location {
	return program.locations
}

// Disassemble returns opcodes as a string.
func (program *Program) Disassemble() string {
	var buf bytes.Buffer
	w := tabwriter.NewWriter(&buf, 0, 0, 2, ' ', 0)
	program.DisassembleWriter(w)
	_ = w.Flush()
	return buf.String()
}

// DisassembleWriter takes a writer and writes opcodes to it.
func (program *Program) DisassembleWriter(w io.Writer) {
	ip := 0
	for ip < len(program.Bytecode) {
		pp := ip
		op := program.Bytecode[ip]
		arg := program.Arguments[ip]
		ip += 1

		code := func(label string) {
			_, _ = fmt.Fprintf(w, "%v\t%v\n", pp, label)
		}
		jump := func(label string) {
			_, _ = fmt.Fprintf(w, "%v\t%v\t<%v>\t(%v)\n", pp, label, arg, ip+arg)
		}
		jumpBack := func(label string) {
			_, _ = fmt.Fprintf(w, "%v\t%v\t<%v>\t(%v)\n", pp, label, arg, ip-arg)
		}
		argument := func(label string) {
			_, _ = fmt.Fprintf(w, "%v\t%v\t<%v>\n", pp, label, arg)
		}
		argumentWithInfo := func(label string, prefix string) {
			_, _ = fmt.Fprintf(w, "%v\t%v\t<%v>\t%v\n", pp, label, arg, program.debugInfo[fmt.Sprintf("%s_%d", prefix, arg)])
		}
		constant := func(label string) {
			var c any
			if arg < len(program.Constants) {
				c = program.Constants[arg]
			} else {
				c = "out of range"
			}
			if name, ok := program.debugInfo[fmt.Sprintf("const_%d", arg)]; ok {
				c = name
			}
			if r, ok := c.(*regexp.Regexp); ok {
				c = r.String()
			}
