// Copyright 2019 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package nogo

import (
	"fmt"
	"regexp"
)

// GroupName is a named group.
type GroupName string

// AnalyzerName is a named analyzer.
type AnalyzerName string

// Group represents a named collection of files.
type Group struct {
	// Name is the short name for the group.
	Name GroupName `yaml:"name"`

	// Regex matches all full paths in the group.
	Regex string         `yaml:"regex"`
	regex *regexp.Regexp `yaml:"-"`

	// Default determines the default group behavior.
	//
	// If Default is true, all Analyzers are enabled for this
	// group. Otherwise, Analyzers must be individually enabled
	// by specifying a (possible empty) ItemConfig for the group
	// in the AnalyzerConfig.
	Default bool `yaml:"default"`
}

func (g *Group) compile() error {
	r, err := regexp.Compile(g.Regex)
	if err != nil {
		return err
	}
	g.regex = r
	return nil
}

// ItemConfig is an (Analyzer,Group) configuration.
type ItemConfig struct {
	// Exclude are analyzer exclusions.
	//
	// Exclude is a list of regular expressions. If the corresponding
	// Analyzer emits a Finding for which Finding.Position.String()
	// matches a regular expression in Exclude, the finding will not
	// be reported.
	Exclude []string         `yaml:"exclude,omitempty"`
	exclude []*regexp.Regexp `yaml:"-"`

	// Suppress are analyzer suppressions.
	//
	// Suppress is a list of regular expressions. If the corresponding
	// Analyzer emits a Finding for which Finding.Message matches a regular
	// expression in Suppress, the finding will not be reported.
	Suppress []string         `yaml:"suppress,omitempty"`
	suppress []*regexp.Regexp `yaml:"-"`
}

func compileRegexps(ss []string, rs *[]*regexp.Regexp) error {
	*rs = make([]*regexp.Regexp, 0, len(ss))
	for _, s := range ss {
		r, err := regexp.Compile(s)
		if err != nil {
			return err
		}
		*rs = append(*rs, r)
	}
	return nil
}

func (i *ItemConfig) compile() error {
	if i == nil {
		// This may be nil if nothing is included in the
		// item configuration. That's fine, there's nothing
		// to compile and nothing to exclude & suppress.
		return nil
	}
	if err := compileRegexps(i.Exclude, &i.exclude); err != nil {
		return fmt.Errorf("in exclude: %w", err)
	}
	if err := compileRegexps(i.Suppress, &i.suppress); err != nil {
		return fmt.Errorf("in suppress: %w", err)
	}
	return nil
}

func (i *ItemConfig) merge(other *ItemConfig) {
	i.Exclude = append(i.Exclude, other.Exclude...)
	i.Suppress = append(i.Suppress, other.Suppress...)
}

func (i *ItemConfig) shouldReport(fullPos, msg string) bool {
	if i == nil {
		// See above.
		return true
	}
	for _, r := range i.exclude {
		if r.MatchString(fullPos) {
			return false
		}
	}
	for _, r := range i.suppress {
		if r.MatchString(msg) {
			return false
		}
	}
	return true
}

// AnalyzerConfig is the configuration for a single analyzers.
//
// This map is keyed by individual Group names, to allow for different
// configurations depending on what Group the file belongs to.
type AnalyzerConfig map[GroupName]*ItemConfig

func (a AnalyzerConfig) compile() error {
	for name, gc := range a {
		if err := gc.compile(); err != nil {
			return fmt.Errorf("invalid group %q: %v", name, err)
		}
	}
	return nil
}

func (a AnalyzerConfig) merge(other AnalyzerConfig) {
	// Merge all the groups.
	for name, gc := range other {
		old, ok := a[name]
		if !ok || old == nil {
			a[name] = gc // Not configured in a.
			continue
		}
		old.merge(gc)
	}
}

func (a AnalyzerConfig) shouldReport(groupConfig *Group, fullPos, msg string) bool {
	gc, ok := a[groupConfig.Name]
	if !ok {
		return groupConfig.Default
	}

	// Note that if a section appears for a particular group
	// for a particular analyzer, then it will now be enabled,
	// and the group default no longer applies.
	return gc.shouldReport(fullPos, msg)
}

// Config is a nogo configuration.
type Config struct {
	// Prefixes defines a set of regular expressions that
	// are standard "prefixes", so that files can be grouped
	// and specific rules applied to individual groups.
	Groups []Group `yaml:"groups"`

	// Global is the global analyzer config.
	Global AnalyzerConfig `yaml:"global"`

	// Analyzers are individual analyzer configurations. The
	// key for each analyzer is the name of the analyzer. The
	// value is either a boolean (enable/disable), or a map to
	// the groups above.
	Analyzers map[AnalyzerName]AnalyzerConfig `yaml:"analyzers"`
}

// Merge merges two configurations.
func (c *Config) Merge(other *Config) {
	// Merge all groups.
	for _, g := range other.Groups {
		// Is there a matching group? If yes, we just delete
		// it. This will preserve the order provided in the
		// overriding file, even if it differs.
		for i := 0; i < len(c.Groups); i++ {
			if g.Name == c.Groups[i].Name {
				copy(c.Groups[i:], c.Groups[i+1:])
				c.Groups = c.Groups[:len(c.Groups)-1]
				break
			}
		}
		c.Groups = append(c.Groups, g)
	}

	// Merge global configurations.
	c.Global.merge(other.Global)

	// Merge all analyzer configurations.
	for name, ac := range other.Analyzers {
		old, ok := c.Analyzers[name]
		if !ok {
			c.Analyzers[name] = ac // No analyzer in original config.
			continue
		}
		old.merge(ac)
	}
}

// Compile compiles a configuration to make it useable.
func (c *Config) Compile() error {
	for i := 0; i < len(c.Groups); i++ {
		if err := c.Groups[i].compile(); err != nil {
			return fmt.Errorf("invalid group %q: %w", c.Groups[i].Name, err)
		}
	}
	if err := c.Global.compile(); err != nil {
		return fmt.Errorf("invalid global: %w", err)
	}
	for name, ac := range c.Analyzers {
		if err := ac.compile(); err != nil {
			return fmt.Errorf("invalid analyzer %q: %w", name, err)
		}
	}
	return nil
}

// ShouldReport returns true iff the finding should match the Config.
func (c *Config) ShouldReport(finding Finding) bool {
	fullPos := finding.Position.String()

	// Find the matching group.
	var groupConfig *Group
	for i := 0; i < len(c.Groups); i++ {
		if c.Groups[i].regex.MatchString(fullPos) {
			groupConfig = &c.Groups[i]
			break
		}
	}

	// If there is no group matching this path, then
	// we default to accept the finding.
	if groupConfig == nil {
		return true
	}

	// Suppress via global rule?
	if !c.Global.shouldReport(groupConfig, fullPos, finding.Message) {
		return false
	}

	// Try the analyzer config.
	ac, ok := c.Analyzers[finding.Category]
	if !ok {
		return groupConfig.Default
	}
	return ac.shouldReport(groupConfig, fullPos, finding.Message)
}