A library for working with Pijul repositories in Go
// The tijo-graph command generates a graph to help visualize the internal
// structure of a pijul repository. It is similar to `pijul debug`, but
// it uses D2 instead of GraphViz. It is primarily intended to help dubug
// the pijul-go package.
package main

import (
	"bytes"
	"context"
	"flag"
	"fmt"
	"os"
	"os/exec"
	"strconv"
	"strings"

	"pijul-go"

	"github.com/klausman/hexdump"
	"oss.terrastruct.com/d2/d2layouts/d2dagrelayout"
	"oss.terrastruct.com/d2/d2lib"
	"oss.terrastruct.com/d2/d2renderers/d2svg"
	"oss.terrastruct.com/d2/lib/textmeasure"
)

var svgFile = flag.String("svg", "", "write an SVG file instead of printing D2 script")

func printErrorAndExit(description string, err error) {
	msg := err.Error()
	if err, ok := err.(*exec.ExitError); ok && len(err.Stderr) > 0 {
		msg = string(err.Stderr)
	}
	fmt.Fprintln(os.Stderr, description, msg)
	os.Exit(2)
}

func main() {
	flag.Parse()

	hashLog, err := exec.Command("pijul", "log", "--hash-only").Output()
	if err != nil {
		printErrorAndExit("error listing changes:", err)
	}

	hashes := strings.Fields(string(hashLog))
	parsedHashes := make([]pijul.Hash, 0, len(hashes))
	for i := len(hashes) - 1; i >= 0; i-- {
		h, err := pijul.HashFromBase32(hashes[i])
		if err != nil {
			printErrorAndExit("", fmt.Errorf("error parsing hash %q: %v", hashes[i], err))
		}
		parsedHashes = append(parsedHashes, h)
	}

	pristine, err := loadChanges(parsedHashes)
	if err != nil {
		printErrorAndExit("error loading changes:", err)
	}

	graph := makeGraph(pristine, parsedHashes)

	if *svgFile == "" {
		os.Stdout.Write(graph)
		return
	}

	ruler, err := textmeasure.NewRuler()
	if err != nil {
		printErrorAndExit("error from textmeasure.NewRuler:", err)
	}

	diagram, _, err := d2lib.Compile(context.Background(), string(graph), &d2lib.CompileOptions{
		Layout: d2dagrelayout.DefaultLayout,
		Ruler:  ruler,
	})
	if err != nil {
		printErrorAndExit("error compiling diagram:", err)
	}

	svg, err := d2svg.Render(diagram, &d2svg.RenderOpts{
		Pad: d2svg.DEFAULT_PADDING,
	})
	if err != nil {
		printErrorAndExit("error rendering SVG:", err)
	}

	err = os.WriteFile(*svgFile, svg, 0644)
	if err != nil {
		printErrorAndExit("error writing SVG file:", err)
	}
}

func loadChanges(hashes []pijul.Hash) (*pijul.Graph, error) {
	pristine := pijul.NewGraph()
	for _, h := range hashes {
		hs := h.String()
		data, err := os.ReadFile(fmt.Sprintf(".pijul/changes/%s/%s.change", hs[:2], hs[2:]))
		if err != nil {
			return nil, fmt.Errorf("error reading change %s: %w", hs, err)
		}
		c, err := pijul.DeserializeChange(data)
		if err != nil {
			return nil, fmt.Errorf("error deserializing change %s: %w", hs, err)
		}
		err = pristine.ApplyChange(h, c)
		if err != nil {
			return nil, fmt.Errorf("error applying change %s: %w", hs, err)
		}
	}
	return pristine, nil
}

func makeGraph(pristine *pijul.Graph, hashes []pijul.Hash) []byte {
	b := new(bytes.Buffer)

	graphBlock(b, pristine.Root)
	for _, h := range hashes {
		for _, block := range pristine.Index[h] {
			graphBlock(b, block)
		}
	}

	return b.Bytes()
}

func blockID(block *pijul.Block) string {
	hash := block.Change.String()
	if len(hash) > 8 {
		hash = hash[:8]
	}
	return fmt.Sprintf("%s:%d:%d", hash, block.Start, block.End)
}

func graphBlock(b *bytes.Buffer, block *pijul.Block) {
	fmt.Fprintf(b, "%q: {\n", blockID(block))

	if bytes.ContainsAny(block.Content, "\x00") {
		fmt.Fprintf(b, "  content: %s {shape: code}\n", quoteString(hexdump.Dump(block.Content, 0, 4)))
	} else if len(block.Content) > 0 {
		fmt.Fprintf(b, "  content: %s {shape: code}\n", quoteString(string(block.Content)))
	}

	fmt.Fprintf(b, "}\n")

	for _, e := range block.Edges {
		style := "stroke: black"
		switch e.Flag {
		case 17:
			style = "stroke: royalblue"
		case 0, 1:
			style = "stroke: forestgreen"
		case 128, 129:
			style = "stroke: forestgreen; stroke-dash: 3"
		}
		fmt.Fprintf(b, "%q -> %q {style: {%s}}\n", blockID(e.From), blockID(e.To), style)
	}
}

func quoteString(s string) string {
	return strings.Replace(strconv.Quote(s), "$", `\x24`, -1)
}