package patch

import (
	"bytes"
	"os"
)

type TextDiff []TextChunk

// A TextChunk specifies an edit to a section of a file:
// the text beginning at Line, which should be exactly Old,
// is to be replaced with New.
type TextChunk struct {
	Line int
	Old  []byte
	New  []byte
}

func ParseTextDiff(raw []byte) (TextDiff, os.Error) {
	// Copy raw so it is safe to keep references to slices.
	_, chunks := sections(raw, "@@ -")
	delta := 0
	diff := make(TextDiff, len(chunks))
	for i, raw := range chunks {
		c := &diff[i]

		// Parse start line: @@ -oldLine,oldCount +newLine,newCount @@ junk
		chunk := splitLines(raw)
		chunkHeader := chunk[0]
		var ok bool
		var oldLine, oldCount, newLine, newCount int
		s := chunkHeader
		if oldLine, s, ok = atoi(s, "@@ -", 10); !ok {
		ErrChunkHdr:
			return nil, SyntaxError("unexpected chunk header line: " + string(chunkHeader))
		}
		if len(s) == 0 || s[0] != ',' {
			oldCount = 1
		} else if oldCount, s, ok = atoi(s, ",", 10); !ok {
			goto ErrChunkHdr
		}
		if newLine, s, ok = atoi(s, " +", 10); !ok {
			goto ErrChunkHdr
		}
		if len(s) == 0 || s[0] != ',' {
			newCount = 1
		} else if newCount, s, ok = atoi(s, ",", 10); !ok {
			goto ErrChunkHdr
		}
		if !hasPrefix(s, " @@") {
			goto ErrChunkHdr
		}

		// Special case: for created or deleted files, the empty half
		// is given as starting at line 0.  Translate to line 1.
		if oldCount == 0 && oldLine == 0 {
			oldLine = 1
		}
		if newCount == 0 && newLine == 0 {
			newLine = 1
		}

		// Count lines in text
		var dropOldNL, dropNewNL bool
		var nold, nnew int
		var lastch byte
		chunk = chunk[1:]
		for _, l := range chunk {
			if nold == oldCount && nnew == newCount && (len(l) == 0 || l[0] != '\\') {
				if len(bytes.TrimSpace(l)) != 0 {
					return nil, SyntaxError("too many chunk lines")
				}
				continue
			}
			if len(l) == 0 {
				return nil, SyntaxError("empty chunk line")
			}
			switch l[0] {
			case '+':
				nnew++
			case '-':
				nold++
			case ' ':
				nnew++
				nold++
			case '\\':
				if _, ok := skip(l, "\\ No newline at end of file"); ok {
					switch lastch {
					case '-':
						dropOldNL = true
					case '+':
						dropNewNL = true
					case ' ':
						dropOldNL = true
						dropNewNL = true
					default:
						return nil, SyntaxError("message `\\ No newline at end of file' out of context")
					}
					break
				}
				fallthrough
			default:
				return nil, SyntaxError("unexpected chunk line: " + string(l))
			}
			lastch = l[0]
		}

		// Does it match the header?
		if nold != oldCount || nnew != newCount {
			return nil, SyntaxError("chunk header does not match line count: " + string(chunkHeader))
		}
		if oldLine+delta != newLine {
			return nil, SyntaxError("chunk delta is out of sync with previous chunks")
		}
		delta += nnew - nold
		c.Line = oldLine

		var old, new bytes.Buffer
		nold = 0
		nnew = 0
		for _, l := range chunk {
			if nold == oldCount && nnew == newCount {
				break
			}
			ch, l := l[0], l[1:]
			if ch == '\\' {
				continue
			}
			if ch != '+' {
				old.Write(l)
				nold++
			}
			if ch != '-' {
				new.Write(l)
				nnew++
			}
		}
		c.Old = old.Bytes()
		c.New = new.Bytes()
		if dropOldNL {
			c.Old = c.Old[0 : len(c.Old)-1]
		}
		if dropNewNL {
			c.New = c.New[0 : len(c.New)-1]
		}
	}
	return diff, nil
}

var ErrPatchFailure = os.NewError("patch did not apply cleanly")

// Apply applies the changes listed in the diff
// to the data, returning the new version.
func (d TextDiff) Apply(data []byte) ([]byte, os.Error) {
	var buf bytes.Buffer
	line := 1
	for _, c := range d {
		var ok bool
		var prefix []byte
		prefix, data, ok = getLine(data, c.Line-line)
		if !ok || !bytes.HasPrefix(data, c.Old) {
			return nil, ErrPatchFailure
		}
		buf.Write(prefix)
		data = data[len(c.Old):]
		buf.Write(c.New)
		line = c.Line + bytes.Count(c.Old, newline)
	}
	buf.Write(data)
	return buf.Bytes(), nil
}