// 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 diff

import (
	"fmt"
	"strings"
)

type Unified struct {
	From, To string
	Hunks    []*Hunk
}

type Hunk struct {
	FromLine int
	ToLine   int
	Lines    []Line
}

type Line struct {
	Kind    OpKind
	Content string
}

const (
	edge = 3
	gap  = edge * 2
)

func ToUnified(from, to string, lines []string, ops []*Op) Unified {
	u := Unified{
		From: from,
		To:   to,
	}
	if len(ops) == 0 {
		return u
	}
	var h *Hunk
	last := -(gap + 2)
	for _, op := range ops {
		switch {
		case op.I1 < last:
			panic("cannot convert unsorted operations to unified diff")
		case op.I1 == last:
			//direct extension
		case op.I1 <= last+gap:
			//within range of previous lines, add the joiners
			addEqualLines(h, lines, last, op.I1)
		default:
			//need to start a new hunk
			if h != nil {
				// add the edge to the previous hunk
				addEqualLines(h, lines, last, last+edge)
				u.Hunks = append(u.Hunks, h)
			}
			h = &Hunk{
				FromLine: op.I1 + 1,
				ToLine:   op.J1 + 1,
			}
			// add the edge to the new hunk
			delta := addEqualLines(h, lines, op.I1-edge, op.I1)
			h.FromLine -= delta
			h.ToLine -= delta
		}
		last = op.I1
		switch op.Kind {
		case Delete:
			for i := op.I1; i < op.I2; i++ {
				h.Lines = append(h.Lines, Line{Kind: Delete, Content: lines[i]})
				last++
			}
		case Insert:
			for _, c := range op.Content {
				h.Lines = append(h.Lines, Line{Kind: Insert, Content: c})
			}
		default:
			// all other op types ignored
		}
	}
	if h != nil {
		// add the edge to the final hunk
		addEqualLines(h, lines, last, last+edge)
		u.Hunks = append(u.Hunks, h)
	}
	return u
}

func addEqualLines(h *Hunk, lines []string, start, end int) int {
	delta := 0
	for i := start; i < end; i++ {
		if i < 0 {
			continue
		}
		if i >= len(lines) {
			return delta
		}
		h.Lines = append(h.Lines, Line{Kind: Equal, Content: lines[i]})
		delta++
	}
	return delta
}

func (u Unified) Format(f fmt.State, r rune) {
	fmt.Fprintf(f, "--- %s\n", u.From)
	fmt.Fprintf(f, "+++ %s\n", u.To)
	for _, hunk := range u.Hunks {
		fromCount, toCount := 0, 0
		for _, l := range hunk.Lines {
			switch l.Kind {
			case Delete:
				fromCount++
			case Insert:
				toCount++
			default:
				fromCount++
				toCount++
			}
		}
		fmt.Fprint(f, "@@")
		if fromCount > 1 {
			fmt.Fprintf(f, " -%d,%d", hunk.FromLine, fromCount)
		} else {
			fmt.Fprintf(f, " -%d", hunk.FromLine)
		}
		if toCount > 1 {
			fmt.Fprintf(f, " +%d,%d", hunk.ToLine, toCount)
		} else {
			fmt.Fprintf(f, " +%d", hunk.ToLine)
		}
		fmt.Fprint(f, " @@\n")
		for _, l := range hunk.Lines {
			switch l.Kind {
			case Delete:
				fmt.Fprintf(f, "-%s", l.Content)
			case Insert:
				fmt.Fprintf(f, "+%s", l.Content)
			default:
				fmt.Fprintf(f, " %s", l.Content)
			}
			if !strings.HasSuffix(l.Content, "\n") {
				fmt.Fprintf(f, "\n\\ No newline at end of file\n")
			}
		}
	}
}
