//go:build gojq_debug

package gojq

import (
	"fmt"
	"io"
	"os"
	"strconv"
	"strings"
)

var (
	debug    bool
	debugOut io.Writer
)

func init() {
	if out := os.Getenv("GOJQ_DEBUG"); out != "" {
		debug = true
		if out == "stdout" {
			debugOut = os.Stdout
		} else {
			debugOut = os.Stderr
		}
	}
}

type codeinfo struct {
	name string
	pc   int
}

func (c *compiler) appendCodeInfo(x any) {
	if !debug {
		return
	}
	var name string
	switch x := x.(type) {
	case string:
		name = x
	default:
		name = fmt.Sprint(x)
	}
	var diff int
	if c.codes[len(c.codes)-1] != nil && c.codes[len(c.codes)-1].op == opret && strings.HasPrefix(name, "end of ") {
		diff = -1
	}
	c.codeinfos = append(c.codeinfos, codeinfo{name, len(c.codes) + diff})
}

func (c *compiler) deleteCodeInfo(name string) {
	for i := 0; i < len(c.codeinfos); i++ {
		if strings.HasSuffix(c.codeinfos[i].name, name) {
			copy(c.codeinfos[i:], c.codeinfos[i+1:])
			c.codeinfos = c.codeinfos[:len(c.codeinfos)-1]
			i--
		}
	}
}

func (env *env) lookupInfoName(pc int) string {
	var name string
	for _, ci := range env.codeinfos {
		if ci.pc == pc {
			if name != "" {
				name += ", "
			}
			name += ci.name
		}
	}
	return name
}

func (env *env) debugCodes() {
	if !debug {
		return
	}
	for i, c := range env.codes {
		pc := i
		switch c.op {
		case opcall, opcallrec:
			if x, ok := c.v.(int); ok {
				pc = x
			}
		case opjump:
			x := c.v.(int)
			if x > 0 && env.codes[x-1].op == opscope {
				pc = x - 1
			}
		}
		var s string
		if name := env.lookupInfoName(pc); name != "" {
			switch c.op {
			case opcall, opcallrec, opjump:
				if !strings.HasPrefix(name, "module ") {
					s = "\t## call " + name
					break
				}
				fallthrough
			default:
				s = "\t## " + name
			}
		}
		fmt.Fprintf(debugOut, "\t%d\t%-*s%s%s\n", i, 25, c.op, debugOperand(c), s)
	}
	fmt.Fprintln(debugOut, "\t"+strings.Repeat("-", 40)+"+")
}

func (env *env) debugState(pc int, backtrack bool) {
	if !debug {
		return
	}
	var sb strings.Builder
	c := env.codes[pc]
	op := c.op.String()
	if backtrack {
		op += " <backtrack>"
	}
	fmt.Fprintf(&sb, "\t%d\t%-*s%s\t|", pc, 25, op, debugOperand(c))
	var xs []int
	for i := env.stack.index; i >= 0; i = env.stack.data[i].next {
		xs = append(xs, i)
	}
	for i := len(xs) - 1; i >= 0; i-- {
		sb.WriteString("\t")
		sb.WriteString(debugValue(env.stack.data[xs[i]].value))
	}
	switch c.op {
	case opcall, opcallrec:
		if x, ok := c.v.(int); ok {
			pc = x
		}
	case opjump:
		x := c.v.(int)
		if x > 0 && env.codes[x-1].op == opscope {
			pc = x - 1
		}
	}
	if name := env.lookupInfoName(pc); name != "" {
		switch c.op {
		case opcall, opcallrec, opjump:
			if !strings.HasPrefix(name, "module ") {
				sb.WriteString("\t\t\t## call " + name)
				break
			}
			fallthrough
		default:
			sb.WriteString("\t\t\t## " + name)
		}
	}
	fmt.Fprintln(debugOut, sb.String())
}

func (env *env) debugForks(pc int, op string) {
	if !debug {
		return
	}
	var sb strings.Builder
	for i, v := range env.forks {
		if i > 0 {
			sb.WriteByte('\t')
		}
		if i == len(env.forks)-1 {
			sb.WriteByte('<')
		}
		fmt.Fprintf(&sb, "%d, %s", v.pc, debugValue(env.stack.data[v.stackindex].value))
		if i == len(env.forks)-1 {
			sb.WriteByte('>')
		}
	}
	fmt.Fprintf(debugOut, "\t-\t%-*s%d\t|\t%s\n", 25, op, pc, sb.String())
}

func debugOperand(c *code) string {
	switch c.op {
	case opcall, opcallrec:
		switch v := c.v.(type) {
		case int:
			return strconv.Itoa(v)
		case [3]any:
			return fmt.Sprintf("%s/%d", v[2], v[1])
		default:
			panic(c)
		}
	default:
		return debugValue(c.v)
	}
}

func debugValue(v any) string {
	switch v := v.(type) {
	case Iter:
		return fmt.Sprintf("gojq.Iter(%#v)", v)
	case []pathValue:
		return fmt.Sprintf("[]gojq.pathValue(%v)", v)
	case [2]int:
		return fmt.Sprintf("[%d,%d]", v[0], v[1])
	case [3]int:
		return fmt.Sprintf("[%d,%d,%d]", v[0], v[1], v[2])
	case [3]any:
		return fmt.Sprintf("[%v,%v,%v]", v[0], v[1], v[2])
	case allocator:
		return fmt.Sprintf("%v", v)
	default:
		return Preview(v)
	}
}
