// Package text implements text editing helper routines for LSP.
package text

import (
	"fmt"
	"io"
	"net/url"
	"sort"
	"strings"

	"9fans.net/internal/go-lsp/lsp/protocol"
)

// File represents an open file in text editor.
type File interface {
	// Reader returns a reader for the entire file text buffer ("body" in acme).
	Reader() (io.Reader, error)

	// WriteAt replaces the text in rune range [q0, q1) with bytes b.
	WriteAt(q0, q1 int, b []byte) (int, error)

	// Mark marks the file for later undo.
	Mark() error

	// DisableMark turns off automatic marking (e.g. generated by WriteAt).
	DisableMark() error
}

type EditList []protocol.TextEdit

func (l EditList) Len() int      { return len(l) }
func (l EditList) Swap(i, j int) { l[i], l[j] = l[j], l[i] }
func (l EditList) Less(i, j int) bool {
	if l[i].Range.Start.Line == l[j].Range.Start.Line {
		return l[i].Range.Start.Character < l[j].Range.Start.Character
	}
	return l[i].Range.Start.Line < l[j].Range.Start.Line
}

// Edit applied edits to file f.
func Edit(f File, edits []protocol.TextEdit) error {
	if len(edits) == 0 {
		return nil
	}
	reader, err := f.Reader()
	if err != nil {
		return err
	}
	off, err := GetNewlineOffsets(reader)
	if err != nil {
		return fmt.Errorf("failed to obtain newline offsets: %v", err)
	}

	f.DisableMark()
	f.Mark()

	// Make sure edits follow the sequence from beginning to end
	// See comments below.
	sort.Sort(EditList(edits))

	// Applying the edits in reverse order gets the job done.
	// See https://github.com/golang/go/wiki/gopls#textdocumentformatting-response
	for i := len(edits) - 1; i >= 0; i-- {
		e := edits[i]
		q0 := off.LineToOffset(int(e.Range.Start.Line), int(e.Range.Start.Character))
		q1 := off.LineToOffset(int(e.Range.End.Line), int(e.Range.End.Character))
		f.WriteAt(q0, q1, []byte(e.NewText))
	}

	return nil
}

// AddressableFile represents an open file in text editor which has a current adddress.
type AddressableFile interface {
	File

	// Filename returns the filesystem path to the file.
	Filename() (string, error)

	// CurrentAddr returns the address of current selection.
	CurrentAddr() (q0, q1 int, err error)
}

// DocumentURI returns the URI and filename of a file being edited.
func DocumentURI(f AddressableFile) (uri protocol.DocumentURI, filename string, err error) {
	name, err := f.Filename()
	if err != nil {
		return "", "", err
	}
	return ToURI(name), name, nil
}

func PositionQ0(f AddressableFile, q0 int) (pos *protocol.TextDocumentPositionParams, filename string, err error) {
	name, err := f.Filename()
	if err != nil {
		return nil, "", fmt.Errorf("could not get window filename: %v", err)
	}
	reader, err := f.Reader()
	if err != nil {
		return nil, "", fmt.Errorf("could not get window body reader: %v", err)
	}
	off, err := GetNewlineOffsets(reader)
	if err != nil {
		return nil, "", fmt.Errorf("failed to get newline offset: %v", err)
	}
	line, col := off.OffsetToLine(q0)
	return &protocol.TextDocumentPositionParams{
		TextDocument: protocol.TextDocumentIdentifier{
			URI: ToURI(name),
		},
		Position: protocol.Position{
			Line:      uint32(line),
			Character: uint32(col),
		},
	}, name, nil
}

// Position returns the current position within a file being edited.
func Position(f AddressableFile) (pos *protocol.TextDocumentPositionParams, filename string, err error) {
	q0, _, err := f.CurrentAddr()
	if err != nil {
		return nil, "", fmt.Errorf("could not get current address: %v", err)
	}
	return PositionQ0(f, q0)
}

// ToURI converts filename to URI.
func ToURI(filename string) protocol.DocumentURI {
	u := &url.URL{
		Scheme: "file",
		Path:   filename,
	}
	return protocol.DocumentURI(u.String())
}

// ToPath converts URI to filename.
func ToPath(uri protocol.DocumentURI) string {
	u, err := url.Parse(string(uri))
	if err != nil {
		return string(uri)
	}
	return u.Path
}

// CutPrefix returns s without the provided leading prefix string
// and reports whether it found the prefix.
// If s doesn't start with prefix, CutPrefix returns s, false.
// If prefix is the empty string, CutPrefix returns s, true.
//
// TODO(fhs): remove and use strings.CutPrefix in Go >= 1.20
func CutPrefix(s, prefix string) (after string, found bool) {
	if !strings.HasPrefix(s, prefix) {
		return s, false
	}
	return s[len(prefix):], true
}
