package shared

import (
	"fmt"
	"os"
	"runtime"
	"strings"
)

type Error struct {
	message     string
	loc         Location
	isWarning   bool
	warningKind WarningKind
	notes       []DiagnosticNote
}

type DiagnosticNote struct {
	message string
	loc     Location
}

type WarningKind string

const (
	WarningUnusedVariable  WarningKind = "unused-variable"
	WarningUnusedParameter WarningKind = "unused-parameter"
)

func NewError(loc Location, message string, a ...any) Error {
	return Error{
		message:   fmt.Sprintf(message, a...),
		loc:       loc,
		isWarning: false,
	}
}

func NewWarning(kind WarningKind, loc Location, message string, a ...any) Error {
	return Error{
		message:     fmt.Sprintf(message, a...),
		loc:         loc,
		isWarning:   true,
		warningKind: kind,
	}
}

func (err Error) WarningKind() WarningKind { return err.warningKind }

func (err Error) AsError() Error {
	err.isWarning = false
	return err
}

// WithNote adds a source-located explanation to a primary diagnostic.
func (err Error) WithNote(loc Location, message string, a ...any) Error {
	err.notes = append(err.notes, DiagnosticNote{loc: loc, message: fmt.Sprintf(message, a...)})
	return err
}

func (err Error) Error() string {
	source := err.loc.SourceText
	if source == "" {
		f, e := os.ReadFile(err.loc.FilePath)
		if e == nil {
			source = string(f)
		}
	}
	if source == "" {
		if err.loc.FilePath != "" && err.loc.LC.Line > 0 {
			kind := "Error"
			if err.isWarning {
				kind = "Warning"
			}
			return fmt.Sprintf("%s: %s:%d:%d\n\n%s", kind, err.loc.FilePath,
				err.loc.LC.Line, err.loc.LC.Col, err.message)
		}
		return err.message
	}

	lines := strings.Split(source, "\n")
	startLineIdx := err.loc.LC.Line - 1
	if startLineIdx < 0 || startLineIdx >= len(lines) {
		return err.message
	}
	endLC := err.loc.EndLC
	if endLC.Line <= 0 || endLC.Col <= 0 ||
		endLC.Line < err.loc.LC.Line ||
		(endLC.Line == err.loc.LC.Line && endLC.Col <= err.loc.LC.Col) {
		endLC = LineCol{Line: err.loc.LC.Line, Col: err.loc.LC.Col + 1}
	}
	endLineIdx := min(len(lines)-1, endLC.Line-1)

	start := max(0, startLineIdx-2)
	end := min(len(lines)-1, endLineIdx+2)

	var b strings.Builder
	if err.isWarning {
		b.WriteString("Warning: ")
	} else {
		b.WriteString("Error: ")
	}
	fmt.Fprintf(&b, "%s:%d:%d\n", err.loc.FilePath, err.loc.LC.Line, err.loc.LC.Col)

	red := "\x1b[31m"
	yellow := "\x1b[33m"
	reset := "\x1b[0m"
	if runtime.GOOS == "windows" || os.Getenv("NO_COLOR") != "" {
		red = ""
		yellow = ""
		reset = ""
	}

	var color string
	if err.isWarning {
		color = yellow
	} else {
		color = red
	}

	for i := start; i <= end; i++ {
		ln := i + 1
		if i < startLineIdx || i > endLineIdx {
			fmt.Fprintf(&b, "%4d | %s\n", ln, lines[i])
			continue
		}

		line := []rune(lines[i])
		highlightStart := 0
		if i == startLineIdx {
			highlightStart = max(0, err.loc.LC.Col-1)
		}
		highlightEnd := len(line)
		if i == endLineIdx {
			highlightEnd = max(0, endLC.Col-1)
		}
		highlightStart = min(highlightStart, len(line))
		highlightEnd = min(max(highlightEnd, highlightStart), len(line))
		if highlightEnd == highlightStart && highlightStart < len(line) {
			highlightEnd++
		}
		fmt.Fprintf(&b, "%s%4d |%s %s%s%s%s\n", color, ln, reset,
			string(line[:highlightStart]), color, string(line[highlightStart:highlightEnd]), reset+string(line[highlightEnd:]))
		if color == "" {
			markerWidth := max(1, highlightEnd-highlightStart)
			fmt.Fprintf(&b, "     | %s%s\n", strings.Repeat(" ", highlightStart), strings.Repeat("^", markerWidth))
		}
	}

	fmt.Fprintf(&b, "\n%s", err.message)
	for _, note := range err.notes {
		formatted := NewWarning("", note.loc, note.message).Error()
		formatted = strings.Replace(formatted, "Warning: ", "Note: ", 1)
		b.WriteString("\n\n")
		b.WriteString(formatted)
	}
	return b.String()
}

type LineCol struct {
	Line int
	Col  int
}

type Location struct {
	LC         LineCol
	EndLC      LineCol
	Offset     int
	EndOffset  int
	FilePath   string
	SourceText string
}

func (l Location) WithEnd(end Location) Location {
	l.EndLC = end.EndLC
	l.EndOffset = end.EndOffset
	if l.EndLC.Line == 0 {
		l.EndLC = end.LC
		l.EndOffset = end.Offset
	}
	return l
}

func (l Location) String() string {
	return fmt.Sprintf("%s:%d:%d", l.FilePath, l.LC.Line, l.LC.Col)
}

type Pair[T1 any, T2 any] struct {
	L T1
	R T2
}