package input

import (
	"fmt"
	"strings"

	"github.com/emirpasic/gods/trees/redblacktree"
)

// SourcePositionMapper defines a helper struct for cached, faster lookup of rune position <->
// (line, column) for a specific source file.
type SourcePositionMapper struct {
	// rangeTree holds a tree that maps from rune position to a line and start position.
	rangeTree *redblacktree.Tree

	// lineMap holds a map from line number to rune positions for that line.
	lineMap map[int]inclusiveRange
}

// EmptySourcePositionMapper returns an empty source position mapper.
func EmptySourcePositionMapper() SourcePositionMapper {
	rangeTree := redblacktree.NewWith(inclusiveComparator)
	return SourcePositionMapper{rangeTree, map[int]inclusiveRange{}}
}

// CreateSourcePositionMapper returns a source position mapper for the contents of a source file.
func CreateSourcePositionMapper(contents []byte) SourcePositionMapper {
	lines := strings.Split(string(contents), "\n")
	rangeTree := redblacktree.NewWith(inclusiveComparator)
	lineMap := map[int]inclusiveRange{}

	currentStart := int(0)
	for index, line := range lines {
		lineEnd := currentStart + len(line)
		rangeTree.Put(inclusiveRange{currentStart, lineEnd}, lineAndStart{index, currentStart})
		lineMap[index] = inclusiveRange{currentStart, lineEnd}
		currentStart = lineEnd + 1
	}

	return SourcePositionMapper{rangeTree, lineMap}
}

type inclusiveRange struct {
	start int
	end   int
}

type lineAndStart struct {
	lineNumber    int
	startPosition int
}

func inclusiveComparator(a, b any) int {
	i1 := a.(inclusiveRange)
	i2 := b.(inclusiveRange)

	if i1.start >= i2.start && i1.end <= i2.end {
		return 0
	}

	diff := int64(i1.start) - int64(i2.start)

	if diff < 0 {
		return -1
	}
	if diff > 0 {
		return 1
	}
	return 0
}

// RunePositionToLineAndCol returns the line number and column position of the rune position in source.
func (spm SourcePositionMapper) RunePositionToLineAndCol(runePosition int) (int, int, error) {
	ls, found := spm.rangeTree.Get(inclusiveRange{runePosition, runePosition})
	if !found {
		return 0, 0, fmt.Errorf("unknown rune position %v in source file", runePosition)
	}

	las := ls.(lineAndStart)
	return las.lineNumber, runePosition - las.startPosition, nil
}

// LineAndColToRunePosition returns the rune position of the line number and column position in source.
func (spm SourcePositionMapper) LineAndColToRunePosition(lineNumber int, colPosition int) (int, error) {
	lineRuneInfo, hasLine := spm.lineMap[lineNumber]
	if !hasLine {
		return 0, fmt.Errorf("unknown line %v in source file", lineNumber)
	}

	if colPosition > lineRuneInfo.end-lineRuneInfo.start {
		return 0, fmt.Errorf("column position %v not found on line %v in source file", colPosition, lineNumber)
	}

	return lineRuneInfo.start + colPosition, nil
}
