// Package lint provides abstractions on top of go/analysis.
// These abstractions add extra information to analyzes, such as structured documentation and severities.
package lint

import (
	"flag"
	"fmt"
	"go/ast"
	"go/build"
	"go/token"
	"regexp"
	"strconv"
	"strings"

	"golang.org/x/tools/go/analysis"
)

// Analyzer wraps a go/analysis.Analyzer and provides structured documentation.
type Analyzer struct {
	// The analyzer's documentation. Unlike go/analysis.Analyzer.Doc,
	// this field is structured, providing access to severity, options
	// etc.
	Doc      *Documentation
	Analyzer *analysis.Analyzer
}

func (a *Analyzer) initialize() {
	a.Analyzer.Doc = a.Doc.String()
	if a.Analyzer.Flags.Usage == nil {
		fs := flag.NewFlagSet("", flag.PanicOnError)
		fs.Var(newVersionFlag(), "go", "Target Go version")
		a.Analyzer.Flags = *fs
	}
}

// InitializeAnalyzers takes a map of documentation and a map of go/analysis.Analyzers and returns a slice of Analyzers.
// The map keys are the analyzer names.
func InitializeAnalyzers(docs map[string]*Documentation, analyzers map[string]*analysis.Analyzer) []*Analyzer {
	out := make([]*Analyzer, 0, len(analyzers))
	for k, v := range analyzers {
		v.Name = k
		a := &Analyzer{
			Doc:      docs[k],
			Analyzer: v,
		}
		a.initialize()
		out = append(out, a)
	}
	return out
}

// Severity describes the severity of diagnostics reported by an analyzer.
type Severity int

const (
	SeverityNone Severity = iota
	SeverityError
	SeverityDeprecated
	SeverityWarning
	SeverityInfo
	SeverityHint
)

// MergeStrategy sets how merge mode should behave for diagnostics of an analyzer.
type MergeStrategy int

const (
	MergeIfAny MergeStrategy = iota
	MergeIfAll
)

type RawDocumentation struct {
	Title      string
	Text       string
	Before     string
	After      string
	Since      string
	NonDefault bool
	Options    []string
	Severity   Severity
	MergeIf    MergeStrategy
}

type Documentation struct {
	Title string
	Text  string

	TitleMarkdown string
	TextMarkdown  string

	Before     string
	After      string
	Since      string
	NonDefault bool
	Options    []string
	Severity   Severity
	MergeIf    MergeStrategy
}

func Markdownify(m map[string]*RawDocumentation) map[string]*Documentation {
	out := make(map[string]*Documentation, len(m))
	for k, v := range m {
		out[k] = &Documentation{
			Title: strings.TrimSpace(stripMarkdown(v.Title)),
			Text:  strings.TrimSpace(stripMarkdown(v.Text)),

			TitleMarkdown: strings.TrimSpace(toMarkdown(v.Title)),
			TextMarkdown:  strings.TrimSpace(toMarkdown(v.Text)),

			Before:     strings.TrimSpace(v.Before),
			After:      strings.TrimSpace(v.After),
			Since:      v.Since,
			NonDefault: v.NonDefault,
			Options:    v.Options,
			Severity:   v.Severity,
			MergeIf:    v.MergeIf,
		}
	}
	return out
}

func toMarkdown(s string) string {
	return strings.NewReplacer(`\'`, "`", `\"`, "`").Replace(s)
}

func stripMarkdown(s string) string {
	return strings.NewReplacer(`\'`, "", `\"`, "'").Replace(s)
}

func (doc *Documentation) Format(metadata bool) string {
	return doc.format(false, metadata)
}

func (doc *Documentation) FormatMarkdown(metadata bool) string {
	return doc.format(true, metadata)
}

func (doc *Documentation) format(markdown bool, metadata bool) string {
	b := &strings.Builder{}
	if markdown {
		fmt.Fprintf(b, "%s\n\n", doc.TitleMarkdown)
		if doc.Text != "" {
			fmt.Fprintf(b, "%s\n\n", doc.TextMarkdown)
		}
	} else {
		fmt.Fprintf(b, "%s\n\n", doc.Title)
		if doc.Text != "" {
			fmt.Fprintf(b, "%s\n\n", doc.Text)
		}
	}

	if doc.Before != "" {
		fmt.Fprintln(b, "Before:")
		fmt.Fprintln(b, "")
		for _, line := range strings.Split(doc.Before, "\n") {
			fmt.Fprint(b, "    ", line, "\n")
		}
		fmt.Fprintln(b, "")
		fmt.Fprintln(b, "After:")
		fmt.Fprintln(b, "")
		for _, line := range strings.Split(doc.After, "\n") {
			fmt.Fprint(b, "    ", line, "\n")
		}
		fmt.Fprintln(b, "")
	}

	if metadata {
		fmt.Fprint(b, "Available since\n    ")
		if doc.Since == "" {
			fmt.Fprint(b, "unreleased")
		} else {
			fmt.Fprintf(b, "%s", doc.Since)
		}
		if doc.NonDefault {
			fmt.Fprint(b, ", non-default")
		}
		fmt.Fprint(b, "\n")
		if len(doc.Options) > 0 {
			fmt.Fprintf(b, "\nOptions\n")
			for _, opt := range doc.Options {
				fmt.Fprintf(b, "    %s", opt)
			}
			fmt.Fprint(b, "\n")
		}
	}

	return b.String()
}

func (doc *Documentation) String() string {
	return doc.Format(true)
}

func newVersionFlag() flag.Getter {
	tags := build.Default.ReleaseTags
	v := tags[len(tags)-1][2:]
	version := new(VersionFlag)
	if err := version.Set(v); err != nil {
		panic(fmt.Sprintf("internal error: %s", err))
	}
	return version
}

type VersionFlag int

func (v *VersionFlag) String() string {
	return fmt.Sprintf("1.%d", *v)
}

var goVersionRE = regexp.MustCompile(`^(?:go)?1.(\d+).*$`)

// ParseGoVersion parses Go versions of the form 1.M, 1.M.N, or 1.M.NrcR, with an optional "go" prefix. It assumes that
// versions have already been verified and are valid.
func ParseGoVersion(s string) (int, bool) {
	m := goVersionRE.FindStringSubmatch(s)
	if m == nil {
		return 0, false
	}
	n, err := strconv.Atoi(m[1])
	if err != nil {
		return 0, false
	}
	return n, true
}

func (v *VersionFlag) Set(s string) error {
	n, ok := ParseGoVersion(s)
	if !ok {
		return fmt.Errorf("invalid Go version: %q", s)
	}
	*v = VersionFlag(n)
	return nil
}

func (v *VersionFlag) Get() interface{} {
	return int(*v)
}

// ExhaustiveTypeSwitch panics when called. It can be used to ensure
// that type switches are exhaustive.
func ExhaustiveTypeSwitch(v interface{}) {
	panic(fmt.Sprintf("internal error: unhandled case %T", v))
}

// A directive is a comment of the form '//lint:<command>
// [arguments...]'. It represents instructions to the static analysis
// tool.
type Directive struct {
	Command   string
	Arguments []string
	Directive *ast.Comment
	Node      ast.Node
}

func parseDirective(s string) (cmd string, args []string) {
	if !strings.HasPrefix(s, "//lint:") {
		return "", nil
	}
	s = strings.TrimPrefix(s, "//lint:")
	fields := strings.Split(s, " ")
	return fields[0], fields[1:]
}

// ParseDirectives extracts all directives from a list of Go files.
func ParseDirectives(files []*ast.File, fset *token.FileSet) []Directive {
	var dirs []Directive
	for _, f := range files {
		// OPT(dh): in our old code, we skip all the comment map work if we
		// couldn't find any directives, benchmark if that's actually
		// worth doing
		cm := ast.NewCommentMap(fset, f, f.Comments)
		for node, cgs := range cm {
			for _, cg := range cgs {
				for _, c := range cg.List {
					if !strings.HasPrefix(c.Text, "//lint:") {
						continue
					}
					cmd, args := parseDirective(c.Text)
					d := Directive{
						Command:   cmd,
						Arguments: args,
						Directive: c,
						Node:      node,
					}
					dirs = append(dirs, d)
				}
			}
		}
	}
	return dirs
}
