// Copyright 2019 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 gotextdiff supports a pluggable diff algorithm.
package gotextdiff

import (
	"sort"
	"strings"

	"github.com/hexops/gotextdiff/span"
)

// TextEdit represents a change to a section of a document.
// The text within the specified span should be replaced by the supplied new text.
type TextEdit struct {
	Span    span.Span
	NewText string
}

// ComputeEdits is the type for a function that produces a set of edits that
// convert from the before content to the after content.
type ComputeEdits func(uri span.URI, before, after string) []TextEdit

// SortTextEdits attempts to order all edits by their starting points.
// The sort is stable so that edits with the same starting point will not
// be reordered.
func SortTextEdits(d []TextEdit) {
	// Use a stable sort to maintain the order of edits inserted at the same position.
	sort.SliceStable(d, func(i int, j int) bool {
		return span.Compare(d[i].Span, d[j].Span) < 0
	})
}

// ApplyEdits applies the set of edits to the before and returns the resulting
// content.
// It may panic or produce garbage if the edits are not valid for the provided
// before content.
func ApplyEdits(before string, edits []TextEdit) string {
	// Preconditions:
	//   - all of the edits apply to before
	//   - and all the spans for each TextEdit have the same URI
	if len(edits) == 0 {
		return before
	}
	_, edits, _ = prepareEdits(before, edits)
	after := strings.Builder{}
	last := 0
	for _, edit := range edits {
		start := edit.Span.Start().Offset()
		if start > last {
			after.WriteString(before[last:start])
			last = start
		}
		after.WriteString(edit.NewText)
		last = edit.Span.End().Offset()
	}
	if last < len(before) {
		after.WriteString(before[last:])
	}
	return after.String()
}

// LineEdits takes a set of edits and expands and merges them as necessary
// to ensure that there are only full line edits left when it is done.
func LineEdits(before string, edits []TextEdit) []TextEdit {
	if len(edits) == 0 {
		return nil
	}
	c, edits, partial := prepareEdits(before, edits)
	if partial {
		edits = lineEdits(before, c, edits)
	}
	return edits
}

// prepareEdits returns a sorted copy of the edits
func prepareEdits(before string, edits []TextEdit) (*span.TokenConverter, []TextEdit, bool) {
	partial := false
	c := span.NewContentConverter("", []byte(before))
	copied := make([]TextEdit, len(edits))
	for i, edit := range edits {
		edit.Span, _ = edit.Span.WithAll(c)
		copied[i] = edit
		partial = partial ||
			edit.Span.Start().Offset() >= len(before) ||
			edit.Span.Start().Column() > 1 || edit.Span.End().Column() > 1
	}
	SortTextEdits(copied)
	return c, copied, partial
}

// lineEdits rewrites the edits to always be full line edits
func lineEdits(before string, c *span.TokenConverter, edits []TextEdit) []TextEdit {
	adjusted := make([]TextEdit, 0, len(edits))
	current := TextEdit{Span: span.Invalid}
	for _, edit := range edits {
		if current.Span.IsValid() && edit.Span.Start().Line() <= current.Span.End().Line() {
			// overlaps with the current edit, need to combine
			// first get the gap from the previous edit
			gap := before[current.Span.End().Offset():edit.Span.Start().Offset()]
			// now add the text of this edit
			current.NewText += gap + edit.NewText
			// and then adjust the end position
			current.Span = span.New(current.Span.URI(), current.Span.Start(), edit.Span.End())
		} else {
			// does not overlap, add previous run (if there is one)
			adjusted = addEdit(before, adjusted, current)
			// and then remember this edit as the start of the next run
			current = edit
		}
	}
	// add the current pending run if there is one
	return addEdit(before, adjusted, current)
}

func addEdit(before string, edits []TextEdit, edit TextEdit) []TextEdit {
	if !edit.Span.IsValid() {
		return edits
	}
	// if edit is partial, expand it to full line now
	start := edit.Span.Start()
	end := edit.Span.End()
	if start.Column() > 1 {
		// prepend the text and adjust to start of line
		delta := start.Column() - 1
		start = span.NewPoint(start.Line(), 1, start.Offset()-delta)
		edit.Span = span.New(edit.Span.URI(), start, end)
		edit.NewText = before[start.Offset():start.Offset()+delta] + edit.NewText
	}
	if start.Offset() >= len(before) && start.Line() > 1 && before[len(before)-1] != '\n' {
		// after end of file that does not end in eol, so join to last line of file
		// to do this we need to know where the start of the last line was
		eol := strings.LastIndex(before, "\n")
		if eol < 0 {
			// file is one non terminated line
			eol = 0
		}
		delta := len(before) - eol
		start = span.NewPoint(start.Line()-1, 1, start.Offset()-delta)
		edit.Span = span.New(edit.Span.URI(), start, end)
		edit.NewText = before[start.Offset():start.Offset()+delta] + edit.NewText
	}
	if end.Column() > 1 {
		remains := before[end.Offset():]
		eol := strings.IndexRune(remains, '\n')
		if eol < 0 {
			eol = len(remains)
		} else {
			eol++
		}
		end = span.NewPoint(end.Line()+1, 1, end.Offset()+eol)
		edit.Span = span.New(edit.Span.URI(), start, end)
		edit.NewText = edit.NewText + remains[:eol]
	}
	edits = append(edits, edit)
	return edits
}
