// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// godoc: Go Documentation Server

// Web server tree:
//
//	http://godoc/	main landing page
//	http://godoc/doc/	serve from $GOROOT/doc - spec, mem, tutorial, etc.
//	http://godoc/src/	serve files from $GOROOT/src; .go gets pretty-printed
//	http://godoc/cmd/	serve documentation about commands (TODO)
//	http://godoc/pkg/	serve documentation about packages
//		(idea is if you say import "compress/zlib", you go to
//		http://godoc/pkg/compress/zlib)
//
// Command-line interface:
//
//	godoc packagepath [name ...]
//
//	godoc compress/zlib
//		- prints doc for package compress/zlib
//	godoc crypto/block Cipher NewCMAC
//		- prints doc for Cipher and NewCMAC in package crypto/block


package main

import (
	"bytes";
	"container/vector";
	"flag";
	"fmt";
	"go/ast";
	"go/doc";
	"go/parser";
	"go/printer";
	"go/scanner";
	"go/token";
	"http";
	"io";
	"log";
	"net";
	"os";
	pathutil "path";
	"sort";
	"strings";
	"sync";
	"syscall";
	"tabwriter";
	"template";
	"time";
)


const Pkg = "/pkg/"	// name for auto-generated package documentation tree


type timeStamp struct {
	mutex sync.RWMutex;
	seconds int64;
}


func (ts *timeStamp) set() {
	ts.mutex.Lock();
	ts.seconds = time.Seconds();
	ts.mutex.Unlock();
}


func (ts *timeStamp) get() int64 {
	ts.mutex.RLock();
	defer ts.mutex.RUnlock();
	return ts.seconds;
}


var (
	verbose = flag.Bool("v", false, "verbose mode");

	// file system roots
	goroot string;
	pkgroot = flag.String("pkgroot", "src/pkg", "root package source directory (if unrooted, relative to goroot)");
	tmplroot = flag.String("tmplroot", "lib/godoc", "root template directory (if unrooted, relative to goroot)");

	// periodic sync
	syncCmd = flag.String("sync", "", "sync command; disabled if empty");
	syncMin = flag.Int("sync_minutes", 0, "sync interval in minutes; disabled if <= 0");
	syncTime timeStamp;  // time of last p4 sync

	// layout control
	tabwidth = flag.Int("tabwidth", 4, "tab width");
	html = flag.Bool("html", false, "print HTML in command-line mode");

	// server control
	httpaddr = flag.String("http", "", "HTTP service address (e.g., ':6060')");
)


func init() {
	goroot = os.Getenv("GOROOT");
	if goroot == "" {
		goroot = "/home/r/go-release/go";
	}
	flag.StringVar(&goroot, "goroot", goroot, "Go root directory");
	syncTime.set();  // have a reasonable initial value
}


// ----------------------------------------------------------------------------
// Support

func isDir(name string) bool {
	d, err := os.Stat(name);
	return err == nil && d.IsDirectory();
}


func isGoFile(dir *os.Dir) bool {
	return dir.IsRegular() && pathutil.Ext(dir.Name) == ".go";
}


func isPkgDir(dir *os.Dir) bool {
	return dir.IsDirectory() && dir.Name != "_obj";
}


func makeTabwriter(writer io.Writer) *tabwriter.Writer {
	return tabwriter.NewWriter(writer, *tabwidth, 1, byte(' '), 0);
}


// ----------------------------------------------------------------------------
// Parsing

// A single error in the parsed file.
type parseError struct {
	src []byte;	// source before error
	line int;	// line number of error
	msg string;	// error message
}


// All the errors in the parsed file, plus surrounding source code.
// Each error has a slice giving the source text preceding it
// (starting where the last error occurred).  The final element in list[]
// has msg = "", to give the remainder of the source code.
// This data structure is handed to the templates parseerror.txt and parseerror.html.
//
type parseErrors struct {
	filename string;	// path to file
	list []parseError;	// the errors
	src []byte;	// the file's entire source code
}


// Parses a file (path) and returns the corresponding AST and
// a sorted list (by file position) of errors, if any.
//
func parse(path string, mode uint) (*ast.File, *parseErrors) {
	src, err := io.ReadFile(path);
	if err != nil {
		log.Stderrf("ReadFile %s: %v", path, err);
		errs := []parseError{parseError{nil, 0, err.String()}};
		return nil, &parseErrors{path, errs, nil};
	}

	prog, err := parser.ParseFile(path, src, mode);
	if err != nil {
		var errs []parseError;
		if errors, ok := err.(scanner.ErrorList); ok {
			// convert error list (already sorted)
			// TODO(gri) If the file contains //line comments, the errors
			//           may not be sorted in increasing file offset value
			//           which will lead to incorrect output.
			errs = make([]parseError, len(errors) + 1);	// +1 for final fragment of source
			offs := 0;
			for i, r := range errors {
				// Should always be true, but check for robustness.
				if 0 <= r.Pos.Offset && r.Pos.Offset <= len(src) {
					errs[i].src = src[offs : r.Pos.Offset];
					offs = r.Pos.Offset;
				}
				errs[i].line = r.Pos.Line;
				errs[i].msg = r.Msg;
			}
			errs[len(errors)].src = src[offs : len(src)];
		} else {
			// single error of unspecified type
			errs = make([]parseError, 2);
			errs[0] = parseError{[]byte{}, 0, err.String()};
			errs[1].src = src;
		}
		return nil, &parseErrors{path, errs, src};
	}

	return prog, nil;
}


// ----------------------------------------------------------------------------
// Templates

// Return text for an AST node.
func nodeText(node interface{}) []byte {
	var buf bytes.Buffer;
	tw := makeTabwriter(&buf);
	mode := uint(0);
	if _, isProgram := node.(*ast.File); isProgram {
		mode = printer.DocComments;
	}
	printer.Fprint(tw, node, mode);
	tw.Flush();
	return buf.Data();
}


// Convert x, whatever it is, to text form.
func toText(x interface{}) []byte {
	type String interface { String() string }

	switch v := x.(type) {
	case []byte:
		return v;
	case string:
		return strings.Bytes(v);
	case String:
		return strings.Bytes(v.String());
	case ast.Decl:
		return nodeText(v);
	case ast.Expr:
		return nodeText(v);
	}
	var buf bytes.Buffer;
	fmt.Fprint(&buf, x);
	return buf.Data();
}


// Template formatter for "html" format.
func htmlFmt(w io.Writer, x interface{}, format string) {
	template.HtmlEscape(w, toText(x));
}


// Template formatter for "html-comment" format.
func htmlCommentFmt(w io.Writer, x interface{}, format string) {
	doc.ToHtml(w, toText(x));
}


// Template formatter for "" (default) format.
func textFmt(w io.Writer, x interface{}, format string) {
	w.Write(toText(x));
}


var fmap = template.FormatterMap{
	"": textFmt,
	"html": htmlFmt,
	"html-comment": htmlCommentFmt,
}


func readTemplate(name string) *template.Template {
	path := pathutil.Join(*tmplroot, name);
	data, err := io.ReadFile(path);
	if err != nil {
		log.Exitf("ReadFile %s: %v", path, err);
	}
	t, err1 := template.Parse(string(data), fmap);
	if err1 != nil {
		log.Exitf("%s: %v", name, err);
	}
	return t;
}


var godocHtml *template.Template
var packageHtml *template.Template
var packageText *template.Template
var parseerrorHtml *template.Template;
var parseerrorText *template.Template;

func readTemplates() {
	// have to delay until after flags processing,
	// so that main has chdir'ed to goroot.
	godocHtml = readTemplate("godoc.html");
	packageHtml = readTemplate("package.html");
	packageText = readTemplate("package.txt");
	parseerrorHtml = readTemplate("parseerror.html");
	parseerrorText = readTemplate("parseerror.txt");
}


// ----------------------------------------------------------------------------
// Generic HTML wrapper

func servePage(c *http.Conn, title, content interface{}) {
	type Data struct {
		title interface{};
		header interface{};
		timestamp string;
		content interface{};
	}

	var d Data;
	d.title = title;
	d.header = title;
	d.timestamp = time.SecondsToLocalTime(syncTime.get()).String();
	d.content = content;
	godocHtml.Execute(&d, c);
}


func serveText(c *http.Conn, text []byte) {
	c.SetHeader("content-type", "text/plain; charset=utf-8");
	c.Write(text);
}


// ----------------------------------------------------------------------------
// Files

func serveParseErrors(c *http.Conn, errors *parseErrors) {
	// format errors
	var buf bytes.Buffer;
	parseerrorHtml.Execute(errors, &buf);
	servePage(c, errors.filename + " - Parse Errors", buf.Data());
}


func serveGoSource(c *http.Conn, name string) {
	prog, errors := parse(name, parser.ParseComments);
	if errors != nil {
		serveParseErrors(c, errors);
		return;
	}

	var buf bytes.Buffer;
	fmt.Fprintln(&buf, "<pre>");
	template.HtmlEscape(&buf, nodeText(prog));
	fmt.Fprintln(&buf, "</pre>");

	servePage(c, name + " - Go source", buf.Data());
}


var fileServer = http.FileServer(".", "");

func serveFile(c *http.Conn, req *http.Request) {
	// pick off special cases and hand the rest to the standard file server
	switch {
	case req.Url.Path == "/":
		// serve landing page.
		// TODO: hide page from ordinary file serving.
		// writing doc/index.html will take care of that.
		http.ServeFile(c, req, "doc/root.html");

	case req.Url.Path == "/doc/root.html":
		// hide landing page from its real name
		// TODO why - there is no reason for this (remove eventually)
		http.NotFound(c, req);

	case pathutil.Ext(req.Url.Path) == ".go":
		serveGoSource(c, req.Url.Path[1 : len(req.Url.Path)]);  // strip leading '/' from name

	default:
		// TODO not good enough - don't want to download files
		// want to see them
		fileServer.ServeHTTP(c, req);
	}
}


// ----------------------------------------------------------------------------
// Packages

type pakDesc struct {
	dirname string;  // relative to goroot
	pakname string;  // same as last component of importpath
	importpath string;	// import "___"
	filenames map[string] bool;  // set of file (names) belonging to this package
}


// TODO if we don't plan to use the directory information, simplify to []string
type dirList []*os.Dir

func (d dirList) Len() int  { return len(d) }
func (d dirList) Less(i, j int) bool  { return d[i].Name < d[j].Name }
func (d dirList) Swap(i, j int)  { d[i], d[j] = d[j], d[i] }


func isPackageFile(dirname, filename, pakname string) bool {
	// ignore test files
	if strings.HasSuffix(filename, "_test.go") {
		return false;
	}

	// determine package name
	prog, errors := parse(dirname + "/" + filename, parser.PackageClauseOnly);
	if prog == nil {
		return false;
	}

	return prog != nil && prog.Name.Value == pakname;
}


// Returns the canonical URL path, the package denoted by path, and
// the list of sub-directories in the corresponding package directory.
// If there is no such package, the package descriptor pd is nil.
// If there are no sub-directories, the dirs list is nil.
func findPackage(path string) (canonical string, pd *pakDesc, dirs dirList) {
	canonical = pathutil.Clean(Pkg + path) + "/";

	// get directory contents, if possible
	importpath := pathutil.Clean(path);  // no trailing '/'
	dirname := pathutil.Join(*pkgroot, importpath);
	if !isDir(dirname) {
		return;
	}

	fd, err1 := os.Open(dirname, os.O_RDONLY, 0);
	if err1 != nil {
		log.Stderrf("open %s: %v", dirname, err1);
		return;
	}

	list, err2 := fd.Readdir(-1);
	if err2 != nil {
		log.Stderrf("readdir %s: %v", dirname, err2);
		return;
	}

	// the package name is the directory name within its parent
	_, pakname := pathutil.Split(dirname);

	// collect all files belonging to the package and count the
	// number of sub-directories
	filenames := make(map[string]bool);
	nsub := 0;
	for i, entry := range list {
		switch {
		case isGoFile(&entry) && isPackageFile(dirname, entry.Name, pakname):
			// add file to package desc
			if tmp, found := filenames[entry.Name]; found {
				panic("internal error: same file added more than once: " + entry.Name);
			}
			filenames[entry.Name] = true;
		case isPkgDir(&entry):
			nsub++;
		}
	}

	// make the list of sub-directories, if any
	var subdirs dirList;
	if nsub > 0 {
		subdirs = make(dirList, nsub);
		nsub = 0;
		for i, entry := range list {
			if isPkgDir(&entry) {
				// make a copy here so sorting (and other code) doesn't
				// have to make one every time an entry is moved
				copy := new(os.Dir);
				*copy = entry;
				subdirs[nsub] = copy;
				nsub++;
			}
		}
		sort.Sort(subdirs);
	}

	// if there are no package files, then there is no package
	if len(filenames) == 0 {
		return canonical, nil, subdirs;
	}

	return canonical, &pakDesc{dirname, pakname, importpath, filenames}, subdirs;
}


func (p *pakDesc) doc() (*doc.PackageDoc, *parseErrors) {
	if p == nil {
		return nil, nil;
	}

	// compute documentation
	// TODO(gri) change doc to work on entire ast.Package at once
	var r doc.DocReader;
	i := 0;
	for filename := range p.filenames {
		src, err := parse(p.dirname + "/" + filename, parser.ParseComments);
		if err != nil {
			return nil, err;
		}
		if i == 0 {
			// first file - initialize doc
			r.Init(src.Name.Value, p.importpath);
		}
		i++;
		ast.FilterExports(src);  // we only care about exports
		r.AddFile(src);
	}

	return r.Doc(), nil;
}


type PageInfo struct {
	PDoc *doc.PackageDoc;
	Dirs dirList;
}

func servePkg(c *http.Conn, r *http.Request) {
	path := r.Url.Path;
	path = path[len(Pkg) : len(path)];
	canonical, desc, dirs := findPackage(path);

	if r.Url.Path != canonical {
		http.Redirect(c, canonical, http.StatusMovedPermanently);
		return;
	}

	pdoc, errors := desc.doc();
	if errors != nil {
		serveParseErrors(c, errors);
		return;
	}

	var buf bytes.Buffer;
	if false {	// TODO req.Params["format"] == "text"
		err := packageText.Execute(PageInfo{pdoc, dirs}, &buf);
		if err != nil {
			log.Stderrf("packageText.Execute: %s", err);
		}
		serveText(c, buf.Data());
		return;
	}

	err := packageHtml.Execute(PageInfo{pdoc, dirs}, &buf);
	if err != nil {
		log.Stderrf("packageHtml.Execute: %s", err);
	}

	if path == "" {
		path = ".";  // don't display an empty path
	}
	servePage(c, path + " - Go package documentation", buf.Data());
}


// ----------------------------------------------------------------------------
// Server

func loggingHandler(h http.Handler) http.Handler {
	return http.HandlerFunc(func(c *http.Conn, req *http.Request) {
		log.Stderrf("%s\t%s", c.RemoteAddr, req.Url);
		h.ServeHTTP(c, req);
	})
}


func exec(c *http.Conn, args []string) bool {
	r, w, err := os.Pipe();
	if err != nil {
		log.Stderrf("os.Pipe(): %v\n", err);
		return false;
	}

	bin := args[0];
	fds := []*os.File{nil, w, w};
	if *verbose {
		log.Stderrf("executing %v", args);
	}
	pid, err := os.ForkExec(bin, args, os.Environ(), goroot, fds);
	defer r.Close();
	w.Close();
	if err != nil {
		log.Stderrf("os.ForkExec(%q): %v\n", bin, err);
		return false;
	}

	var buf bytes.Buffer;
	io.Copy(r, &buf);
	wait, err := os.Wait(pid, 0);
	if err != nil {
		os.Stderr.Write(buf.Data());
		log.Stderrf("os.Wait(%d, 0): %v\n", pid, err);
		return false;
	}
	if !wait.Exited() || wait.ExitStatus() != 0 {
		os.Stderr.Write(buf.Data());
		log.Stderrf("executing %v failed (exit status = %d)", args, wait.ExitStatus());
		return false;
	}

	if *verbose {
		os.Stderr.Write(buf.Data());
	}
	if c != nil {
		c.SetHeader("content-type", "text/plain; charset=utf-8");
		c.Write(buf.Data());
	}

	return true;
}


func sync(c *http.Conn, r *http.Request) {
	args := []string{"/bin/sh", "-c", *syncCmd};
	if !exec(c, args) {
		*syncMin = 0;  // disable sync
		return;
	}
	syncTime.set();
}


func usage() {
	fmt.Fprintf(os.Stderr,
		"usage: godoc package [name ...]\n"
		"	godoc -http=:6060\n"
	);
	flag.PrintDefaults();
	os.Exit(2);
}


func main() {
	flag.Usage = usage;
	flag.Parse();

	// Check usage first; get usage message out early.
	switch {
	case *httpaddr != "":
		if flag.NArg() != 0 {
			usage();
		}
	default:
		if flag.NArg() == 0 {
			usage();
		}
	}

	if err := os.Chdir(goroot); err != nil {
		log.Exitf("chdir %s: %v", goroot, err);
	}

	readTemplates();

	if *httpaddr != "" {
		var handler http.Handler = http.DefaultServeMux;
		if *verbose {
			log.Stderrf("Go Documentation Server\n");
			log.Stderrf("address = %s\n", *httpaddr);
			log.Stderrf("goroot = %s\n", goroot);
			log.Stderrf("pkgroot = %s\n", *pkgroot);
			log.Stderrf("tmplroot = %s\n", *tmplroot);
			handler = loggingHandler(handler);
		}

		http.Handle(Pkg, http.HandlerFunc(servePkg));
		if *syncCmd != "" {
			http.Handle("/debug/sync", http.HandlerFunc(sync));
		}
		http.Handle("/", http.HandlerFunc(serveFile));

		// The server may have been restarted; always wait 1sec to
		// give the forking server a chance to shut down and release
		// the http port.
		time.Sleep(1e9);

		// Start sync goroutine, if enabled.
		if *syncCmd != "" && *syncMin > 0 {
			go func() {
				if *verbose {
					log.Stderrf("sync every %dmin", *syncMin);
				}
				for *syncMin > 0 {
					sync(nil, nil);
					time.Sleep(int64(*syncMin) * (60 * 1e9));
				}
				if *verbose {
					log.Stderrf("periodic sync stopped");
				}
			}();
		}

		if err := http.ListenAndServe(*httpaddr, handler); err != nil {
			log.Exitf("ListenAndServe %s: %v", *httpaddr, err)
		}
		return;
	}

	if *html {
		packageText = packageHtml;
		parseerrorText = parseerrorHtml;
	}

	_, desc, dirs := findPackage(flag.Arg(0));
	pdoc, errors := desc.doc();
	if errors != nil {
		err := parseerrorText.Execute(errors, os.Stderr);
		if err != nil {
			log.Stderrf("parseerrorText.Execute: %s", err);
		}
		os.Exit(1);
	}

	if pdoc != nil && flag.NArg() > 1 {
		args := flag.Args();
		pdoc.Filter(args[1 : len(args)]);
	}

	packageText.Execute(PageInfo{pdoc, dirs}, os.Stdout);
}