// doc generates HTML files from the comments in header files.
//
// doc expects to be given the path to a JSON file via the --config option.
// From that JSON (which is defined by the Config struct) it reads a list of
// header file locations and generates HTML files for each in the current
// directory.

package main

import (
	"bufio"
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"html/template"
	"io/ioutil"
	"os"
	"path/filepath"
	"strings"
)

// Config describes the structure of the config JSON file.
type Config struct {
	// BaseDirectory is a path to which other paths in the file are
	// relative.
	BaseDirectory string
	Sections      []ConfigSection
}

type ConfigSection struct {
	Name string
	// Headers is a list of paths to header files.
	Headers []string
}

// HeaderFile is the internal representation of a header file.
type HeaderFile struct {
	// Name is the basename of the header file (e.g. "ex_data.html").
	Name string
	// Preamble contains a comment for the file as a whole. Each string
	// is a separate paragraph.
	Preamble []string
	Sections []HeaderSection
}

type HeaderSection struct {
	// Preamble contains a comment for a group of functions.
	Preamble []string
	Decls    []HeaderDecl
	// Num is just the index of the section. It's included in order to help
	// text/template generate anchors.
	Num int
	// IsPrivate is true if the section contains private functions (as
	// indicated by its name).
	IsPrivate bool
}

type HeaderDecl struct {
	// Comment contains a comment for a specific function. Each string is a
	// paragraph. Some paragraph may contain \n runes to indicate that they
	// are preformatted.
	Comment []string
	// Name contains the name of the function, if it could be extracted.
	Name string
	// Decl contains the preformatted C declaration itself.
	Decl string
	// Num is an index for the declaration, but the value is unique for all
	// declarations in a HeaderFile. It's included in order to help
	// text/template generate anchors.
	Num int
}

const (
	cppGuard     = "#if defined(__cplusplus)"
	commentStart = "/* "
	commentEnd   = " */"
)

func extractComment(lines []string, lineNo int) (comment []string, rest []string, restLineNo int, err error) {
	if len(lines) == 0 {
		return nil, lines, lineNo, nil
	}

	restLineNo = lineNo
	rest = lines

	if !strings.HasPrefix(rest[0], commentStart) {
		panic("extractComment called on non-comment")
	}
	commentParagraph := rest[0][len(commentStart):]
	rest = rest[1:]
	restLineNo++

	for len(rest) > 0 {
		i := strings.Index(commentParagraph, commentEnd)
		if i >= 0 {
			if i != len(commentParagraph)-len(commentEnd) {
				err = fmt.Errorf("garbage after comment end on line %d", restLineNo)
				return
			}
			commentParagraph = commentParagraph[:i]
			if len(commentParagraph) > 0 {
				comment = append(comment, commentParagraph)
			}
			return
		}

		line := rest[0]
		if !strings.HasPrefix(line, " *") {
			err = fmt.Errorf("comment doesn't start with block prefix on line %d: %s", restLineNo, line)
			return
		}
		if len(line) == 2 || line[2] != '/' {
			line = line[2:]
		}
		if strings.HasPrefix(line, "   ") {
			/* Identing the lines of a paragraph marks them as
			* preformatted. */
			if len(commentParagraph) > 0 {
				commentParagraph += "\n"
			}
			line = line[3:]
		}
		if len(line) > 0 {
			commentParagraph = commentParagraph + line
			if len(commentParagraph) > 0 && commentParagraph[0] == ' ' {
				commentParagraph = commentParagraph[1:]
			}
		} else {
			comment = append(comment, commentParagraph)
			commentParagraph = ""
		}
		rest = rest[1:]
		restLineNo++
	}

	err = errors.New("hit EOF in comment")
	return
}

func extractDecl(lines []string, lineNo int) (decl string, rest []string, restLineNo int, err error) {
	if len(lines) == 0 {
		return "", lines, lineNo, nil
	}

	rest = lines
	restLineNo = lineNo

	var stack []rune
	for len(rest) > 0 {
		line := rest[0]
		for _, c := range line {
			switch c {
			case '(', '{', '[':
				stack = append(stack, c)
			case ')', '}', ']':
				if len(stack) == 0 {
					err = fmt.Errorf("unexpected %c on line %d", c, restLineNo)
					return
				}
				var expected rune
				switch c {
				case ')':
					expected = '('
				case '}':
					expected = '{'
				case ']':
					expected = '['
				default:
					panic("internal error")
				}
				if last := stack[len(stack)-1]; last != expected {
					err = fmt.Errorf("found %c when expecting %c on line %d", c, last, restLineNo)
					return
				}
				stack = stack[:len(stack)-1]
			}
		}
		if len(decl) > 0 {
			decl += "\n"
		}
		decl += line
		rest = rest[1:]
		restLineNo++

		if len(stack) == 0 && (len(decl) == 0 || decl[len(decl)-1] != '\\') {
			break
		}
	}

	return
}

func skipPast(s, skip string) string {
	i := strings.Index(s, skip)
	if i > 0 {
		return s[i:]
	}
	return s
}

func skipLine(s string) string {
	i := strings.Index(s, "\n")
	if i > 0 {
		return s[i:]
	}
	return ""
}

func getNameFromDecl(decl string) (string, bool) {
	for strings.HasPrefix(decl, "#if") || strings.HasPrefix(decl, "#elif") {
		decl = skipLine(decl)
	}
	if strings.HasPrefix(decl, "struct ") {
		return "", false
	}
	if strings.HasPrefix(decl, "#define ") {
		// This is a preprocessor #define. The name is the next symbol.
		decl = strings.TrimPrefix(decl, "#define ")
		for len(decl) > 0 && decl[0] == ' ' {
			decl = decl[1:]
		}
		i := strings.IndexAny(decl, "( ")
		if i < 0 {
			return "", false
		}
		return decl[:i], true
	}
	decl = skipPast(decl, "STACK_OF(")
	decl = skipPast(decl, "LHASH_OF(")
	i := strings.Index(decl, "(")
	if i < 0 {
		return "", false
	}
	j := strings.LastIndex(decl[:i], " ")
	if j < 0 {
		return "", false
	}
	for j+1 < len(decl) && decl[j+1] == '*' {
		j++
	}
	return decl[j+1 : i], true
}

func (config *Config) parseHeader(path string) (*HeaderFile, error) {
	headerPath := filepath.Join(config.BaseDirectory, path)

	headerFile, err := os.Open(headerPath)
	if err != nil {
		return nil, err
	}
	defer headerFile.Close()

	scanner := bufio.NewScanner(headerFile)
	var lines, oldLines []string
	for scanner.Scan() {
		lines = append(lines, scanner.Text())
	}
	if err := scanner.Err(); err != nil {
		return nil, err
	}

	lineNo := 0
	found := false
	for i, line := range lines {
		lineNo++
		if line == cppGuard {
			lines = lines[i+1:]
			lineNo++
			found = true
			break
		}
	}

	if !found {
		return nil, errors.New("no C++ guard found")
	}

	if len(lines) == 0 || lines[0] != "extern \"C\" {" {
		return nil, errors.New("no extern \"C\" found after C++ guard")
	}
	lineNo += 2
	lines = lines[2:]

	header := &HeaderFile{
		Name: filepath.Base(path),
	}

	for i, line := range lines {
		lineNo++
		if len(line) > 0 {
			lines = lines[i:]
			break
		}
	}

	oldLines = lines
	if len(lines) > 0 && strings.HasPrefix(lines[0], commentStart) {
		comment, rest, restLineNo, err := extractComment(lines, lineNo)
		if err != nil {
			return nil, err
		}

		if len(rest) > 0 && len(rest[0]) == 0 {
			if len(rest) < 2 || len(rest[1]) != 0 {
				return nil, errors.New("preamble comment should be followed by two blank lines")
			}
			header.Preamble = comment
			lineNo = restLineNo + 2
			lines = rest[2:]
		} else {
			lines = oldLines
		}
	}

	var sectionNumber, declNumber int

	for {
		// Start of a section.
		if len(lines) == 0 {
			return nil, errors.New("unexpected end of file")
		}
		line := lines[0]
		if line == cppGuard {
			break
		}

		if len(line) == 0 {
			return nil, fmt.Errorf("blank line at start of section on line %d", lineNo)
		}

		section := HeaderSection{
			Num: sectionNumber,
		}
		sectionNumber++

		if strings.HasPrefix(line, commentStart) {
			comment, rest, restLineNo, err := extractComment(lines, lineNo)
			if err != nil {
				return nil, err
			}
			if len(rest) > 0 && len(rest[0]) == 0 {
				section.Preamble = comment
				section.IsPrivate = len(comment) > 0 && strings.HasPrefix(comment[0], "Private functions")
				lines = rest[1:]
				lineNo = restLineNo + 1
			}
		}

		for len(lines) > 0 {
			line := lines[0]
			if len(line) == 0 {
				lines = lines[1:]
				lineNo++
				break
			}
			if line == cppGuard {
				return nil, errors.New("hit ending C++ guard while in section")
			}

			var comment []string
			var decl string
			if strings.HasPrefix(line, commentStart) {
				comment, lines, lineNo, err = extractComment(lines, lineNo)
				if err != nil {
					return nil, err
				}
			}
			if len(lines) == 0 {
				return nil, errors.New("expected decl at EOF")
			}
			decl, lines, lineNo, err = extractDecl(lines, lineNo)
			if err != nil {
				return nil, err
			}
			name, ok := getNameFromDecl(decl)
			if !ok {
				name = ""
			}
			if last := len(section.Decls) - 1; len(name) == 0 && len(comment) == 0 && last >= 0 {
				section.Decls[last].Decl += "\n" + decl
			} else {
				section.Decls = append(section.Decls, HeaderDecl{
					Comment: comment,
					Name:    name,
					Decl:    decl,
					Num:     declNumber,
				})
				declNumber++
			}

			if len(lines) > 0 && len(lines[0]) == 0 {
				lines = lines[1:]
				lineNo++
			}
		}

		header.Sections = append(header.Sections, section)
	}

	return header, nil
}

func firstSentence(paragraphs []string) string {
	if len(paragraphs) == 0 {
		return ""
	}
	s := paragraphs[0]
	i := strings.Index(s, ". ")
	if i >= 0 {
		return s[:i]
	}
	if lastIndex := len(s) - 1; s[lastIndex] == '.' {
		return s[:lastIndex]
	}
	return s
}

func markupPipeWords(s string) template.HTML {
	ret := ""

	for {
		i := strings.Index(s, "|")
		if i == -1 {
			ret += s
			break
		}
		ret += s[:i]
		s = s[i+1:]

		i = strings.Index(s, "|")
		j := strings.Index(s, " ")
		if i > 0 && (j == -1 || j > i) {
			ret += "<tt>"
			ret += s[:i]
			ret += "</tt>"
			s = s[i+1:]
		} else {
			ret += "|"
		}
	}

	return template.HTML(ret)
}

func markupFirstWord(s template.HTML) template.HTML {
	start := 0
again:
	end := strings.Index(string(s[start:]), " ")
	if end > 0 {
		end += start
		w := strings.ToLower(string(s[start:end]))
		if w == "a" || w == "an" || w == "deprecated:" {
			start = end + 1
			goto again
		}
		return s[:start] + "<span class=\"first-word\">" + s[start:end] + "</span>" + s[end:]
	}
	return s
}

func newlinesToBR(html template.HTML) template.HTML {
	s := string(html)
	if !strings.Contains(s, "\n") {
		return html
	}
	s = strings.Replace(s, "\n", "<br>", -1)
	s = strings.Replace(s, " ", "&nbsp;", -1)
	return template.HTML(s)
}

func generate(outPath string, config *Config) (map[string]string, error) {
	headerTmpl := template.New("headerTmpl")
	headerTmpl.Funcs(template.FuncMap{
		"firstSentence":   firstSentence,
		"markupPipeWords": markupPipeWords,
		"markupFirstWord": markupFirstWord,
		"newlinesToBR":    newlinesToBR,
	})
	headerTmpl, err := headerTmpl.Parse(`<!DOCTYPE html>
<html>
  <head>
    <title>BoringSSL - {{.Name}}</title>
    <meta charset="utf-8">
    <link rel="stylesheet" type="text/css" href="doc.css">
  </head>

  <body>
    <div id="main">
    <h2>{{.Name}}</h2>

    {{range .Preamble}}<p>{{. | html | markupPipeWords}}</p>{{end}}

    <ol>
      {{range .Sections}}
        {{if not .IsPrivate}}
          {{if .Preamble}}<li class="header"><a href="#section-{{.Num}}">{{.Preamble | firstSentence | html | markupPipeWords}}</a></li>{{end}}
          {{range .Decls}}
            {{if .Name}}<li><a href="#decl-{{.Num}}"><tt>{{.Name}}</tt></a></li>{{end}}
          {{end}}
        {{end}}
      {{end}}
    </ol>

    {{range .Sections}}
      {{if not .IsPrivate}}
        <div class="section">
        {{if .Preamble}}
          <div class="sectionpreamble">
          <a name="section-{{.Num}}">
          {{range .Preamble}}<p>{{. | html | markupPipeWords}}</p>{{end}}
          </a>
          </div>
        {{end}}

        {{range .Decls}}
          <div class="decl">
          <a name="decl-{{.Num}}">
          {{range .Comment}}
            <p>{{. | html | markupPipeWords | newlinesToBR | markupFirstWord}}</p>
          {{end}}
          <pre>{{.Decl}}</pre>
          </a>
          </div>
        {{end}}
        </div>
      {{end}}
    {{end}}
    </div>
  </body>
</html>`)
	if err != nil {
		return nil, err
	}

	headerDescriptions := make(map[string]string)

	for _, section := range config.Sections {
		for _, headerPath := range section.Headers {
			header, err := config.parseHeader(headerPath)
			if err != nil {
				return nil, errors.New("while parsing " + headerPath + ": " + err.Error())
			}
			headerDescriptions[header.Name] = firstSentence(header.Preamble)
			filename := filepath.Join(outPath, header.Name+".html")
			file, err := os.OpenFile(filename, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
			if err != nil {
				panic(err)
			}
			defer file.Close()
			if err := headerTmpl.Execute(file, header); err != nil {
				return nil, err
			}
		}
	}

	return headerDescriptions, nil
}

func generateIndex(outPath string, config *Config, headerDescriptions map[string]string) error {
	indexTmpl := template.New("indexTmpl")
	indexTmpl.Funcs(template.FuncMap{
		"baseName": filepath.Base,
		"headerDescription": func(header string) string {
			return headerDescriptions[header]
		},
	})
	indexTmpl, err := indexTmpl.Parse(`<!DOCTYPE html5>

  <head>
    <title>BoringSSL - Headers</title>
    <meta charset="utf-8">
    <link rel="stylesheet" type="text/css" href="doc.css">
  </head>

  <body>
    <div id="main">
      <table>
        {{range .Sections}}
	  <tr class="header"><td colspan="2">{{.Name}}</td></tr>
	  {{range .Headers}}
	    <tr><td><a href="{{. | baseName}}.html">{{. | baseName}}</a></td><td>{{. | baseName | headerDescription}}</td></tr>
	  {{end}}
	{{end}}
      </table>
    </div>
  </body>
</html>`)

	if err != nil {
		return err
	}

	file, err := os.OpenFile(filepath.Join(outPath, "headers.html"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
	if err != nil {
		panic(err)
	}
	defer file.Close()

	if err := indexTmpl.Execute(file, config); err != nil {
		return err
	}

	return nil
}

func main() {
	var (
		configFlag *string = flag.String("config", "doc.config", "Location of config file")
		outputDir  *string = flag.String("out", ".", "Path to the directory where the output will be written")
		config     Config
	)

	flag.Parse()

	if len(*configFlag) == 0 {
		fmt.Printf("No config file given by --config\n")
		os.Exit(1)
	}

	if len(*outputDir) == 0 {
		fmt.Printf("No output directory given by --out\n")
		os.Exit(1)
	}

	configBytes, err := ioutil.ReadFile(*configFlag)
	if err != nil {
		fmt.Printf("Failed to open config file: %s\n", err)
		os.Exit(1)
	}

	if err := json.Unmarshal(configBytes, &config); err != nil {
		fmt.Printf("Failed to parse config file: %s\n", err)
		os.Exit(1)
	}

	headerDescriptions, err := generate(*outputDir, &config)
	if err != nil {
		fmt.Printf("Failed to generate output: %s\n", err)
		os.Exit(1)
	}

	if err := generateIndex(*outputDir, &config, headerDescriptions); err != nil {
		fmt.Printf("Failed to generate index: %s\n", err)
		os.Exit(1)
	}
}
