// Disassembles compiled program to text assembly instructions

package compiler

import (
	"fmt"
	"io"
	"strings"

	"github.com/benhoyt/goawk/internal/ast"
	"github.com/benhoyt/goawk/internal/resolver"
	"github.com/benhoyt/goawk/lexer"
)

// Disassemble writes a human-readable form of the program's virtual machine
// instructions to writer.
func (p *Program) Disassemble(writer io.Writer) error {
	if p.Begin != nil {
		d := &disassembler{
			program:         p,
			writer:          writer,
			code:            p.Begin,
			nativeFuncNames: p.nativeFuncNames,
		}
		err := d.disassemble("BEGIN")
		if err != nil {
			return err
		}
	}

	for _, action := range p.Actions {
		switch len(action.Pattern) {
		case 0:
			// Nothing to do here.
		case 1:
			d := &disassembler{
				program:         p,
				writer:          writer,
				code:            action.Pattern[0],
				nativeFuncNames: p.nativeFuncNames,
			}
			err := d.disassemble("pattern")
			if err != nil {
				return err
			}
		case 2:
			d := &disassembler{
				program:         p,
				writer:          writer,
				code:            action.Pattern[0],
				nativeFuncNames: p.nativeFuncNames,
			}
			err := d.disassemble("start")
			if err != nil {
				return err
			}
			d = &disassembler{
				program:         p,
				writer:          writer,
				code:            action.Pattern[1],
				nativeFuncNames: p.nativeFuncNames,
			}
			err = d.disassemble("stop")
			if err != nil {
				return err
			}
		}
		if len(action.Body) > 0 {
			d := &disassembler{
				program:         p,
				writer:          writer,
				code:            action.Body,
				nativeFuncNames: p.nativeFuncNames,
			}
			err := d.disassemble("{ body }")
			if err != nil {
				return err
			}
		}
	}

	if p.End != nil {
		d := &disassembler{
			program:         p,
			writer:          writer,
			code:            p.End,
			nativeFuncNames: p.nativeFuncNames,
		}
		err := d.disassemble("END")
		if err != nil {
			return err
		}
	}

	for i, f := range p.Functions {
		d := &disassembler{
			program:         p,
			writer:          writer,
			code:            f.Body,
			nativeFuncNames: p.nativeFuncNames,
			funcIndex:       i,
		}
		err := d.disassemble("function " + f.Name)
		if err != nil {
			return err
		}
	}

	return nil
}

// Disassembles a single block of opcodes.
type disassembler struct {
	program         *Program
	writer          io.Writer
	code            []Opcode
	nativeFuncNames []string
	funcIndex       int
	ip              int
	opAddr          int
	err             error
}

func (d *disassembler) disassemble(prefix string) error {
	if prefix != "" {
		d.writef("        // %s\n", prefix)
	}

	for d.ip < len(d.code) && d.err == nil {
		d.opAddr = d.ip
		op := d.fetch()

		switch op {
		case Num:
			index := d.fetch()
			num := d.program.Nums[index]
			if num == float64(int64(num)) {
				d.writeOpf("Num %d (%d)", int64(num), index)
			} else {
				d.writeOpf("Num %.6g (%d)", num, index)
			}

		case Str:
			index := d.fetch()
			d.writeOpf("Str %q (%d)", d.program.Strs[index], index)

		case FieldInt:
			index := d.fetch()
			d.writeOpf("FieldInt %d", index)

		case FieldByNameStr:
			index := d.fetch()
			d.writeOpf("FieldByNameStr %q (%d)", d.program.Strs[index], index)

		case Global:
			index := d.fetch()
			d.writeOpf("Global %s", d.program.scalarNames[index])

		case Local:
			index := int(d.fetch())
			d.writeOpf("Local %s", d.localName(index))

		case Special:
			index := d.fetch()
			d.writeOpf("Special %s", ast.SpecialVarName(int(index)))

		case ArrayGlobal:
			arrayIndex := d.fetch()
			d.writeOpf("ArrayGlobal %s", d.program.arrayNames[arrayIndex])

		case ArrayLocal:
			arrayIndex := d.fetch()
			d.writeOpf("ArrayLocal %s", d.localArrayName(int(arrayIndex)))

		case InGlobal:
			arrayIndex := d.fetch()
			d.writeOpf("InGlobal %s", d.program.arrayNames[arrayIndex])

		case InLocal:
			arrayIndex := int(d.fetch())
			d.writeOpf("InLocal %s", d.localArrayName(arrayIndex))

		case AssignGlobal:
			index := d.fetch()
			d.writeOpf("AssignGlobal %s", d.program.scalarNames[index])

		case AssignLocal:
			index := int(d.fetch())
			d.writeOpf("AssignLocal %s", d.localName(index))

		case AssignSpecial:
			index := d.fetch()
			d.writeOpf("AssignSpecial %s", ast.SpecialVarName(int(index)))

		case AssignArrayGlobal:
			arrayIndex := d.fetch()
			d.writeOpf("AssignArrayGlobal %s", d.program.arrayNames[arrayIndex])

		case AssignArrayLocal:
			arrayIndex := int(d.fetch())
			d.writeOpf("AssignArrayLocal %s", d.localArrayName(arrayIndex))

		case Delete:
			arrayScope := resolver.Scope(d.fetch())
			arrayIndex := int(d.fetch())
			d.writeOpf("Delete %s", d.arrayName(arrayScope, arrayIndex))

		case DeleteAll:
			arrayScope := resolver.Scope(d.fetch())
			arrayIndex := int(d.fetch())
			d.writeOpf("DeleteAll %s", d.arrayName(arrayScope, arrayIndex))

		case IncrField:
			amount := d.fetch()
			d.writeOpf("IncrField %d", amount)

		case IncrGlobal:
			amount := d.fetch()
			index := d.fetch()
			d.writeOpf("IncrGlobal %d %s", amount, d.program.scalarNames[index])

		case IncrLocal:
			amount := d.fetch()
			index := int(d.fetch())
			d.writeOpf("IncrLocal %d %s", amount, d.localName(index))

		case IncrSpecial:
			amount := d.fetch()
			index := d.fetch()
			d.writeOpf("IncrSpecial %d %s", amount, ast.SpecialVarName(int(index)))

		case IncrArrayGlobal:
			amount := d.fetch()
			arrayIndex := d.fetch()
			d.writeOpf("IncrArrayGlobal %d %s", amount, d.program.arrayNames[arrayIndex])

		case IncrArrayLocal:
			amount := d.fetch()
			arrayIndex := int(d.fetch())
			d.writeOpf("IncrArrayLocal %d %s", amount, d.localArrayName(arrayIndex))

		case AugAssignField:
			operation := AugOp(d.fetch())
			d.writeOpf("AugAssignField %s", operation)

		case AugAssignGlobal:
			operation := AugOp(d.fetch())
			index := d.fetch()
			d.writeOpf("AugAssignGlobal %s %s", operation, d.program.scalarNames[index])

		case AugAssignLocal:
			operation := AugOp(d.fetch())
			index := int(d.fetch())
			d.writeOpf("AugAssignLocal %s %s", operation, d.localName(index))

		case AugAssignSpecial:
			operation := AugOp(d.fetch())
			index := d.fetch()
			d.writeOpf("AugAssignSpecial %s %d", operation, ast.SpecialVarName(int(index)))

		case AugAssignArrayGlobal:
			operation := AugOp(d.fetch())
			arrayIndex := d.fetch()
			d.writeOpf("AugAssignArrayGlobal %s %s", operation, d.program.arrayNames[arrayIndex])

		case AugAssignArrayLocal:
			operation := AugOp(d.fetch())
			arrayIndex := int(d.fetch())
			d.writeOpf("AugAssignArrayLocal %s %s", operation, d.localArrayName(arrayIndex))

		case Regex:
			regexIndex := d.fetch()
			d.writeOpf("Regex %q (%d)", d.program.Regexes[regexIndex], regexIndex)

		case IndexMulti:
			num := d.fetch()
			d.writeOpf("IndexMulti %d", num)

		case ConcatMulti:
			num := d.fetch()
			d.writeOpf("ConcatMulti %d", num)

		case Jump:
			offset := d.fetch()
			d.writeOpf("Jump 0x%04x", d.ip+int(offset))

		case JumpFalse:
			offset := d.fetch()
			d.writeOpf("JumpFalse 0x%04x", d.ip+int(offset))

		case JumpTrue:
			offset := d.fetch()
			d.writeOpf("JumpTrue 0x%04x", d.ip+int(offset))

		case JumpEquals:
			offset := d.fetch()
			d.writeOpf("JumpEquals 0x%04x", d.ip+int(offset))

		case JumpNotEquals:
			offset := d.fetch()
			d.writeOpf("JumpNotEquals 0x%04x", d.ip+int(offset))

		case JumpLess:
			offset := d.fetch()
			d.writeOpf("JumpLess 0x%04x", d.ip+int(offset))

		case JumpGreater:
			offset := d.fetch()
			d.writeOpf("JumpGreater 0x%04x", d.ip+int(offset))

		case JumpLessOrEqual:
			offset := d.fetch()
			d.writeOpf("JumpLessOrEqual 0x%04x", d.ip+int(offset))

		case JumpGreaterOrEqual:
			offset := d.fetch()
			d.writeOpf("JumpGreaterOrEqual 0x%04x", d.ip+int(offset))

		case ForIn:
			varScope := resolver.Scope(d.fetch())
			varIndex := int(d.fetch())
			arrayScope := resolver.Scope(d.fetch())
			arrayIndex := int(d.fetch())
			offset := d.fetch()
			d.writeOpf("ForIn %s %s 0x%04x", d.varName(varScope, varIndex), d.arrayName(arrayScope, arrayIndex), d.ip+int(offset))

		case CallBuiltin:
			builtinOp := BuiltinOp(d.fetch())
			d.writeOpf("CallBuiltin %s", builtinOp)

		case CallLengthArray:
			arrayScope := resolver.Scope(d.fetch())
			arrayIndex := int(d.fetch())
			d.writeOpf("CallLengthArray %s", d.arrayName(arrayScope, arrayIndex))

		case CallSplit:
			arrayScope := resolver.Scope(d.fetch())
			arrayIndex := int(d.fetch())
			d.writeOpf("CallSplit %s", d.arrayName(arrayScope, arrayIndex))

		case CallSplitSep:
			arrayScope := resolver.Scope(d.fetch())
			arrayIndex := int(d.fetch())
			d.writeOpf("CallSplitSep %s", d.arrayName(arrayScope, arrayIndex))

		case CallSprintf:
			numArgs := d.fetch()
			d.writeOpf("CallSprintf %d", numArgs)

		case CallUser:
			funcIndex := d.fetch()
			numArrayArgs := int(d.fetch())
			var arrayArgs []string
			for i := 0; i < numArrayArgs; i++ {
				arrayScope := resolver.Scope(d.fetch())
				arrayIndex := int(d.fetch())
				arrayArgs = append(arrayArgs, d.arrayName(arrayScope, arrayIndex))
			}
			d.writeOpf("CallUser %s [%s]", d.program.Functions[funcIndex].Name, strings.Join(arrayArgs, ", "))

		case CallNative:
			funcIndex := d.fetch()
			numArgs := d.fetch()
			d.writeOpf("CallNative %s %d", d.nativeFuncNames[funcIndex], numArgs)

		case Nulls:
			numNulls := d.fetch()
			d.writeOpf("Nulls %d", numNulls)

		case Print:
			numArgs := d.fetch()
			redirect := lexer.Token(d.fetch())
			if redirect == lexer.ILLEGAL {
				d.writeOpf("Print %d", numArgs)
			} else {
				d.writeOpf("Print %d %s", numArgs, redirect)
			}

		case Printf:
			numArgs := d.fetch()
			redirect := lexer.Token(d.fetch())
			if redirect == lexer.ILLEGAL {
				d.writeOpf("Printf %d", numArgs)
			} else {
				d.writeOpf("Printf %d %s", numArgs, redirect)
			}

		case Getline:
			redirect := lexer.Token(d.fetch())
			d.writeOpf("Getline %s", redirect)

		case GetlineField:
			redirect := lexer.Token(d.fetch())
			d.writeOpf("GetlineField %s", redirect)

		case GetlineGlobal:
			redirect := lexer.Token(d.fetch())
			index := d.fetch()
			d.writeOpf("GetlineGlobal %s %s", redirect, d.program.scalarNames[index])

		case GetlineLocal:
			redirect := lexer.Token(d.fetch())
			index := int(d.fetch())
			d.writeOpf("GetlineLocal %s %s", redirect, d.localName(index))

		case GetlineSpecial:
			redirect := lexer.Token(d.fetch())
			index := d.fetch()
			d.writeOpf("GetlineSpecial %s %s", redirect, ast.SpecialVarName(int(index)))

		case GetlineArray:
			redirect := lexer.Token(d.fetch())
			arrayScope := resolver.Scope(d.fetch())
			arrayIndex := int(d.fetch())
			d.writeOpf("GetlineArray %s %s", redirect, d.arrayName(arrayScope, arrayIndex))

		default:
			// Handles all other opcodes with no arguments
			d.writeOpf("%s", op)
		}
	}

	d.writef("\n")
	return d.err
}

// Fetch the next opcode and increment the "instruction pointer".
func (d *disassembler) fetch() Opcode {
	op := d.code[d.ip]
	d.ip++
	return op
}

// Write formatted string to the disassembly output.
func (d *disassembler) writef(format string, args ...any) {
	if d.err != nil {
		return
	}
	_, d.err = fmt.Fprintf(d.writer, format, args...)
}

// Write formatted opcode (with address and newline) to disassembly output.
func (d *disassembler) writeOpf(format string, args ...any) {
	if d.err != nil {
		return
	}
	addrStr := fmt.Sprintf("%04x", d.opAddr)
	_, d.err = fmt.Fprintf(d.writer, addrStr+"    "+format+"\n", args...)
}

// Return the scalar variable name described by scope and index.
func (d *disassembler) varName(scope resolver.Scope, index int) string {
	switch scope {
	case resolver.Global:
		return d.program.scalarNames[index]
	case resolver.Local:
		return d.localName(index)
	default: // resolver.Special
		return ast.SpecialVarName(index)
	}
}

// Return the local variable name with the given index.
func (d *disassembler) localName(index int) string {
	f := d.program.Functions[d.funcIndex]
	n := 0
	for i, p := range f.Params {
		if f.Arrays[i] {
			continue
		}
		if n == index {
			return p
		}
		n++
	}
	panic(fmt.Sprintf("unexpected local variable index %d", index))
}

// Return the array variable name describes by scope and index.
func (d *disassembler) arrayName(scope resolver.Scope, index int) string {
	if scope == resolver.Local {
		return d.localArrayName(index)
	}
	return d.program.arrayNames[index]
}

// Return the local array name with the given index.
func (d *disassembler) localArrayName(index int) string {
	f := d.program.Functions[d.funcIndex]
	n := 0
	for i, p := range f.Params {
		if !f.Arrays[i] {
			continue
		}
		if n == index {
			return p
		}
		n++
	}
	panic(fmt.Sprintf("unexpected local array index %d", index))
}
