package patchutil

import (
	"fmt"
	"strings"

	"github.com/bluekeyes/go-gitdiff/gitdiff"
)

// original1 -> patch1 -> rev1
// original2 -> patch2 -> rev2
//
// original2 must be equal to rev1, so we can merge them to get maximal context
//
// finally,
// rev2' <- apply(patch2, merged)
// combineddiff <- diff(rev2', original1)
func combineFiles(file1, file2 *gitdiff.File) (*gitdiff.File, error) {
	fileName := bestName(file1)

	o1 := CreatePreImage(file1)
	r1 := CreatePostImage(file1)
	o2 := CreatePreImage(file2)

	merged, err := r1.Merge(&o2)
	if err != nil {
		return nil, err
	}

	r2Prime, err := merged.Apply(file2)
	if err != nil {
		return nil, err
	}

	// produce combined diff
	diff, err := Unified(o1.String(), fileName, r2Prime, fileName)
	if err != nil {
		return nil, err
	}

	parsed, _, err := gitdiff.Parse(strings.NewReader(diff))

	if len(parsed) != 1 {
		// no diff? the second commit reverted the changes from the first
		return nil, nil
	}

	return parsed[0], nil
}

// use empty lines for lines we are unaware of
//
// this raises an error only if the two patches were invalid or non-contiguous
func mergeLines(old, new string) (string, error) {
	var i, j int

	// TODO: use strings.Lines
	linesOld := strings.Split(old, "\n")
	linesNew := strings.Split(new, "\n")

	result := []string{}

	for i < len(linesOld) || j < len(linesNew) {
		if i >= len(linesOld) {
			// rest of the file is populated from `new`
			result = append(result, linesNew[j])
			j++
			continue
		}

		if j >= len(linesNew) {
			// rest of the file is populated from `old`
			result = append(result, linesOld[i])
			i++
			continue
		}

		oldLine := linesOld[i]
		newLine := linesNew[j]

		if oldLine != newLine && (oldLine != "" && newLine != "") {
			// context mismatch
			return "", fmt.Errorf("failed to merge files, found context mismatch at %d; oldLine: `%s`, newline: `%s`", i+1, oldLine, newLine)
		}

		if oldLine == newLine {
			result = append(result, oldLine)
		} else if oldLine == "" {
			result = append(result, newLine)
		} else if newLine == "" {
			result = append(result, oldLine)
		}
		i++
		j++
	}

	return strings.Join(result, "\n"), nil
}

func combineTwo(patch1, patch2 []*gitdiff.File) []*gitdiff.File {
	fileToIdx1 := make(map[string]int)
	fileToIdx2 := make(map[string]int)
	visited := make(map[string]struct{})
	var result []*gitdiff.File

	for idx, f := range patch1 {
		fileToIdx1[bestName(f)] = idx
	}

	for idx, f := range patch2 {
		fileToIdx2[bestName(f)] = idx
	}

	for _, f1 := range patch1 {
		fileName := bestName(f1)
		if idx, ok := fileToIdx2[fileName]; ok {
			f2 := patch2[idx]

			// we have f1 and f2, combine them
			combined, err := combineFiles(f1, f2)
			if err != nil {
				// fmt.Println(err)
			}

			// combined can be nil commit 2 reverted all changes from commit 1
			if combined != nil {
				result = append(result, combined)
			}

		} else {
			// only in patch1; add as-is
			result = append(result, f1)
		}

		visited[fileName] = struct{}{}
	}

	// for all files in patch2 that remain unvisited; we can just add them into the output
	for _, f2 := range patch2 {
		fileName := bestName(f2)
		if _, ok := visited[fileName]; ok {
			continue
		}

		result = append(result, f2)
	}

	return result
}

// pairwise combination from first to last patch
func CombineDiff(patches ...[]*gitdiff.File) []*gitdiff.File {
	if len(patches) == 0 {
		return nil
	}

	if len(patches) == 1 {
		return patches[0]
	}

	combined := combineTwo(patches[0], patches[1])

	newPatches := [][]*gitdiff.File{}
	newPatches = append(newPatches, combined)
	for i, p := range patches {
		if i >= 2 {
			newPatches = append(newPatches, p)
		}
	}

	return CombineDiff(newPatches...)
}
