// Licensed to Apache Software Foundation (ASF) under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Apache Software Foundation (ASF) licenses this file to you 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 flamegraph

import (
	"bytes"
	"context"
	"fmt"
	"html/template"
	"os"
	"path/filepath"
	"time"

	"github.com/apache/skywalking-cli/pkg/logger"

	api "skywalking.apache.org/repo/goapi/query"
)

func DisplayByTrace(ctx context.Context, analysis api.ProfileAnalyzation) error {
	trees := make([]*ProfilingDataTree, 0)
	for _, tree := range analysis.Trees {
		elements := make([]ProfilingDataStackElement, 0)
		for _, e := range tree.Elements {
			elements = append(elements, &traceStackElementAdapter{e})
		}
		trees = append(trees, &ProfilingDataTree{Elements: elements})
	}

	return display(ctx, trees)
}

func DisplayByEBPF(ctx context.Context, analysis *api.EBPFProfilingAnalyzation) error {
	trees := make([]*ProfilingDataTree, 0)
	for _, tree := range analysis.Trees {
		elements := make([]ProfilingDataStackElement, 0)
		for _, e := range tree.Elements {
			elements = append(elements, &eBPFStackElementAdapter{e})
		}
		trees = append(trees, &ProfilingDataTree{Elements: elements})
	}

	return display(ctx, trees)
}

func display(_ context.Context, trees []*ProfilingDataTree) error {
	if len(trees) == 0 {
		return fmt.Errorf("could not find the analysis data")
	}

	// generate flame graph file path for write
	flameGraphPath, err := generateFlameGraphFile()
	if err != nil {
		return err
	}

	// build data
	data := make(map[string]any)
	elements, maxDepth := buildFlameGraphElements(trees)
	data["elements"] = elements
	data["maxDepth"] = maxDepth + 1
	data["canvasHeight"] = maxDepth*16 + 30

	// render template
	return renderFlameGraphAndWrite(flameGraphPath, data)
}

func renderFlameGraphAndWrite(path string, data map[string]any) error {
	// render template
	var b bytes.Buffer
	tmpl, err := template.New("flameGraphTemplate").Parse(flameGraphHTML)
	if err != nil {
		return fmt.Errorf("failed to parse flame graph template: %v", err)
	}
	if err = tmpl.Execute(&b, data); err != nil {
		return fmt.Errorf("failed to render flame graph: %v", err)
	}

	// write to file
	file, err := os.Create(path)
	if err != nil {
		return fmt.Errorf("could not create the flame graph file, %v", err)
	}
	_, err = file.Write(b.Bytes())
	if err != nil {
		return fmt.Errorf("could not write the flame graph to file, %v", err)
	}
	logger.Log.Infof("success write the flame graph to: %s", path)
	return nil
}

func generateFlameGraphFile() (string, error) {
	wd, err := os.Getwd()
	if err != nil {
		return "", fmt.Errorf("could not get current directory: %v", err)
	}
	flameGraphPath := filepath.Join(wd, buildFileName())
	return flameGraphPath, nil
}

func buildFileName() string {
	return fmt.Sprintf("flame_graph_%d.html", time.Now().Unix())
}

func buildFlameGraphElements(trees []*ProfilingDataTree) (result []*StackElement, maxDepth int64) {
	// adding the root element
	root := &StackElement{Symbol: "all"}
	result = append(result, root)

	// build elements
	var left int64
	for _, t := range trees {
		result, left = buildFlameGraphChildElements(result, t.Elements, nil, left)
	}

	// calculate the root total count
	for _, r := range result {
		if r.ParentID == "0" {
			root.Count += r.Count
		}
		if maxDepth < r.Depth {
			maxDepth = r.Depth
		}
	}
	return result, maxDepth
}

func buildFlameGraphChildElements(renderElements []*StackElement, dataElements []ProfilingDataStackElement,
	parent *StackElement, relativeLeft int64,
) (result []*StackElement, left int64) {
	parentID := "0"
	var depth int64 = 1
	left = relativeLeft
	if parent != nil {
		parentID = parent.ID
		depth = parent.Depth + 1
		left += parent.Left
	}
	es := findProfilingElementByParentID(dataElements, parentID)
	for _, element := range es {
		current := GenerateStackElementByProfilingData(element, depth, left)
		renderElements = append(renderElements, current)

		left += element.DumpCount()
		renderElements, _ = buildFlameGraphChildElements(renderElements, dataElements, current, relativeLeft)
	}

	return renderElements, left
}

func findProfilingElementByParentID(elements []ProfilingDataStackElement, parentID string) []ProfilingDataStackElement {
	children := make([]ProfilingDataStackElement, 0)
	for _, e := range elements {
		if e.ParentID() == parentID {
			children = append(children, e)
		}
	}
	return children
}
