package gitdiff

import (
	"errors"
	"fmt"
	"os"
	"strings"
)

// File describes changes to a single file. It can be either a text file or a
// binary file.
type File struct {
	OldName string
	NewName string

	IsNew    bool
	IsDelete bool
	IsCopy   bool
	IsRename bool

	OldMode os.FileMode
	NewMode os.FileMode

	OldOIDPrefix string
	NewOIDPrefix string
	Score        int

	// TextFragments contains the fragments describing changes to a text file. It
	// may be empty if the file is empty or if only the mode changes.
	TextFragments []*TextFragment

	// IsBinary is true if the file is a binary file. If the patch includes
	// binary data, BinaryFragment will be non-nil and describe the changes to
	// the data. If the patch is reversible, ReverseBinaryFragment will also be
	// non-nil and describe the changes needed to restore the original file
	// after applying the changes in BinaryFragment.
	IsBinary              bool
	BinaryFragment        *BinaryFragment
	ReverseBinaryFragment *BinaryFragment
}

// String returns a git diff representation of this file. The value can be
// parsed by this library to obtain the same File, but may not be the same as
// the original input.
func (f *File) String() string {
	var diff strings.Builder
	newFormatter(&diff).FormatFile(f)
	return diff.String()
}

// TextFragment describes changed lines starting at a specific line in a text file.
type TextFragment struct {
	Comment string

	OldPosition int64
	OldLines    int64

	NewPosition int64
	NewLines    int64

	LinesAdded   int64
	LinesDeleted int64

	LeadingContext  int64
	TrailingContext int64

	Lines []Line
}

// String returns a git diff format of this fragment. See [File.String] for
// more details on this format.
func (f *TextFragment) String() string {
	var diff strings.Builder
	newFormatter(&diff).FormatTextFragment(f)
	return diff.String()
}

// Header returns a git diff header of this fragment. See [File.String] for
// more details on this format.
func (f *TextFragment) Header() string {
	var hdr strings.Builder
	newFormatter(&hdr).FormatTextFragmentHeader(f)
	return hdr.String()
}

// Validate checks that the fragment is self-consistent and appliable. Validate
// returns an error if and only if the fragment is invalid.
func (f *TextFragment) Validate() error {
	if f == nil {
		return errors.New("nil fragment")
	}

	var (
		oldLines, newLines                     int64
		leadingContext, trailingContext        int64
		contextLines, addedLines, deletedLines int64
	)

	// count the types of lines in the fragment content
	for i, line := range f.Lines {
		switch line.Op {
		case OpContext:
			oldLines++
			newLines++
			contextLines++
			if addedLines == 0 && deletedLines == 0 {
				leadingContext++
			} else {
				trailingContext++
			}
		case OpAdd:
			newLines++
			addedLines++
			trailingContext = 0
		case OpDelete:
			oldLines++
			deletedLines++
			trailingContext = 0
		default:
			return fmt.Errorf("unknown operator %q on line %d", line.Op, i+1)
		}
	}

	// check the actual counts against the reported counts
	if oldLines != f.OldLines {
		return lineCountErr("old", oldLines, f.OldLines)
	}
	if newLines != f.NewLines {
		return lineCountErr("new", newLines, f.NewLines)
	}
	if leadingContext != f.LeadingContext {
		return lineCountErr("leading context", leadingContext, f.LeadingContext)
	}
	if trailingContext != f.TrailingContext {
		return lineCountErr("trailing context", trailingContext, f.TrailingContext)
	}
	if addedLines != f.LinesAdded {
		return lineCountErr("added", addedLines, f.LinesAdded)
	}
	if deletedLines != f.LinesDeleted {
		return lineCountErr("deleted", deletedLines, f.LinesDeleted)
	}

	// if a file is being created, it can only contain additions
	if f.OldPosition == 0 && f.OldLines != 0 {
		return errors.New("file creation fragment contains context or deletion lines")
	}

	return nil
}

func lineCountErr(kind string, actual, reported int64) error {
	return fmt.Errorf("fragment contains %d %s lines but reports %d", actual, kind, reported)
}

// Line is a line in a text fragment.
type Line struct {
	Op   LineOp
	Line string
}

func (fl Line) String() string {
	return fl.Op.String() + fl.Line
}

// Old returns true if the line appears in the old content of the fragment.
func (fl Line) Old() bool {
	return fl.Op == OpContext || fl.Op == OpDelete
}

// New returns true if the line appears in the new content of the fragment.
func (fl Line) New() bool {
	return fl.Op == OpContext || fl.Op == OpAdd
}

// NoEOL returns true if the line is missing a trailing newline character.
func (fl Line) NoEOL() bool {
	return len(fl.Line) == 0 || fl.Line[len(fl.Line)-1] != '\n'
}

// LineOp describes the type of a text fragment line: context, added, or removed.
type LineOp int

const (
	// OpContext indicates a context line
	OpContext LineOp = iota
	// OpDelete indicates a deleted line
	OpDelete
	// OpAdd indicates an added line
	OpAdd
)

func (op LineOp) String() string {
	switch op {
	case OpContext:
		return " "
	case OpDelete:
		return "-"
	case OpAdd:
		return "+"
	}
	return "?"
}

// BinaryFragment describes changes to a binary file.
type BinaryFragment struct {
	Method BinaryPatchMethod
	Size   int64
	Data   []byte
}

// BinaryPatchMethod is the method used to create and apply the binary patch.
type BinaryPatchMethod int

const (
	// BinaryPatchDelta indicates the data uses Git's packfile encoding
	BinaryPatchDelta BinaryPatchMethod = iota
	// BinaryPatchLiteral indicates the data is the exact file content
	BinaryPatchLiteral
)

// String returns a git diff format of this fragment. Due to differences in
// zlib implementation between Go and Git, encoded binary data in the result
// will likely differ from what Git produces for the same input. See
// [File.String] for more details on this format.
func (f *BinaryFragment) String() string {
	var diff strings.Builder
	newFormatter(&diff).FormatBinaryFragment(f)
	return diff.String()
}
