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:len(chunk)];
		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:len(l)];
			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):len(data)];
		buf.Write(c.New);
		line = c.Line + bytes.Count(c.Old, newline);
	}
	buf.Write(data);
	return buf.Bytes(), nil;
}
