// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package dag

import (
	"bytes"
	"fmt"
	"sort"
	"strings"
)

// DotOpts are the options for generating a dot formatted Graph.
type DotOpts struct {
	// Allows some nodes to decide to only show themselves when the user has
	// requested the "verbose" graph.
	Verbose bool

	// Highlight Cycles
	DrawCycles bool

	// How many levels to expand modules as we draw
	MaxDepth int

	// use this to keep the cluster_ naming convention from the previous dot writer
	cluster bool
}

// GraphNodeDotter can be implemented by a node to cause it to be included
// in the dot graph. The Dot method will be called which is expected to
// return a representation of this node.
type GraphNodeDotter interface {
	// Dot is called to return the dot formatting for the node.
	// The first parameter is the title of the node.
	// The second parameter includes user-specified options that affect the dot
	// graph. See GraphDotOpts below for details.
	DotNode(string, *DotOpts) *DotNode
}

// DotNode provides a structure for Vertices to return in order to specify their
// dot format.
type DotNode struct {
	Name  string
	Attrs map[string]string
}

// Returns the DOT representation of this Graph.
func (g *marshalGraph) Dot(opts *DotOpts) []byte {
	if opts == nil {
		opts = &DotOpts{
			DrawCycles: true,
			MaxDepth:   -1,
			Verbose:    true,
		}
	}

	var w indentWriter
	w.writeString("digraph {\n")
	w.Indent()

	// some dot defaults
	w.writeString(`compound = "true"` + "\n")
	w.writeString(`newrank = "true"` + "\n")

	// the top level graph is written as the first subgraph
	w.writeString(`subgraph "root" {` + "\n")
	g.writeBody(opts, &w)

	// cluster isn't really used other than for naming purposes in some graphs
	opts.cluster = opts.MaxDepth != 0
	maxDepth := opts.MaxDepth
	if maxDepth == 0 {
		maxDepth = -1
	}

	for _, s := range g.Subgraphs {
		g.writeSubgraph(s, opts, maxDepth, &w)
	}

	w.Unindent()
	w.writeString("}\n")
	return w.Bytes()
}

func (v *marshalVertex) dot(g *marshalGraph, opts *DotOpts) []byte {
	var buf bytes.Buffer
	graphName := g.Name
	if graphName == "" {
		graphName = "root"
	}

	name := v.Name
	attrs := v.Attrs
	if v.graphNodeDotter != nil {
		node := v.graphNodeDotter.DotNode(name, opts)
		if node == nil {
			return []byte{}
		}

		newAttrs := make(map[string]string)
		for k, v := range attrs {
			newAttrs[k] = v
		}
		for k, v := range node.Attrs {
			newAttrs[k] = v
		}

		name = node.Name
		attrs = newAttrs
	}

	buf.WriteString(fmt.Sprintf(`"[%s] %s"`, graphName, name))
	writeAttrs(&buf, attrs)
	buf.WriteByte('\n')

	return buf.Bytes()
}

func (e *marshalEdge) dot(g *marshalGraph) string {
	var buf bytes.Buffer
	graphName := g.Name
	if graphName == "" {
		graphName = "root"
	}

	sourceName := g.vertexByID(e.Source).Name
	targetName := g.vertexByID(e.Target).Name
	s := fmt.Sprintf(`"[%s] %s" -> "[%s] %s"`, graphName, sourceName, graphName, targetName)
	buf.WriteString(s)
	writeAttrs(&buf, e.Attrs)

	return buf.String()
}

func cycleDot(e *marshalEdge, g *marshalGraph) string {
	return e.dot(g) + ` [color = "red", penwidth = "2.0"]`
}

// Write the subgraph body. The is recursive, and the depth argument is used to
// record the current depth of iteration.
func (g *marshalGraph) writeSubgraph(sg *marshalGraph, opts *DotOpts, depth int, w *indentWriter) {
	if depth == 0 {
		return
	}
	depth--

	name := sg.Name
	if opts.cluster {
		// we prefix with cluster_ to match the old dot output
		name = "cluster_" + name
		sg.Attrs["label"] = sg.Name
	}
	// writing to the buffer does not produce an error
	_, _ = fmt.Fprintf(w, "subgraph %q {\n", name)
	sg.writeBody(opts, w)

	for _, sg := range sg.Subgraphs {
		g.writeSubgraph(sg, opts, depth, w)
	}
}

func (g *marshalGraph) writeBody(opts *DotOpts, w *indentWriter) {
	w.Indent()

	for _, as := range attrStrings(g.Attrs) {
		w.writeString(as + "\n")
	}

	// list of Vertices that aren't to be included in the dot output
	skip := map[string]bool{}

	for _, v := range g.Vertices {
		if v.graphNodeDotter == nil {
			skip[v.ID] = true
			continue
		}

		_, _ = w.Write(v.dot(g, opts))
	}

	var dotEdges []string

	if opts.DrawCycles {
		for _, c := range g.Cycles {
			if len(c) < 2 {
				continue
			}

			for i, j := 0, 1; i < len(c); i, j = i+1, j+1 {
				if j >= len(c) {
					j = 0
				}
				src := c[i]
				tgt := c[j]

				if skip[src.ID] || skip[tgt.ID] {
					continue
				}

				e := &marshalEdge{
					Name:   fmt.Sprintf("%s|%s", src.Name, tgt.Name),
					Source: src.ID,
					Target: tgt.ID,
					Attrs:  make(map[string]string),
				}

				dotEdges = append(dotEdges, cycleDot(e, g))
				src = tgt
			}
		}
	}

	for _, e := range g.Edges {
		dotEdges = append(dotEdges, e.dot(g))
	}

	// sort these again to match the old output
	sort.Strings(dotEdges)

	for _, e := range dotEdges {
		w.writeString(e + "\n")
	}

	w.Unindent()
	w.writeString("}\n")
}

func writeAttrs(buf *bytes.Buffer, attrs map[string]string) {
	if len(attrs) > 0 {
		buf.WriteString(" [")
		buf.WriteString(strings.Join(attrStrings(attrs), ", "))
		buf.WriteString("]")
	}
}

func attrStrings(attrs map[string]string) []string {
	strings := make([]string, 0, len(attrs))
	for k, v := range attrs {
		strings = append(strings, fmt.Sprintf("%s = %q", k, v))
	}
	sort.Strings(strings)
	return strings
}

// Provide a bytes.Buffer like structure, which will indent when starting a
// newline.
type indentWriter struct {
	bytes.Buffer
	level int
}

func (w *indentWriter) indent() {
	newline := []byte("\n")
	if !bytes.HasSuffix(w.Bytes(), newline) {
		return
	}
	for i := 0; i < w.level; i++ {
		w.Buffer.WriteString("\t")
	}
}

// Indent increases indentation by 1
func (w *indentWriter) Indent() { w.level++ }

// Unindent decreases indentation by 1
func (w *indentWriter) Unindent() { w.level-- }

// the following methods intercept the byte.Buffer writes and insert the
// indentation when starting a new line.
func (w *indentWriter) Write(b []byte) (int, error) {
	w.indent()
	return w.Buffer.Write(b)
}

// writeString is a helper function to write a string to the indentWriter without the need for handling errors.
// the errors are ignored here because writing to a bytes.Buffer should never fail
func (w *indentWriter) writeString(s string) {
	_, _ = w.WriteString(s)
}

func (w *indentWriter) WriteString(s string) (int, error) {
	w.indent()
	return w.Buffer.WriteString(s)
}
func (w *indentWriter) WriteByte(b byte) error {
	w.indent()
	return w.Buffer.WriteByte(b)
}
func (w *indentWriter) WriteRune(r rune) (int, error) {
	w.indent()
	return w.Buffer.WriteRune(r)
}
