package patchutil

import (
	"fmt"
	"strings"

	"github.com/bluekeyes/go-gitdiff/gitdiff"
	"tangled.org/core/appview/filetree"
	"tangled.org/core/types"
)

type InterdiffResult struct {
	Files []*InterdiffFile
}

func (i *InterdiffResult) Stats() types.DiffStat {
	var ins, del int64
	for _, s := range i.ChangedFiles() {
		stat := s.Stats()
		ins += stat.Insertions
		del += stat.Deletions
	}
	return types.DiffStat{
		Insertions:   ins,
		Deletions:    del,
		FilesChanged: len(i.Files),
	}
}

func (i *InterdiffResult) ChangedFiles() []types.DiffFileRenderer {
	drs := make([]types.DiffFileRenderer, len(i.Files))
	for i, s := range i.Files {
		drs[i] = s
	}
	return drs
}

func (i *InterdiffResult) FileTree() *filetree.FileTreeNode {
	fs := make([]string, len(i.Files))
	for i, s := range i.Files {
		fs[i] = s.Name
	}
	return filetree.FileTree(fs)
}

func (i *InterdiffResult) String() string {
	var b strings.Builder
	for _, f := range i.Files {
		b.WriteString(f.String())
		b.WriteString("\n")
	}

	return b.String()
}

type InterdiffFile struct {
	*gitdiff.File
	Name   string
	Status InterdiffFileStatus
}

func (s *InterdiffFile) Id() string {
	return s.Name
}

func (s *InterdiffFile) Split() types.SplitDiff {
	fragments := make([]types.SplitFragment, len(s.TextFragments))

	for i, fragment := range s.TextFragments {
		leftLines, rightLines := types.SeparateLines(fragment)

		fragments[i] = types.SplitFragment{
			Header:     fragment.Header(),
			LeftLines:  leftLines,
			RightLines: rightLines,
		}
	}

	return types.SplitDiff{
		Name:          s.Id(),
		TextFragments: fragments,
	}
}

func (s *InterdiffFile) CanRender() string {
	if s.Status.IsUnchanged() {
		return "This file has not been changed."
	} else if s.Status.IsRebased() {
		return "This patch was likely rebased, as context lines do not match."
	} else if s.Status.IsError() {
		return "Failed to calculate interdiff for this file."
	} else {
		return ""
	}
}

func (s *InterdiffFile) Names() types.DiffFileName {
	var n types.DiffFileName
	n.New = s.Name
	return n
}

func (s *InterdiffFile) Stats() types.DiffFileStat {
	var ins, del int64

	if s.File != nil {
		for _, f := range s.TextFragments {
			ins += f.LinesAdded
			del += f.LinesDeleted
		}
	}

	return types.DiffFileStat{
		Insertions: ins,
		Deletions:  del,
	}
}

func (s *InterdiffFile) String() string {
	var b strings.Builder
	b.WriteString(s.Status.String())
	b.WriteString(" ")

	if s.File != nil {
		b.WriteString(bestName(s.File))
		b.WriteString("\n")
		b.WriteString(s.File.String())
	}

	return b.String()
}

type InterdiffFileStatus struct {
	StatusKind StatusKind
	Error      error
}

func (s *InterdiffFileStatus) String() string {
	kind := s.StatusKind.String()
	if s.Error != nil {
		return fmt.Sprintf("%s [%s]", kind, s.Error.Error())
	} else {
		return kind
	}
}

func (s *InterdiffFileStatus) IsOk() bool {
	return s.StatusKind == StatusOk
}

func (s *InterdiffFileStatus) IsUnchanged() bool {
	return s.StatusKind == StatusUnchanged
}

func (s *InterdiffFileStatus) IsOnlyInOne() bool {
	return s.StatusKind == StatusOnlyInOne
}

func (s *InterdiffFileStatus) IsOnlyInTwo() bool {
	return s.StatusKind == StatusOnlyInTwo
}

func (s *InterdiffFileStatus) IsRebased() bool {
	return s.StatusKind == StatusRebased
}

func (s *InterdiffFileStatus) IsError() bool {
	return s.StatusKind == StatusError
}

type StatusKind int

func (k StatusKind) String() string {
	switch k {
	case StatusOnlyInOne:
		return "only in one"
	case StatusOnlyInTwo:
		return "only in two"
	case StatusUnchanged:
		return "unchanged"
	case StatusRebased:
		return "rebased"
	case StatusError:
		return "error"
	default:
		return "changed"
	}
}

const (
	StatusOk StatusKind = iota
	StatusOnlyInOne
	StatusOnlyInTwo
	StatusUnchanged
	StatusRebased
	StatusError
)

func interdiffFiles(f1, f2 *gitdiff.File) *InterdiffFile {
	re1 := CreatePreImage(f1)
	re2 := CreatePreImage(f2)

	interdiffFile := InterdiffFile{
		Name: bestName(f1),
	}

	merged, err := re1.Merge(&re2)
	if err != nil {
		interdiffFile.Status = InterdiffFileStatus{
			StatusKind: StatusRebased,
			Error:      err,
		}
		return &interdiffFile
	}

	rev1, err := merged.Apply(f1)
	if err != nil {
		interdiffFile.Status = InterdiffFileStatus{
			StatusKind: StatusError,
			Error:      err,
		}
		return &interdiffFile
	}

	rev2, err := merged.Apply(f2)
	if err != nil {
		interdiffFile.Status = InterdiffFileStatus{
			StatusKind: StatusError,
			Error:      err,
		}
		return &interdiffFile
	}

	diff, err := Unified(rev1, bestName(f1), rev2, bestName(f2))
	if err != nil {
		interdiffFile.Status = InterdiffFileStatus{
			StatusKind: StatusError,
			Error:      err,
		}
		return &interdiffFile
	}

	parsed, _, err := gitdiff.Parse(strings.NewReader(diff))
	if err != nil {
		interdiffFile.Status = InterdiffFileStatus{
			StatusKind: StatusError,
			Error:      err,
		}
		return &interdiffFile
	}

	if len(parsed) != 1 {
		// files are identical?
		interdiffFile.Status = InterdiffFileStatus{
			StatusKind: StatusUnchanged,
		}
		return &interdiffFile
	}

	if interdiffFile.Status.StatusKind == StatusOk {
		interdiffFile.File = parsed[0]
	}

	return &interdiffFile
}

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

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

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

	for _, f1 := range patch1 {
		var interdiffFile *InterdiffFile

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

			// we have f1 and f2, calculate interdiff
			interdiffFile = interdiffFiles(f1, f2)
		} else {
			// only in patch 1, this change would have to be "inverted" to disappear
			// from patch 2, so we reverseDiff(f1)
			reverseDiff(f1)

			interdiffFile = &InterdiffFile{
				File: f1,
				Name: fileName,
				Status: InterdiffFileStatus{
					StatusKind: StatusOnlyInOne,
				},
			}
		}

		result.Files = append(result.Files, interdiffFile)
		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.Files = append(result.Files, &InterdiffFile{
			File: f2,
			Name: fileName,
			Status: InterdiffFileStatus{
				StatusKind: StatusOnlyInTwo,
			},
		})
	}

	return &result
}
