//go:build antlr.stats

package antlr

import (
	"fmt"
	"log"
	"os"
	"path/filepath"
	"sort"
	"strconv"
	"sync"
)

// This file allows the user to collect statistics about the runtime of the ANTLR runtime. It is not enabled by default
// and so incurs no time penalty. To enable it, you must build the runtime with the antlr.stats build tag.
//

// Tells various components to collect statistics - because it is only true when this file is included, it will
// allow the compiler to completely eliminate all the code that is only used when collecting statistics.
const collectStats = true

// goRunStats is a collection of all the various data the ANTLR runtime has collected about a particular run.
// It is exported so that it can be used by others to look for things that are not already looked for in the
// runtime statistics.
type goRunStats struct {

	// jStats is a slice of all the [JStatRec] records that have been created, which is one for EVERY collection created
	// during a run. It is exported so that it can be used by others to look for things that are not already looked for
	// within this package.
	//
	jStats            []*JStatRec
	jStatsLock        sync.RWMutex
	topN              int
	topNByMax         []*JStatRec
	topNByUsed        []*JStatRec
	unusedCollections map[CollectionSource]int
	counts            map[CollectionSource]int
}

const (
	collectionsFile = "collections"
)

var (
	Statistics = &goRunStats{
		topN: 10,
	}
)

type statsOption func(*goRunStats) error

// Configure allows the statistics system to be configured as the user wants and override the defaults
func (s *goRunStats) Configure(options ...statsOption) error {
	for _, option := range options {
		err := option(s)
		if err != nil {
			return err
		}
	}
	return nil
}

// WithTopN sets the number of things to list in the report when we are concerned with the top N things.
//
// For example, if you want to see the top 20 collections by size, you can do:
//
//	antlr.Statistics.Configure(antlr.WithTopN(20))
func WithTopN(topN int) statsOption {
	return func(s *goRunStats) error {
		s.topN = topN
		return nil
	}
}

// Analyze looks through all the statistical records and computes all the outputs that might be useful to the user.
//
// The function gathers and analyzes a number of statistics about any particular run of
// an ANTLR generated recognizer. In the vast majority of cases, the statistics are only
// useful to maintainers of ANTLR itself, but they can be useful to users as well. They may be
// especially useful in tracking down bugs or performance problems when an ANTLR user could
// supply the output from this package, but cannot supply the grammar file(s) they are using, even
// privately to the maintainers.
//
// The statistics are gathered by the runtime itself, and are not gathered by the parser or lexer, but the user
// must call this function their selves to analyze the statistics. This is because none of the infrastructure is
// extant unless the calling program is built with the antlr.stats tag like so:
//
// go build -tags antlr.stats .
//
// When a program is built with the antlr.stats tag, the Statistics object is created and available outside
// the package. The user can then call the [Statistics.Analyze] function to analyze the statistics and then call the
// [Statistics.Report] function to report the statistics.
//
// Please forward any questions about this package to the ANTLR discussion groups on GitHub or send to them to
// me [Jim Idle] directly at jimi@idle.ws
//
// [Jim Idle]: https:://github.com/jim-idle
func (s *goRunStats) Analyze() {

	// Look for anything that looks strange and record it in our local maps etc for the report to present it
	//
	s.CollectionAnomalies()
	s.TopNCollections()
}

// TopNCollections looks through all the statistical records and gathers the top ten collections by size.
func (s *goRunStats) TopNCollections() {

	// Let's sort the stat records by MaxSize
	//
	sort.Slice(s.jStats, func(i, j int) bool {
		return s.jStats[i].MaxSize > s.jStats[j].MaxSize
	})

	for i := 0; i < len(s.jStats) && i < s.topN; i++ {
		s.topNByMax = append(s.topNByMax, s.jStats[i])
	}

	// Sort by the number of times used
	//
	sort.Slice(s.jStats, func(i, j int) bool {
		return s.jStats[i].Gets+s.jStats[i].Puts > s.jStats[j].Gets+s.jStats[j].Puts
	})
	for i := 0; i < len(s.jStats) && i < s.topN; i++ {
		s.topNByUsed = append(s.topNByUsed, s.jStats[i])
	}
}

// Report dumps a markdown formatted report of all the statistics collected during a run to the given dir output
// path, which should represent a directory. Generated files will be prefixed with the given prefix and will be
// given a type name such as `anomalies` and a time stamp such as `2021-09-01T12:34:56` and a .md suffix.
func (s *goRunStats) Report(dir string, prefix string) error {

	isDir, err := isDirectory(dir)
	switch {
	case err != nil:
		return err
	case !isDir:
		return fmt.Errorf("output directory `%s` is not a directory", dir)
	}
	s.reportCollections(dir, prefix)

	// Clean out any old data in case the user forgets
	//
	s.Reset()
	return nil
}

func (s *goRunStats) Reset() {
	s.jStats = nil
	s.topNByUsed = nil
	s.topNByMax = nil
}

func (s *goRunStats) reportCollections(dir, prefix string) {
	cname := filepath.Join(dir, ".asciidoctor")
	// If the file doesn't exist, create it, or append to the file
	f, err := os.OpenFile(cname, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		log.Fatal(err)
	}
	_, _ = f.WriteString(`// .asciidoctorconfig
++++
<style>
body {
font-family: "Quicksand", "Montserrat", "Helvetica";
background-color: black;
}
</style>
++++`)
	_ = f.Close()

	fname := filepath.Join(dir, prefix+"_"+"_"+collectionsFile+"_"+".adoc")
	// If the file doesn't exist, create it, or append to the file
	f, err = os.OpenFile(fname, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		log.Fatal(err)
	}
	defer func(f *os.File) {
		err := f.Close()
		if err != nil {
			log.Fatal(err)
		}
	}(f)
	_, _ = f.WriteString("= Collections for " + prefix + "\n\n")

	_, _ = f.WriteString("== Summary\n")

	if s.unusedCollections != nil {
		_, _ = f.WriteString("=== Unused Collections\n")
		_, _ = f.WriteString("Unused collections incur a penalty for allocation that makes them a candidate for either\n")
		_, _ = f.WriteString(" removal or optimization. If you are using a collection that is not used, you should\n")
		_, _ = f.WriteString(" consider removing it. If you are using a collection that is used, but not very often,\n")
		_, _ = f.WriteString(" you should consider using lazy initialization to defer the allocation until it is\n")
		_, _ = f.WriteString(" actually needed.\n\n")

		_, _ = f.WriteString("\n.Unused collections\n")
		_, _ = f.WriteString(`[cols="<3,>1"]` + "\n\n")
		_, _ = f.WriteString("|===\n")
		_, _ = f.WriteString("| Type | Count\n")

		for k, v := range s.unusedCollections {
			_, _ = f.WriteString("| " + CollectionDescriptors[k].SybolicName + " | " + strconv.Itoa(v) + "\n")
		}
		f.WriteString("|===\n\n")
	}

	_, _ = f.WriteString("\n.Summary of Collections\n")
	_, _ = f.WriteString(`[cols="<3,>1"]` + "\n\n")
	_, _ = f.WriteString("|===\n")
	_, _ = f.WriteString("| Type | Count\n")
	for k, v := range s.counts {
		_, _ = f.WriteString("| " + CollectionDescriptors[k].SybolicName + " | " + strconv.Itoa(v) + "\n")
	}
	_, _ = f.WriteString("| Total | " + strconv.Itoa(len(s.jStats)) + "\n")
	_, _ = f.WriteString("|===\n\n")

	_, _ = f.WriteString("\n.Summary of Top " + strconv.Itoa(s.topN) + " Collections by MaxSize\n")
	_, _ = f.WriteString(`[cols="<1,<3,>1,>1,>1,>1"]` + "\n\n")
	_, _ = f.WriteString("|===\n")
	_, _ = f.WriteString("| Source | Description | MaxSize | EndSize | Puts | Gets\n")
	for _, c := range s.topNByMax {
		_, _ = f.WriteString("| " + CollectionDescriptors[c.Source].SybolicName + "\n")
		_, _ = f.WriteString("| " + c.Description + "\n")
		_, _ = f.WriteString("| " + strconv.Itoa(c.MaxSize) + "\n")
		_, _ = f.WriteString("| " + strconv.Itoa(c.CurSize) + "\n")
		_, _ = f.WriteString("| " + strconv.Itoa(c.Puts) + "\n")
		_, _ = f.WriteString("| " + strconv.Itoa(c.Gets) + "\n")
		_, _ = f.WriteString("\n")
	}
	_, _ = f.WriteString("|===\n\n")

	_, _ = f.WriteString("\n.Summary of Top " + strconv.Itoa(s.topN) + " Collections by Access\n")
	_, _ = f.WriteString(`[cols="<1,<3,>1,>1,>1,>1,>1"]` + "\n\n")
	_, _ = f.WriteString("|===\n")
	_, _ = f.WriteString("| Source | Description | MaxSize | EndSize | Puts | Gets | P+G\n")
	for _, c := range s.topNByUsed {
		_, _ = f.WriteString("| " + CollectionDescriptors[c.Source].SybolicName + "\n")
		_, _ = f.WriteString("| " + c.Description + "\n")
		_, _ = f.WriteString("| " + strconv.Itoa(c.MaxSize) + "\n")
		_, _ = f.WriteString("| " + strconv.Itoa(c.CurSize) + "\n")
		_, _ = f.WriteString("| " + strconv.Itoa(c.Puts) + "\n")
		_, _ = f.WriteString("| " + strconv.Itoa(c.Gets) + "\n")
		_, _ = f.WriteString("| " + strconv.Itoa(c.Gets+c.Puts) + "\n")
		_, _ = f.WriteString("\n")
	}
	_, _ = f.WriteString("|===\n\n")
}

// AddJStatRec adds a [JStatRec] record to the [goRunStats] collection when build runtimeConfig antlr.stats is enabled.
func (s *goRunStats) AddJStatRec(rec *JStatRec) {
	s.jStatsLock.Lock()
	defer s.jStatsLock.Unlock()
	s.jStats = append(s.jStats, rec)
}

// CollectionAnomalies looks through all the statistical records and gathers any anomalies that have been found.
func (s *goRunStats) CollectionAnomalies() {
	s.jStatsLock.RLock()
	defer s.jStatsLock.RUnlock()
	s.counts = make(map[CollectionSource]int, len(s.jStats))
	for _, c := range s.jStats {

		// Accumlate raw counts
		//
		s.counts[c.Source]++

		// Look for allocated but unused collections and count them
		if c.MaxSize == 0 && c.Puts == 0 {
			if s.unusedCollections == nil {
				s.unusedCollections = make(map[CollectionSource]int)
			}
			s.unusedCollections[c.Source]++
		}
		if c.MaxSize > 6000 {
			fmt.Println("Collection ", c.Description, "accumulated a max size of ", c.MaxSize, " - this is probably too large and indicates a poorly formed grammar")
		}
	}

}
