package errors

import (
	"bytes"
	"fmt"
	"os"
	"path/filepath"

	"git.urbach.dev/cli/q/src/fs"
	"git.urbach.dev/cli/q/src/token"
)

// FileError is an error inside a file at a given line and column.
type FileError struct {
	err      error
	file     *fs.File
	stack    string
	position token.Position
}

// Error implements the error interface.
func (e *FileError) Error() string {
	return e.err.Error()
}

// File returns the file the error came from.
func (e *FileError) File() *fs.File {
	return e.file
}

// Line returns the line contents and the offset from the position.
func (e *FileError) Line() (string, int) {
	contents := e.file.Bytes
	start := bytes.LastIndexByte(contents[:e.position], '\n')

	if start == -1 {
		start = 0
	} else {
		start++
	}

	end := bytes.IndexByte(contents[e.position:], '\n')

	if end == -1 {
		end = len(contents)
	} else {
		end += int(e.position)
	}

	asciiSpace := [256]uint8{'\t': 1, '\n': 1, '\v': 1, '\f': 1, '\r': 1, ' ': 1}

	for asciiSpace[contents[start]] == 1 {
		start++
	}

	return string(contents[start:end]), int(e.position) - start
}

// LineColumn returns the line and column of the error.
func (e *FileError) LineColumn() (int, int) {
	line := 1
	column := 1
	lineStart := -1

	for _, t := range e.file.Tokens {
		if t.Position >= e.position {
			column = int(e.position) - lineStart
			break
		}

		if t.Kind == token.NewLine {
			lineStart = int(t.Position)
			line++
		}
	}

	return line, column
}

// Link returns a clickable link containing the path, line and column.
func (e *FileError) Link() string {
	path := e.Path()
	line, column := e.LineColumn()
	return fmt.Sprintf("%s:%d:%d", path, line, column)
}

// Path returns the relative path of the file to shorten the error message.
func (e *FileError) Path() string {
	workDir, err := os.Getwd()

	if err != nil {
		return e.file.Path
	}

	relative, err := filepath.Rel(workDir, e.file.Path)

	if err != nil {
		return e.file.Path
	}

	return filepath.ToSlash(relative)
}

// Position returns the byte offset inside the file.
func (e *FileError) Position() token.Position {
	return e.position
}

// Stack returns the call stack at the time the error was created.
func (e *FileError) Stack() string {
	return e.stack
}

// Unwrap returns the wrapped error.
func (e *FileError) Unwrap() error {
	return e.err
}