package main

import (
	"fmt"
	"os"
	"os/signal"
	"path"
	"runtime"
	"strings"
	"sync"
	"sync/atomic"
	"time"

	"github.com/JetBrains/qodana-cli/internal/platform"
	"github.com/JetBrains/qodana-cli/internal/platform/strutil"
	"github.com/JetBrains/qodana-cli/internal/platform/thirdpartyscan"
	"github.com/JetBrains/qodana-cli/internal/platform/utils"
	"github.com/briandowns/spinner"
	log "github.com/sirupsen/logrus"
)

const spinnerIndex = 34
const spinnerInterval = 100 * time.Millisecond

// runClangTidyUnderProgress runs clang-tidy for each file in filesAndCompilers and shows a progress bar.
func runClangTidyUnderProgress(c thirdpartyscan.Context, filesAndCompilers []FileWithHeaders, checks string) {
	spin := initializeSpinner()
	stdoutChannel, stderrChannel := createFileLoggers(c.LogDir())
	worker(c, filesAndCompilers, checks, spin, stdoutChannel, stderrChannel)
}

func initializeSpinner() *spinner.Spinner {
	spin := spinner.New(spinner.CharSets[9], spinnerInterval, spinner.WithWriter(os.Stderr))
	spin.UpdateCharSet(spinner.CharSets[spinnerIndex])
	return spin
}

func createFileLoggers(logDir string) (chan string, chan string) {
	stdout := make(chan string)
	stderr := make(chan string)

	go logToFile(path.Join(logDir, "clang-out.txt"), stdout)
	go logToFile(path.Join(logDir, "clang-err.txt"), stderr)

	return stdout, stderr
}

func logToFile(fileName string, logChannel chan string) {
	for logItem := range logChannel {
		if err := utils.AppendToFile(fileName, logItem); err != nil {
			log.Error(err)
		}
	}
}

func worker(
	c thirdpartyscan.Context,
	filesAndCompilers []FileWithHeaders,
	checks string,
	spin *spinner.Spinner,
	stdoutChannel, stderrChannel chan string,
) {
	spin.Start()
	spin.Suffix = fmt.Sprintf(" %d/%d", 0, len(filesAndCompilers))

	defer close(stdoutChannel)
	defer close(stderrChannel)

	var wg sync.WaitGroup
	progressCounter := int32(0)
	sem := make(chan bool, runtime.NumCPU())
	quit := make(chan os.Signal, 1)
	finished := make(chan bool)
	signal.Notify(quit, os.Interrupt)

	go func() {
		for counter, fileWithHeader := range filesAndCompilers {
			select {
			case <-quit:
				fmt.Println("Interrupt signal received. Exiting function.")
				spin.Stop()
				finished <- true
				return
			default:
				wg.Add(1)
				go func(counter int, input FileWithHeaders) {
					defer wg.Done()
					sem <- true
					spin.Suffix = fmt.Sprintf(" %d/%d", atomic.AddInt32(&progressCounter, 1), len(filesAndCompilers))
					spin.Restart()
					defer func() { <-sem }()

					err := runClangTidy(
						counter,
						input,
						checks,
						c,
						platform.GetTmpResultsDir(c.ResultsDir()),
						stderrChannel,
						stdoutChannel,
					)
					if err != nil {
						log.Errorf("Error running clang-tidy: %s", err)
					}
				}(counter, fileWithHeader)
			}
		}
		wg.Wait()
		spin.Stop()
		finished <- true
	}()
	select {
	case <-quit:
		return
	case <-finished:
		return
	}
}

// runClangTidy runs clang-tidy for a single file.
func runClangTidy(
	counter int,
	input FileWithHeaders,
	checks string,
	c thirdpartyscan.Context,
	tmpResultsDir string,
	stderrChannel chan string,
	stdoutChannel chan string,
) error {
	args := []string{
		strutil.QuoteIfSpace(c.ClangPath()),
		checks,
		"-p",
		strutil.QuoteIfSpace(c.ClangCompileCommands()),
		"--export-sarif",
		strutil.QuoteIfSpace(path.Join(tmpResultsDir, fmt.Sprintf("%d.sarif.json", counter))),
	}
	args = append(args, input.Headers...)
	args = append(args, input.File)
	args = append(args, "--quiet")
	args = append(args, strings.Split(c.ClangArgs(), " ")...)
	stdout, stderr, _, err := utils.RunCmdRedirectOutput(
		strutil.QuoteIfSpace(c.ProjectDir()),
		args...,
	)
	if stderr != "" {
		log.Debug(stderr)
		stderrChannel <- fmt.Sprintf("File: %s\n%s\n", input.File, stderr)
	}
	if stdout != "" {
		log.Debug(stdout)
		stdoutChannel <- fmt.Sprintf("File: %s\n%s\n%s\n", input.File, stdout, stderr)
	}
	return err
}
