/*
Copyright 2020 Google LLC

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

    https://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 utils

import (
	"encoding/json"
	"fmt"
	"github.com/bazelbuild/buildtools/build"
	"github.com/bazelbuild/buildtools/warn"
	"strings"
)

// Diagnostics contains diagnostic information returned by formatter and linter
type Diagnostics struct {
	Success bool               `json:"success"` // overall success (whether all files are formatted properly and have no warnings)
	Files   []*FileDiagnostics `json:"files"`   // diagnostics per file
}

// Format formats a Diagnostics object either as plain text or as json
func (d *Diagnostics) Format(format string, verbose bool) string {
	switch format {
	case "text", "":
		var output strings.Builder
		for _, f := range d.Files {
			for _, w := range f.Warnings {
				formatString := "%s:%d: %s: %s (%s)\n"
				if !w.Actionable {
					formatString = "%s:%d: %s: %s [%s]\n"
				}
				output.WriteString(fmt.Sprintf(formatString,
					f.Filename,
					w.Start.Line,
					w.Category,
					w.Message,
					w.URL))
			}
			if !f.Formatted {
				output.WriteString(fmt.Sprintf("%s # reformat\n", f.Filename))
			}
		}
		return output.String()
	case "json":
		var result []byte
		if verbose {
			result, _ = json.MarshalIndent(*d, "", "    ")
		} else {
			result, _ = json.Marshal(*d)
		}
		return string(result) + "\n"
	}
	return ""
}

// FileDiagnostics contains diagnostics information for a file
type FileDiagnostics struct {
	Filename  string     `json:"filename"`
	Formatted bool       `json:"formatted"`
	Valid     bool       `json:"valid"`
	Warnings  []*warning `json:"warnings"`
}

type warning struct {
	Start       position `json:"start"`
	End         position `json:"end"`
	Category    string   `json:"category"`
	Actionable  bool     `json:"actionable"`
	AutoFixable bool     `json:"autoFixable"`
	Message     string   `json:"message"`
	URL         string   `json:"url"`
}

type position struct {
	Line   int `json:"line"`
	Column int `json:"column"`
}

// NewDiagnostics returns a new Diagnostics object
func NewDiagnostics(fileDiagnostics ...*FileDiagnostics) *Diagnostics {
	diagnostics := &Diagnostics{
		Success: true,
		Files:   fileDiagnostics,
	}
	for _, file := range diagnostics.Files {
		if !file.Formatted || len(file.Warnings) > 0 {
			diagnostics.Success = false
			break
		}
	}
	return diagnostics
}

// NewFileDiagnostics returns a new FileDiagnostics object
func NewFileDiagnostics(filename string, warnings []*warn.Finding) *FileDiagnostics {
	fileDiagnostics := FileDiagnostics{
		Filename:  filename,
		Formatted: true,
		Valid:     true,
		Warnings:  []*warning{},
	}

	for _, w := range warnings {
		fileDiagnostics.Warnings = append(fileDiagnostics.Warnings, &warning{
			Start:       makePosition(w.Start),
			End:         makePosition(w.End),
			Category:    w.Category,
			Actionable:  w.Actionable,
			AutoFixable: w.AutoFixable,
			Message:     w.Message,
			URL:         w.URL,
		})
	}

	return &fileDiagnostics
}

// InvalidFileDiagnostics returns a new FileDiagnostics object for an invalid file
func InvalidFileDiagnostics(filename string) *FileDiagnostics {
	fileDiagnostics := &FileDiagnostics{
		Filename:  filename,
		Formatted: false,
		Valid:     false,
		Warnings:  []*warning{},
	}
	if filename == "" {
		fileDiagnostics.Filename = "<stdin>"
	}
	return fileDiagnostics
}

func makePosition(p build.Position) position {
	return position{
		Line:   p.Line,
		Column: p.LineRune,
	}
}
