package main

import (
	"bytes"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"reflect"
	"regexp"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/google/shlex"
	kingpin "gopkg.in/alecthomas/kingpin.v3-unstable"
)

type Vars map[string]string

func (v Vars) Copy() Vars {
	out := Vars{}
	for k, v := range v {
		out[k] = v
	}
	return out
}

func (v Vars) Replace(s string) string {
	for k, v := range v {
		prefix := regexp.MustCompile(fmt.Sprintf("{%s=([^}]*)}", k))
		if v != "" {
			s = prefix.ReplaceAllString(s, "$1")
		} else {
			s = prefix.ReplaceAllString(s, "")
		}
		s = strings.Replace(s, fmt.Sprintf("{%s}", k), v, -1)
	}
	return s
}

type linterState struct {
	*Linter
	issues   chan *Issue
	vars     Vars
	exclude  *regexp.Regexp
	include  *regexp.Regexp
	deadline <-chan time.Time
}

func (l *linterState) Partitions(paths []string) ([][]string, error) {
	cmdArgs, err := parseCommand(l.command())
	if err != nil {
		return nil, err
	}
	parts, err := l.Linter.PartitionStrategy(cmdArgs, paths)
	if err != nil {
		return nil, err
	}
	return parts, nil
}

func (l *linterState) command() string {
	return l.vars.Replace(l.Command)
}

func runLinters(linters map[string]*Linter, paths []string, concurrency int, exclude, include *regexp.Regexp) (chan *Issue, chan error) {
	errch := make(chan error, len(linters))
	concurrencych := make(chan bool, concurrency)
	incomingIssues := make(chan *Issue, 1000000)

	directiveParser := newDirectiveParser()
	if config.WarnUnmatchedDirective {
		directiveParser.LoadFiles(paths)
	}

	processedIssues := maybeSortIssues(filterIssuesViaDirectives(
		directiveParser, maybeAggregateIssues(incomingIssues)))

	vars := Vars{
		"duplthreshold":    fmt.Sprintf("%d", config.DuplThreshold),
		"mincyclo":         fmt.Sprintf("%d", config.Cyclo),
		"maxlinelength":    fmt.Sprintf("%d", config.LineLength),
		"min_confidence":   fmt.Sprintf("%f", config.MinConfidence),
		"min_occurrences":  fmt.Sprintf("%d", config.MinOccurrences),
		"min_const_length": fmt.Sprintf("%d", config.MinConstLength),
		"tests":            "",
		"not_tests":        "true",
	}
	if config.Test {
		vars["tests"] = "true"
		vars["not_tests"] = ""
	}

	wg := &sync.WaitGroup{}
	id := 1
	for _, linter := range linters {
		deadline := time.After(config.Deadline.Duration())
		state := &linterState{
			Linter:   linter,
			issues:   incomingIssues,
			vars:     vars,
			exclude:  exclude,
			include:  include,
			deadline: deadline,
		}

		partitions, err := state.Partitions(paths)
		if err != nil {
			errch <- err
			continue
		}
		for _, args := range partitions {
			wg.Add(1)
			concurrencych <- true
			// Call the goroutine with a copy of the args array so that the
			// contents of the array are not modified by the next iteration of
			// the above for loop
			go func(id int, args []string) {
				err := executeLinter(id, state, args)
				if err != nil {
					errch <- err
				}
				<-concurrencych
				wg.Done()
			}(id, args)
			id++
		}
	}

	go func() {
		wg.Wait()
		close(incomingIssues)
		close(errch)
	}()
	return processedIssues, errch
}

func executeLinter(id int, state *linterState, args []string) error {
	if len(args) == 0 {
		return fmt.Errorf("missing linter command")
	}

	start := time.Now()
	dbg := namespacedDebug(fmt.Sprintf("[%s.%d]: ", state.Name, id))
	dbg("executing %s", strings.Join(args, " "))
	buf := bytes.NewBuffer(nil)
	command := args[0]
	cmd := exec.Command(command, args[1:]...) // nolint: gas
	cmd.Stdout = buf
	cmd.Stderr = buf
	err := cmd.Start()
	if err != nil {
		return fmt.Errorf("failed to execute linter %s: %s", command, err)
	}

	done := make(chan bool)
	go func() {
		err = cmd.Wait()
		done <- true
	}()

	// Wait for process to complete or deadline to expire.
	select {
	case <-done:

	case <-state.deadline:
		err = fmt.Errorf("deadline exceeded by linter %s (try increasing --deadline)",
			state.Name)
		kerr := cmd.Process.Kill()
		if kerr != nil {
			warning("failed to kill %s: %s", state.Name, kerr)
		}
		return err
	}

	if err != nil {
		dbg("warning: %s returned %s: %s", command, err, buf.String())
	}

	processOutput(dbg, state, buf.Bytes())
	elapsed := time.Since(start)
	dbg("%s linter took %s", state.Name, elapsed)
	return nil
}

func parseCommand(command string) ([]string, error) {
	args, err := shlex.Split(command)
	if err != nil {
		return nil, err
	}
	if len(args) == 0 {
		return nil, fmt.Errorf("invalid command %q", command)
	}
	exe, err := exec.LookPath(args[0])
	if err != nil {
		return nil, err
	}
	return append([]string{exe}, args[1:]...), nil
}

// nolint: gocyclo
func processOutput(dbg debugFunction, state *linterState, out []byte) {
	re := state.regex
	all := re.FindAllSubmatchIndex(out, -1)
	dbg("%s hits %d: %s", state.Name, len(all), state.Pattern)

	cwd, err := os.Getwd()
	if err != nil {
		warning("failed to get working directory %s", err)
	}

	// Create a local copy of vars so they can be modified by the linter output
	vars := state.vars.Copy()

	for _, indices := range all {
		group := [][]byte{}
		for i := 0; i < len(indices); i += 2 {
			var fragment []byte
			if indices[i] != -1 {
				fragment = out[indices[i]:indices[i+1]]
			}
			group = append(group, fragment)
		}

		issue, err := NewIssue(state.Linter.Name, config.formatTemplate)
		kingpin.FatalIfError(err, "Invalid output format")

		for i, name := range re.SubexpNames() {
			if group[i] == nil {
				continue
			}
			part := string(group[i])
			if name != "" {
				vars[name] = part
			}
			switch name {
			case "path":
				issue.Path = relativePath(cwd, part)

			case "line":
				n, err := strconv.ParseInt(part, 10, 32)
				kingpin.FatalIfError(err, "line matched invalid integer")
				issue.Line = int(n)

			case "col":
				n, err := strconv.ParseInt(part, 10, 32)
				kingpin.FatalIfError(err, "col matched invalid integer")
				issue.Col = int(n)

			case "message":
				issue.Message = part

			case "":
			}
		}
		// TODO: set messageOveride and severity on the Linter instead of reading
		// them directly from the static config
		if m, ok := config.MessageOverride[state.Name]; ok {
			issue.Message = vars.Replace(m)
		}
		if sev, ok := config.Severity[state.Name]; ok {
			issue.Severity = Severity(sev)
		}
		if state.exclude != nil && state.exclude.MatchString(issue.String()) {
			continue
		}
		if state.include != nil && !state.include.MatchString(issue.String()) {
			continue
		}
		state.issues <- issue
	}
}

func relativePath(root, path string) string {
	fallback := path
	root = resolvePath(root)
	path = resolvePath(path)
	var err error
	path, err = filepath.Rel(root, path)
	if err != nil {
		warning("failed to make %s a relative path: %s", fallback, err)
		return fallback
	}
	return path
}

func resolvePath(path string) string {
	var err error
	fallback := path
	if !filepath.IsAbs(path) {
		path, err = filepath.Abs(path)
		if err != nil {
			warning("failed to make %s an absolute path: %s", fallback, err)
			return fallback
		}
	}
	path, err = filepath.EvalSymlinks(path)
	if err != nil {
		warning("failed to resolve symlinks in %s: %s", fallback, err)
		return fallback
	}
	return path
}

func maybeSortIssues(issues chan *Issue) chan *Issue {
	if reflect.DeepEqual([]string{"none"}, config.Sort) {
		return issues
	}
	return SortIssueChan(issues, config.Sort)
}

func maybeAggregateIssues(issues chan *Issue) chan *Issue {
	if !config.Aggregate {
		return issues
	}
	return AggregateIssueChan(issues)
}
