// Copyright 2023 PingCAP, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package debugtrace

import (
	"bytes"
	"encoding/json"
	"runtime"

	"github.com/pingcap/tidb/pkg/planner/planctx"
)

// OptimizerDebugTraceRoot is for recording the optimizer debug trace.
// Each debug information, which is a "step" and can be any type, is placed
// in a "context" (baseDebugTraceContext) as an interface{}.
// Overall, the debug trace is a tree-like hierarchical structure of baseDebugTraceContext.
// This structure can reflect the function call hierarchy of each step during optimization.
// In the end, the entire recorded baseDebugTraceContext will be marshalled to JSON as the result.
//
// EnterContextCommon and LeaveContextCommon can be used to maintain the context easily.
// Usually you just need to add the code below at the beginning of a function:
//
//	if StmtCtx.EnableOptimizerDebugTrace {
//	 EnterContextCommon(ds.ctx)
//	 defer LeaveContextCommon(ds.ctx)
//	}
//
// To record debug information, AppendStepToCurrentContext and AppendStepWithNameToCurrentContext
// are provided as low-level methods.
// RecordAnyValuesWithNames handles some common logic for better usability, so it should
// be the most commonly used function for recording simple information.
// If the tracing logic is more complicated or need extra MarshalJSON logic, you should implement
// separate logic like in planner/core/debug_trace.go and statistics/debug_trace.go
type OptimizerDebugTraceRoot struct {
	traceCtx baseDebugTraceContext
	// currentCtx indicates in which baseDebugTraceContext we should record the debug information.
	currentCtx *baseDebugTraceContext
}

// MarshalJSON overrides the default MarshalJSON behavior and marshals the unexported traceCtx.
func (root *OptimizerDebugTraceRoot) MarshalJSON() ([]byte, error) {
	return EncodeJSONCommon(root.traceCtx.steps)
}

// baseDebugTraceContext is the core of the debug trace.
// The steps field can be used to record any information, or point to another baseDebugTraceContext.
type baseDebugTraceContext struct {
	name      string
	steps     []any
	parentCtx *baseDebugTraceContext
}

func (c *baseDebugTraceContext) MarshalJSON() ([]byte, error) {
	var tmp, content any
	if len(c.steps) > 1 {
		content = c.steps
	} else if len(c.steps) == 1 {
		content = c.steps[0]
	}
	if len(c.name) > 0 {
		tmp = map[string]any{
			c.name: content,
		}
	} else {
		tmp = content
	}
	return EncodeJSONCommon(tmp)
}

// AppendStepToCurrentContext records debug information to the current context of the debug trace.
func (root *OptimizerDebugTraceRoot) AppendStepToCurrentContext(step any) {
	root.currentCtx.steps = append(root.currentCtx.steps, step)
}

// AppendStepWithNameToCurrentContext records debug information and a name to the current context of the debug trace.
func (root *OptimizerDebugTraceRoot) AppendStepWithNameToCurrentContext(step any, name string) {
	tmp := map[string]any{
		name: step,
	}
	root.currentCtx.steps = append(root.currentCtx.steps, tmp)
}

// GetOrInitDebugTraceRoot returns the debug trace root.
// If it's not initialized, it will initialize it first.
func GetOrInitDebugTraceRoot(sctx planctx.PlanContext) *OptimizerDebugTraceRoot {
	stmtCtx := sctx.GetSessionVars().StmtCtx
	res, ok := stmtCtx.OptimizerDebugTrace.(*OptimizerDebugTraceRoot)
	if !ok || res == nil {
		trace := &OptimizerDebugTraceRoot{}
		trace.currentCtx = &trace.traceCtx
		// Though it's not needed in theory, we set the parent of the top level context to itself for safety.
		trace.traceCtx.parentCtx = &trace.traceCtx
		stmtCtx.OptimizerDebugTrace = trace
	}
	return stmtCtx.OptimizerDebugTrace.(*OptimizerDebugTraceRoot)
}

// EncodeJSONCommon contains some common logic for the debug trace,
// like disabling EscapeHTML and recording error.
func EncodeJSONCommon(input any) ([]byte, error) {
	var buf bytes.Buffer
	encoder := json.NewEncoder(&buf)
	// If we do not set this to false, ">", "<", "&"... will be escaped to "\u003c","\u003e", "\u0026"...
	encoder.SetEscapeHTML(false)
	err := encoder.Encode(input)
	if err != nil {
		err = encoder.Encode(err)
	}
	return buf.Bytes(), err
}

// EnterContextCommon records the function name of the caller,
// then creates and enter a new context for this debug trace structure.
func EnterContextCommon(sctx planctx.PlanContext) {
	root := GetOrInitDebugTraceRoot(sctx)
	funcName := "Fail to get function name."
	pc, _, _, ok := runtime.Caller(1)
	if ok {
		funcName = runtime.FuncForPC(pc).Name()
	}
	newCtx := &baseDebugTraceContext{
		name:      funcName,
		parentCtx: root.currentCtx,
	}
	root.currentCtx.steps = append(root.currentCtx.steps, newCtx)
	root.currentCtx = newCtx
}

// LeaveContextCommon makes the debug trace goes to its parent context.
func LeaveContextCommon(sctx planctx.PlanContext) {
	root := GetOrInitDebugTraceRoot(sctx)
	root.currentCtx = root.currentCtx.parentCtx
}

// RecordAnyValuesWithNames is a general debug trace logic for recording some values of any type with a name.
// The vals arguments should be a slice like ["name1", value1, "name2", value2].
// The names must be string, the values can be any type.
func RecordAnyValuesWithNames(
	s planctx.PlanContext,
	vals ...any,
) {
	root := GetOrInitDebugTraceRoot(s)
	tmp := make(map[string]any, len(vals)/2)
	for i := 0; i < len(vals); i += 2 {
		str, _ := vals[i].(string)
		val := vals[i+1]
		tmp[str] = val
	}
	root.AppendStepToCurrentContext(tmp)
}
