package revivelib

import (
	"io/ioutil"
	"log"
	"strings"

	"github.com/mgechev/dots"
	"github.com/mgechev/revive/config"
	"github.com/mgechev/revive/lint"
	"github.com/mgechev/revive/logging"
	"github.com/pkg/errors"
)

// Revive is responsible for running linters and formatters
// and returning a set of results.
type Revive struct {
	config       *lint.Config
	lintingRules []lint.Rule
	logger       *log.Logger
	maxOpenFiles int
}

// New creates a new instance of Revive lint runner.
func New(
	conf *lint.Config,
	setExitStatus bool,
	maxOpenFiles int,
	extraRules ...ExtraRule,
) (*Revive, error) {
	logger, err := logging.GetLogger()
	if err != nil {
		return nil, errors.Wrap(err, "initializing revive - getting logger")
	}

	if setExitStatus {
		conf.ErrorCode = 1
		conf.WarningCode = 1
	}

	extraRuleInstances := make([]lint.Rule, len(extraRules))
	for i, extraRule := range extraRules {
		extraRuleInstances[i] = extraRule.Rule

		ruleName := extraRule.Rule.Name()

		_, isRuleAlreadyConfigured := conf.Rules[ruleName]
		if !isRuleAlreadyConfigured {
			conf.Rules[ruleName] = extraRule.DefaultConfig
		}
	}

	lintingRules, err := config.GetLintingRules(conf, extraRuleInstances)
	if err != nil {
		return nil, errors.Wrap(err, "initializing revive - gettint lint rules")
	}

	logger.Println("Config loaded")

	return &Revive{
		logger:       logger,
		config:       conf,
		lintingRules: lintingRules,
		maxOpenFiles: maxOpenFiles,
	}, nil
}

// Lint the included patterns, skipping excluded ones
func (r *Revive) Lint(patterns ...*LintPattern) (<-chan lint.Failure, error) {
	includePatterns := []string{}
	excludePatterns := []string{}

	for _, lintpkg := range patterns {
		if lintpkg.IsExclude() {
			excludePatterns = append(excludePatterns, lintpkg.GetPattern())
		} else {
			includePatterns = append(includePatterns, lintpkg.GetPattern())
		}
	}

	if len(excludePatterns) == 0 { // if no excludes were set
		excludePatterns = r.config.Exclude // use those from the configuration
	}

	packages, err := getPackages(includePatterns, excludePatterns)
	if err != nil {
		return nil, errors.Wrap(err, "linting - getting packages")
	}

	revive := lint.New(func(file string) ([]byte, error) {
		contents, err := ioutil.ReadFile(file)

		if err != nil {
			return nil, errors.Wrap(err, "reading file "+file)
		}

		return contents, nil
	}, r.maxOpenFiles)

	failures, err := revive.Lint(packages, r.lintingRules, *r.config)
	if err != nil {
		return nil, errors.Wrap(err, "linting - retrieving failures channel")
	}

	return failures, nil
}

// Format gets the output for a given failures channel from Lint.
func (r *Revive) Format(
	formatterName string,
	failuresChan <-chan lint.Failure,
) (string, int, error) {
	conf := r.config
	formatChan := make(chan lint.Failure)
	exitChan := make(chan bool)

	formatter, err := config.GetFormatter(formatterName)
	if err != nil {
		return "", 0, errors.Wrap(err, "formatting - getting formatter")
	}

	var (
		output    string
		formatErr error
	)

	go func() {
		output, formatErr = formatter.Format(formatChan, *conf)

		exitChan <- true
	}()

	exitCode := 0

	for failure := range failuresChan {
		if failure.Confidence < conf.Confidence {
			continue
		}

		if exitCode == 0 {
			exitCode = conf.WarningCode
		}

		if c, ok := conf.Rules[failure.RuleName]; ok && c.Severity == lint.SeverityError {
			exitCode = conf.ErrorCode
		}

		if c, ok := conf.Directives[failure.RuleName]; ok && c.Severity == lint.SeverityError {
			exitCode = conf.ErrorCode
		}

		formatChan <- failure
	}

	close(formatChan)
	<-exitChan

	if formatErr != nil {
		return "", exitCode, errors.Wrap(err, "formatting")
	}

	return output, exitCode, nil
}

func getPackages(includePatterns []string, excludePatterns ArrayFlags) ([][]string, error) {
	globs := normalizeSplit(includePatterns)
	if len(globs) == 0 {
		globs = append(globs, ".")
	}

	packages, err := dots.ResolvePackages(globs, normalizeSplit(excludePatterns))
	if err != nil {
		return nil, errors.Wrap(err, "getting packages - resolving packages in dots")
	}

	return packages, nil
}

func normalizeSplit(strs []string) []string {
	res := []string{}

	for _, s := range strs {
		t := strings.Trim(s, " \t")
		if len(t) > 0 {
			res = append(res, t)
		}
	}

	return res
}

// ArrayFlags type for string list.
type ArrayFlags []string

func (i *ArrayFlags) String() string {
	return strings.Join([]string(*i), " ")
}

// Set value for array flags.
func (i *ArrayFlags) Set(value string) error {
	*i = append(*i, value)

	return nil
}
