// Copyright 2021 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Package lsppos provides utilities for working with LSP positions.
//
// See https://microsoft.github.io/language-server-protocol/specification#textDocuments
// for a description of LSP positions. Notably:
//  - Positions are specified by a 0-based line count and 0-based utf-16
//    character offset.
//  - Positions are line-ending agnostic: there is no way to specify \r|\n or
//    \n|. Instead the former maps to the end of the current line, and the
//    latter to the start of the next line.
package lsppos

import (
	"sort"
	"unicode/utf8"
)

type Mapper struct {
	nonASCII bool
	src      []byte

	// Start-of-line positions. If src is newline-terminated, the final entry will be empty.
	lines []int
}

func NewMapper(src []byte) *Mapper {
	m := &Mapper{src: src}
	if len(src) == 0 {
		return m
	}
	m.lines = []int{0}
	for offset, b := range src {
		if b == '\n' {
			m.lines = append(m.lines, offset+1)
		}
		if b >= utf8.RuneSelf {
			m.nonASCII = true
		}
	}
	return m
}

func (m *Mapper) Position(offset int) (line, char int) {
	if offset < 0 || offset > len(m.src) {
		return -1, -1
	}
	nextLine := sort.Search(len(m.lines), func(i int) bool {
		return offset < m.lines[i]
	})
	if nextLine == 0 {
		return -1, -1
	}
	line = nextLine - 1
	start := m.lines[line]
	var charOffset int
	if m.nonASCII {
		charOffset = UTF16len(m.src[start:offset])
	} else {
		charOffset = offset - start
	}

	var eol int
	if line == len(m.lines)-1 {
		eol = len(m.src)
	} else {
		eol = m.lines[line+1] - 1
	}

	// Adjustment for line-endings: \r|\n is the same as |\r\n.
	if offset == eol && offset > 0 && m.src[offset-1] == '\r' {
		charOffset--
	}

	return line, charOffset
}

func UTF16len(buf []byte) int {
	cnt := 0
	for _, r := range string(buf) {
		cnt++
		if r >= 1<<16 {
			cnt++
		}
	}
	return cnt
}
