// 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 reviver scans the code looking for TODOs and pass them to registered
// Buggers to ensure TODOs point to active issues.
package reviver

import (
	"bufio"
	"fmt"
	"os"
	"path/filepath"
	"regexp"
	"sync"
)

// regexTodo matches a TODO or FIXME comment.
var regexTodo = regexp.MustCompile(`(\/\/|#)\s*(TODO|FIXME)\(([a-zA-Z0-9.\/]+)\):\s*(.+)`)

// Bugger interface is called for every TODO found in the code. If it can handle
// the TODO, it must return true. If it returns false, the next Bugger is
// called. If no Bugger handles the TODO, it's dropped on the floor.
type Bugger interface {
	Activate(todo *Todo) (bool, error)
}

// Location saves the location where the TODO was found.
type Location struct {
	Comment string
	File    string
	Line    uint
}

// Todo represents a unique TODO. There can be several TODOs pointing to the
// same issue in the code. They are all grouped together.
type Todo struct {
	Issue     string
	Locations []Location
}

// Reviver scans the given paths for TODOs and calls Buggers to handle them.
type Reviver struct {
	paths   []string
	buggers []Bugger

	mu    sync.Mutex
	todos map[string]*Todo
	errs  []error
}

// New create a new Reviver.
func New(paths []string, buggers []Bugger) *Reviver {
	return &Reviver{
		paths:   paths,
		buggers: buggers,
		todos:   map[string]*Todo{},
	}
}

// Run runs. It returns all errors found during processing, it doesn't stop
// on errors.
func (r *Reviver) Run() []error {
	// Process each directory in parallel.
	wg := sync.WaitGroup{}
	for _, path := range r.paths {
		wg.Add(1)
		go func(path string) {
			defer wg.Done()
			r.processPath(path, &wg)
		}(path)
	}

	wg.Wait()

	r.mu.Lock()
	defer r.mu.Unlock()

	fmt.Printf("Processing %d TODOs (%d errors)...\n", len(r.todos), len(r.errs))
	dropped := 0
	for _, todo := range r.todos {
		ok, err := r.processTodo(todo)
		if err != nil {
			r.errs = append(r.errs, err)
		}
		if !ok {
			dropped++
		}
	}
	fmt.Printf("Processed %d TODOs, %d were skipped (%d errors)\n", len(r.todos)-dropped, dropped, len(r.errs))

	return r.errs
}

func (r *Reviver) processPath(path string, wg *sync.WaitGroup) {
	fmt.Printf("Processing dir %q\n", path)
	fis, err := os.ReadDir(path)
	if err != nil {
		r.addErr(fmt.Errorf("error processing dir %q: %v", path, err))
		return
	}

	for _, fi := range fis {
		childPath := filepath.Join(path, fi.Name())
		switch {
		case fi.IsDir():
			wg.Add(1)
			go func() {
				defer wg.Done()
				r.processPath(childPath, wg)
			}()

		case fi.Type().IsRegular():
			file, err := os.Open(childPath)
			if err != nil {
				r.addErr(err)
				continue
			}

			scanner := bufio.NewScanner(file)
			lineno := uint(0)
			for scanner.Scan() {
				lineno++
				line := scanner.Text()
				if todo := r.processLine(line, childPath, lineno); todo != nil {
					r.addTodo(todo)
				}
			}
		}
	}
}

func (r *Reviver) processLine(line, path string, lineno uint) *Todo {
	matches := regexTodo.FindStringSubmatch(line)
	if matches == nil {
		return nil
	}
	if len(matches) != 5 {
		panic(fmt.Sprintf("regex returned wrong matches for %q: %v", line, matches))
	}
	return &Todo{
		Issue: matches[3],
		Locations: []Location{
			{
				File:    path,
				Line:    lineno,
				Comment: matches[4],
			},
		},
	}
}

func (r *Reviver) addTodo(newTodo *Todo) {
	r.mu.Lock()
	defer r.mu.Unlock()

	if todo := r.todos[newTodo.Issue]; todo == nil {
		r.todos[newTodo.Issue] = newTodo
	} else {
		todo.Locations = append(todo.Locations, newTodo.Locations...)
	}
}

func (r *Reviver) addErr(err error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	r.errs = append(r.errs, err)
}

func (r *Reviver) processTodo(todo *Todo) (bool, error) {
	for _, bugger := range r.buggers {
		ok, err := bugger.Activate(todo)
		if err != nil {
			return false, err
		}
		if ok {
			return true, nil
		}
	}
	return false, nil
}
