// Copyright 2017 Marc-Antoine Ruel. All rights reserved.
// Use of this source code is governed under the Apache License, Version 2.0
// that can be found in the LICENSE file.

//go:generate go run regen.go

package stack

import (
	"fmt"
	"html/template"
	"io"
	"log"
	"net/url"
	"regexp"
	"runtime"
	"strings"
	"time"
)

// ToHTML formats the aggregated buckets as HTML to the writer.
//
// Use footer to add custom HTML at the bottom of the page.
func (a *Aggregated) ToHTML(w io.Writer, footer template.HTML) error {
	data := map[string]interface{}{
		"Aggregated": a,
		"Footer":     footer,
		"Snapshot":   a.Snapshot,
	}
	return toHTML(w, data)
}

// ToHTML formats the snapshot as HTML to the writer.
//
// Use footer to add custom HTML at the bottom of the page.
func (s *Snapshot) ToHTML(w io.Writer, footer template.HTML) error {
	data := map[string]interface{}{
		"Footer":   footer,
		"Snapshot": s,
	}
	return toHTML(w, data)
}

// Private stuff.

func toHTML(w io.Writer, data map[string]interface{}) error {
	m := template.FuncMap{
		"funcClass": funcClass,
		"minus":     minus,
		"pkgURL":    pkgURL,
		"srcURL":    srcURL,
		"symbol":    symbol,
	}
	data["Favicon"] = favicon
	data["GOMAXPROCS"] = runtime.GOMAXPROCS(0)
	data["Now"] = time.Now().Truncate(time.Second)
	data["Version"] = runtime.Version()
	t, err := template.New("t").Funcs(m).Parse(indexHTML)
	if err != nil {
		return err
	}
	return t.Execute(w, data)
}

var reMethodSymbol = regexp.MustCompile(`^\(\*?([^)]+)\)(\..+)$`)

func funcClass(c *Call) template.HTML {
	if c.Func.IsPkgMain {
		return "FuncMain Exported"
	}
	s := c.Location.String()
	if c.Func.IsExported {
		s += " Exported"
	}
	/* #nosec G203 */
	return template.HTML("Func") + template.HTML(template.HTMLEscapeString(s))
}

func minus(i, j int) int {
	return i - j
}

// pkgURL returns a link to the godoc for the call.
func pkgURL(c *Call) template.URL {
	imp := c.ImportPath
	// Check for vendored code first.
	if i := strings.Index(imp, "/vendor/"); i != -1 {
		imp = imp[i+8:]
	}
	ip := escape(imp)
	if ip == "" {
		return ""
	}
	url := template.URL("")
	if c.Location == Stdlib {
		// This always links to the latest release, past releases are not online.
		// That's somewhat unfortunate.
		url = "https://golang.org/pkg/"
	} else {
		// TODO(maruel): Leverage Location.
		// Use pkg.go.dev when there's a version (go module) and godoc.org when
		// there's none (implies branch master).
		_, branch := getSrcBranchURL(c)
		if branch == "master" || branch == "" {
			url = "https://godoc.org/"
		} else {
			url = "https://pkg.go.dev/"
		}
	}
	if c.Func.IsExported {
		return url + ip + template.URL("#") + symbol(&c.Func)
	}
	return url + ip
}

// srcURL returns an URL to the sources.
//
// TODO(maruel): Support custom local godoc server as it serves files too.
func srcURL(c *Call) template.URL {
	url, _ := getSrcBranchURL(c)
	return url
}

func escape(s string) template.URL {
	// That's the only way I found to get the kind of escaping I wanted, where
	// '/' is not escaped.
	u := url.URL{Path: s}
	/* #nosec G203 */
	return template.URL(u.EscapedPath())
}

// getSrcBranchURL returns a link to the source on the web and the tag name for
// the package version, if possible.
func getSrcBranchURL(c *Call) (template.URL, template.URL) {
	tag := ""
	if c.Location == Stdlib {
		// TODO(maruel): This is not strictly speaking correct. The remote could be
		// running a different Go version from the current executable.
		ver := runtime.Version()
		const devel = "devel +"
		if strings.HasPrefix(ver, devel) {
			ver = ver[len(devel) : len(devel)+10]
		}
		tag = url.QueryEscape(ver)
		/* #nosec G203 */
		return template.URL(fmt.Sprintf("https://github.com/golang/go/blob/%s/src/%s#L%d", tag, escape(c.RelSrcPath), c.Line)), template.URL(tag)
	}
	// TODO(maruel): Leverage Location.
	if rel := c.RelSrcPath; rel != "" {
		// Check for vendored code first.
		if i := strings.Index(rel, "/vendor/"); i != -1 {
			rel = rel[i+8:]
		}
		// Specialized support for github and golang.org. This will cover a fair
		// share of the URLs, but it'd be nice to support others too. Please submit
		// a PR (including a unit test that I was too lazy to add yet).
		switch host, rest := splitHost(rel); host {
		case "github.com":
			if parts := strings.SplitN(rest, "/", 3); len(parts) == 3 {
				p, srcTag, tag := splitTag(parts[1])
				url := fmt.Sprintf("https://github.com/%s/%s/blob/%s/%s#L%d", escape(parts[0]), p, srcTag, escape(parts[2]), c.Line)
				/* #nosec G203 */
				return template.URL(url), tag
			}
			log.Printf("problematic github.com URL: %q", rel)
		case "golang.org":
			// https://github.com/golang/build/blob/HEAD/repos/repos.go lists all
			// the golang.org/x/<foo> packages.
			if parts := strings.SplitN(rest, "/", 3); len(parts) == 3 && parts[0] == "x" {
				// parts is: "x", <project@version>, <path inside the repo>.
				p, srcTag, tag := splitTag(parts[1])
				// The source of truth is are actually go.googlesource.com, but
				// github.com has nicer syntax highlighting.
				url := fmt.Sprintf("https://github.com/golang/%s/blob/%s/%s#L%d", p, srcTag, escape(parts[2]), c.Line)
				/* #nosec G203 */
				return template.URL(url), tag
			}
			log.Printf("problematic golang.org URL: %q", rel)
		default:
			// For example gopkg.in. In this case there's no known way to find the
			// link to the source files, but we can still try to extract the version
			// if fetched from a go module.
			// Do a best effort to find a version by searching for a '@'.
			if i := strings.IndexByte(rel, '@'); i != -1 {
				if j := strings.IndexByte(rel[i:], '/'); j != -1 {
					tag = rel[i+1 : i+j]
				}
			}
		}
	}

	if c.LocalSrcPath != "" {
		/* #nosec G203 */
		return template.URL("file:///" + escape(c.LocalSrcPath)), template.URL(tag)
	}
	if c.RemoteSrcPath != "" {
		/* #nosec G203 */
		return template.URL("file:///" + escape(c.RemoteSrcPath)), template.URL(tag)
	}
	return "", ""
}

func splitHost(s string) (string, string) {
	parts := strings.SplitN(s, "/", 2)
	if len(parts) != 2 {
		return parts[0], ""
	}
	return parts[0], parts[1]
}

// "v0.0.0-20200223170610-d5e6a3e2c0ae"
var reVersion = regexp.MustCompile(`v\d+\.\d+\.\d+\-\d+\-([a-f0-9]+)`)

func splitTag(s string) (string, string, template.URL) {
	// Default to branch master for non-versioned dependencies. It's not
	// optimal but it's better than nothing?
	// TODO(maruel): Replace with HEAD.
	i := strings.IndexByte(s, '@')
	if i == -1 {
		// No tag was found.
		return s, "master", "master"
	}
	// We got a versioned go module.
	tag := s[i+1:]
	srcTag := tag
	if m := reVersion.FindStringSubmatch(tag); len(m) != 0 {
		srcTag = m[1]
	}
	/* #nosec G203 */
	return s[:i], url.QueryEscape(srcTag), template.URL(url.QueryEscape(tag))
}

// symbol is the hashtag to use to refer to the symbol when looking at
// documentation.
//
// All of godoc/gddo, pkg.go.dev and golang.org/godoc use the same symbol
// reference format.
func symbol(f *Func) template.URL {
	s := f.Name
	if reMethodSymbol.MatchString(s) {
		// Transform the method form.
		s = reMethodSymbol.ReplaceAllString(s, "$1$2")
	}
	/* #nosec G203 */
	return template.URL(url.QueryEscape(s))
}
