package patience

// Hunk represents a subsection of a diff.
type Hunk struct {
	Diffs    []DiffLine
	SrcStart int
	SrcLines int
	DstStart int
	DstLines int
}

// makeHunks returns the hunks of a diff.
func makeHunks(diffs []DiffLine, precontext, postcontext int) []Hunk {
	if len(diffs) == 0 {
		return nil
	}

	hunks := []Hunk{}

	// Update hunks with a diff block.
	updateHunks := func(block Hunk, lastBlock bool) {
		curHunk := len(hunks) - 1
		if block.Diffs[0].Type == Equal {
			// Unmodified block.
			if len(hunks) == 0 {
				// Start a new hunk with the tail of the block.
				ctxLen := min(precontext, len(block.Diffs))
				hunks = append(
					hunks,
					Hunk{
						Diffs:    block.Diffs[len(block.Diffs)-ctxLen:],
						SrcStart: len(block.Diffs) - ctxLen + block.SrcStart,
						SrcLines: ctxLen,
						DstStart: len(block.Diffs) - ctxLen + block.DstStart,
						DstLines: ctxLen,
					},
				)
			} else {
				// Update the current hunk.
				maxNonContext := precontext + postcontext
				if lastBlock {
					maxNonContext = postcontext
				}
				if len(block.Diffs) <= maxNonContext {
					// Block is small enough to be appended to the current hunk.
					hunks[curHunk].Diffs = append(hunks[curHunk].Diffs, block.Diffs...)
					hunks[curHunk].SrcLines += len(block.Diffs)
					hunks[curHunk].DstLines += len(block.Diffs)
				} else {
					// Append the head of the block to the current hunk.
					hunks[curHunk].Diffs = append(hunks[curHunk].Diffs, block.Diffs[:postcontext]...)
					hunks[curHunk].SrcLines += postcontext
					hunks[curHunk].DstLines += postcontext
					if !lastBlock {
						// Start a new hunk with the tail of the block.
						hunks = append(
							hunks,
							Hunk{
								Diffs:    block.Diffs[len(block.Diffs)-precontext:],
								SrcStart: len(block.Diffs) - precontext + block.SrcStart,
								SrcLines: precontext,
								DstStart: len(block.Diffs) - precontext + block.DstStart,
								DstLines: precontext,
							},
						)
					}
				}
				// Update starting line numbers if the current hunk had no source or destination diff.
				if hunks[curHunk].SrcStart == 0 {
					hunks[curHunk].SrcStart = block.SrcStart
				}
				if hunks[curHunk].DstStart == 0 {
					hunks[curHunk].DstStart = block.DstStart
				}
			}
		} else {
			// Modified block.
			if len(hunks) > 0 {
				hunks[curHunk].Diffs = append(hunks[curHunk].Diffs, block.Diffs...)
				hunks[curHunk].SrcLines += block.SrcLines
				hunks[curHunk].DstLines += block.DstLines
			} else {
				hunks = append(
					hunks,
					Hunk{
						Diffs:    block.Diffs,
						SrcStart: block.SrcStart,
						SrcLines: block.SrcLines,
						DstStart: block.DstStart,
						DstLines: block.DstLines,
					},
				)
			}
		}
	}

	// Aggregate blocks of modified and unmodified diff lines, creating
	// or updating hunks after each block.
	var block Hunk
	modifiedLines := 0 //nolint:ifshort
	srcLineNum, dstLineNum := 0, 0
	for _, l := range diffs {
		if len(block.Diffs) == 0 ||
			block.Diffs[0].Type == l.Type ||
			(block.Diffs[0].Type != l.Type && block.Diffs[0].Type != Equal && l.Type != Equal) {
			block.Diffs = append(block.Diffs, l)
		} else {
			updateHunks(block, false)
			block = Hunk{Diffs: []DiffLine{l}}
		}

		switch l.Type {
		case Delete:
			srcLineNum++
			block.SrcLines++
			modifiedLines++
		case Insert:
			dstLineNum++
			block.DstLines++
			modifiedLines++
		case Equal:
			srcLineNum++
			dstLineNum++
			block.SrcLines++
			block.DstLines++
		}

		if block.SrcStart == 0 && (l.Type == Equal || l.Type == Delete) {
			block.SrcStart = srcLineNum
		}
		if block.DstStart == 0 && (l.Type == Equal || l.Type == Insert) {
			block.DstStart = dstLineNum
		}
	}
	updateHunks(block, true)

	// Return no hunks if the diffs contain only equal lines.
	if modifiedLines == 0 {
		return nil
	}

	return hunks
}

// min returns the minimum of two integers.
func min(a, b int) int {
	if a < b {
		return a
	}
	return b
}
