package main

import (
	"errors"
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"strings"
	"text/scanner"

	"github.com/fiskerinc/cloud-services/pkg/can-go/internal/generate"
	"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc"
	"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis"
	"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/passes/definitiontypeorder"
	"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/passes/intervals"
	"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/passes/lineendings"
	"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/passes/messagenames"
	"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/passes/multiplexedsignals"
	"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/passes/newsymbols"
	"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/passes/nodereferences"
	"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/passes/noreservedsignals"
	"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/passes/requireddefinitions"
	"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/passes/signalbounds"
	"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/passes/signalnames"
	"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/passes/singletondefinitions"
	"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/passes/siunits"
	"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/passes/uniquenodenames"
	"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/passes/uniquesignalnames"
	"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/passes/unitsuffixes"
	"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/passes/valuedescriptions"
	"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/passes/version"
	"github.com/fatih/color"
	"gopkg.in/alecthomas/kingpin.v2"
)

func main() {
	app := kingpin.New("cantool", "CAN tool for Go programmers")
	generateCommand(app)
	lintCommand(app)
	kingpin.MustParse(app.Parse(os.Args[1:]))
}

func generateCommand(app *kingpin.Application) {
	command := app.Command("generate", "generate CAN messages")
	inputDir := command.
		Arg("input-dir", "input directory").
		Required().
		ExistingDir()
	outputDir := command.
		Arg("output-dir", "output directory").
		Required().
		String()
	command.Action(func(c *kingpin.ParseContext) error {
		return filepath.Walk(*inputDir, func(p string, i os.FileInfo, err error) error {
			if err != nil {
				return err
			}
			if i.IsDir() || filepath.Ext(p) != ".dbc" {
				return nil
			}
			relPath, err := filepath.Rel(*inputDir, p)
			if err != nil {
				return err
			}
			outputFile := relPath + ".go"
			outputPath := filepath.Join(*outputDir, outputFile)
			return genGo(p, outputPath)
		})
	})
}

func lintCommand(app *kingpin.Application) {
	command := app.Command("lint", "lint DBC files")
	fileOrDir := command.
		Arg("file-or-dir", "DBC file or directory").
		Required().
		ExistingFileOrDir()
	command.Action(func(context *kingpin.ParseContext) error {
		filesToLint, err := resolveFileOrDirectory(*fileOrDir)
		if err != nil {
			return err
		}
		var hasFailed bool
		for _, lintFile := range filesToLint {
			f, err := os.Open(lintFile)
			if err != nil {
				return err
			}
			source, err := ioutil.ReadAll(f)
			if err != nil {
				return err
			}
			p := dbc.NewParser(f.Name(), source)
			if err := p.Parse(); err != nil {
				printError(source, err.Position(), err.Reason(), "parse")
				continue
			}
			for _, a := range analyzers() {
				pass := &analysis.Pass{
					Analyzer: a,
					File:     p.File(),
				}
				if err := a.Run(pass); err != nil {
					return err
				}
				hasFailed = hasFailed || len(pass.Diagnostics) > 0
				for _, d := range pass.Diagnostics {
					printError(source, d.Pos, d.Message, a.Name)
				}
			}
		}
		if hasFailed {
			return errors.New("one or more lint errors")
		}
		return nil
	})
}

func analyzers() []*analysis.Analyzer {
	return []*analysis.Analyzer{
		// TODO: Re-evaluate if we want boolprefix.Analyzer(), since it creates a lot of churn in vendor schemas
		definitiontypeorder.Analyzer(),
		intervals.Analyzer(),
		lineendings.Analyzer(),
		messagenames.Analyzer(),
		multiplexedsignals.Analyzer(),
		newsymbols.Analyzer(),
		nodereferences.Analyzer(),
		noreservedsignals.Analyzer(),
		requireddefinitions.Analyzer(),
		signalbounds.Analyzer(),
		signalnames.Analyzer(),
		singletondefinitions.Analyzer(),
		siunits.Analyzer(),
		uniquenodenames.Analyzer(),
		uniquesignalnames.Analyzer(),
		unitsuffixes.Analyzer(),
		valuedescriptions.Analyzer(),
		version.Analyzer(),
	}
}

func genGo(inputFile, outputFile string) error {
	if err := os.MkdirAll(filepath.Dir(outputFile), 0o755); err != nil {
		return err
	}
	input, err := ioutil.ReadFile(inputFile)
	if err != nil {
		return err
	}
	result, err := generate.Compile(inputFile, input)
	if err != nil {
		return err
	}
	for _, warning := range result.Warnings {
		return warning
	}
	output, err := generate.Database(result.Hash, result.Database)
	if err != nil {
		return err
	}
	if err := ioutil.WriteFile(outputFile, output, 0o600); err != nil {
		return err
	}
	fmt.Println("hash:", result.Hash)
	fmt.Println("version:", result.Database.Version)
	fmt.Println("wrote:", outputFile)
	return nil
}

func resolveFileOrDirectory(fileOrDirectory string) ([]string, error) {
	fileInfo, err := os.Stat(fileOrDirectory)
	if err != nil {
		return nil, err
	}
	if !fileInfo.IsDir() {
		return []string{fileOrDirectory}, nil
	}
	var files []string
	if err := filepath.Walk(fileOrDirectory, func(path string, info os.FileInfo, err error) error {
		if !info.IsDir() && filepath.Ext(path) == ".dbc" {
			files = append(files, path)
		}
		return nil
	}); err != nil {
		return nil, err
	}
	return files, nil
}

func printError(source []byte, pos scanner.Position, msg, name string) {
	fmt.Printf("\n%s: %s (%s)\n", pos, color.RedString("%s", msg), name)
	fmt.Printf("%s\n", getSourceLine(source, pos))
	fmt.Printf("%s\n", caretAtPosition(pos))
}

func getSourceLine(source []byte, pos scanner.Position) []byte {
	lineStart := pos.Offset
	for lineStart > 0 && source[lineStart-1] != '\n' {
		lineStart--
	}
	lineEnd := pos.Offset
	for lineEnd < len(source) && source[lineEnd] != '\n' {
		lineEnd++
	}
	return source[lineStart:lineEnd]
}

func caretAtPosition(pos scanner.Position) string {
	return strings.Repeat(" ", pos.Column-1) + color.YellowString("^")
}
