// Copyright 2016-2018, Pulumi Corporation.
//
// 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 dotconv converts a resource graph into its DOT digraph equivalent.  This is useful for integration with
// various visualization tools, like Graphviz.  Please see http://www.graphviz.org/content/dot-language for a thorough
// specification of the DOT file format.
package dotconv

import (
	"bufio"
	"fmt"
	"io"
	"strconv"
	"strings"

	"github.com/pulumi/pulumi/pkg/v3/graph"
	"github.com/pulumi/pulumi/sdk/v3/go/common/slice"
	"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
)

// Print prints a resource graph.
func Print(g graph.Graph, w io.Writer, dotFragment string) error {
	// Allocate a new writer.  In general, we will ignore write errors throughout this function, for simplicity, opting
	// instead to return the result of flushing the buffer at the end, which is generally latching.
	b := bufio.NewWriter(w)

	// Print the graph header.
	if _, err := b.WriteString("strict digraph {\n"); err != nil {
		return err
	}

	// If the caller provided a fragment then insert it here.
	if dotFragment != "" {
		if _, err := b.WriteString(dotFragment); err != nil {
			return err
		}

		// Ensure that the fragment is followed by newline, this reduces
		// problems if the fragment doesn't end with a semicolon
		if _, err := b.WriteString("\n"); err != nil {
			return err
		}
	}

	// Initialize the frontier with unvisited graph vertices.
	queued := make(map[graph.Vertex]bool)
	frontier := slice.Prealloc[graph.Vertex](len(g.Roots()))
	for _, root := range g.Roots() {
		to := root.To()
		queued[to] = true
		frontier = append(frontier, to)
	}

	// For now, we auto-generate IDs.
	// TODO[pulumi/pulumi#76]: use the object URNs instead, once we have them.
	c := 0
	ids := make(map[graph.Vertex]string)
	getID := func(v graph.Vertex) string {
		if id, has := ids[v]; has {
			return id
		}
		id := "Resource" + strconv.Itoa(c)
		c++
		ids[v] = id
		return id
	}

	// Now, until the frontier is empty, emit entries into the stream.
	indent := "    "
	emitted := make(map[graph.Vertex]bool)
	for len(frontier) > 0 {
		// Dequeue the head of the frontier.
		v := frontier[0]
		frontier = frontier[1:]
		contract.Assertf(!emitted[v], "vertex was emitted twice")
		emitted[v] = true

		// Get and lazily allocate the ID for this vertex.
		id := getID(v)

		// Print this vertex; first its "label" (type) and then its direct dependencies.
		// IDEA: consider serializing properties on the node also.
		if _, err := fmt.Fprintf(b, "%v%v", indent, id); err != nil {
			return err
		}
		if label := v.Label(); label != "" {
			if _, err := fmt.Fprintf(b, " [label=\"%v\"]", label); err != nil {
				return err
			}
		}
		if _, err := b.WriteString(";\n"); err != nil {
			return err
		}

		// Now print out all dependencies as "ID -> {A ... Z}".
		outs := v.Outs()
		if len(outs) > 0 {
			base := fmt.Sprintf("%v%v", indent, id)
			// Print the ID of each dependency and, for those we haven't seen, add them to the frontier.
			for _, out := range outs {
				to := out.To()
				if _, err := fmt.Fprintf(b, "%s -> %s", base, getID(to)); err != nil {
					return err
				}

				var attrs []string
				if out.Color() != "" {
					attrs = append(attrs, fmt.Sprintf("color = \"%s\"", out.Color()))
				}
				if out.Label() != "" {
					attrs = append(attrs, fmt.Sprintf("label = \"%s\"", out.Label()))
				}
				if len(attrs) > 0 {
					if _, err := fmt.Fprintf(b, " [%s]", strings.Join(attrs, ", ")); err != nil {
						return err
					}
				}

				if _, err := b.WriteString(";\n"); err != nil {
					return err
				}

				if _, q := queued[to]; !q {
					queued[to] = true
					frontier = append(frontier, to)
				}
			}
		}
	}

	// Finish the graph.
	if _, err := b.WriteString("}\n"); err != nil {
		return err
	}
	return b.Flush()
}