package main

import (
	diff "0dev.org/diff"
	"bufio"
	"fmt"
	"hash"
	"hash/fnv"
	"io"
	"os"
)

const usage = "Usage: plaindiff <file1> <file2>"

func main() {
	var args []string = os.Args
	if len(args) != 3 {
		os.Stderr.WriteString(usage)
		os.Exit(1)
	}

	var hd hashDiff
	hd.hash = fnv.New64a()
	hd.first = read(args[1], hd.hash)
	hd.second = read(args[2], hd.hash)

	var result diff.Delta = diff.Diff(&hd)

	gen := source(result)
	out := bufio.NewWriter(os.Stdout)
	for have, added, mark := gen(); have; have, added, mark = gen() {
		var from []line = hd.line(!added)

		fmt.Fprintln(out)
		for i := mark.From; i < mark.Length; i++ {
			fmt.Fprint(out, i)
			if added {
				fmt.Fprint(out, " > ")
			} else {
				fmt.Fprint(out, " < ")
			}
			fmt.Fprintln(out, from[i].text)
		}
	}
	out.Flush()
}

// Returns a closure over the provided diff.Delta
// that returns diff.Mark entries while prioritizing removals when possible
func source(d diff.Delta) func() (bool, bool, diff.Mark) {
	var addedAt, removedAt int = 0, 0
	return func() (bool, bool, diff.Mark) {
		var addsOver bool = addedAt == len(d.Added)
		var removesOver bool = removedAt == len(d.Removed)

		var add, remove diff.Mark

		// Check whether both mark slices have been exhausted
		if addsOver && removesOver {
			return false, false, diff.Mark{}
		}

		// Return an add if removes are over
		if removesOver {
			add = d.Added[addedAt]
			addedAt++
			return true, true, add
		}

		// Return a remove if the adds are over
		if addsOver {
			remove = d.Removed[removedAt]
			removedAt++
			return true, false, remove
		}

		add = d.Added[addedAt]
		remove = d.Removed[removedAt]

		// Prioritize a remove if it happens before an add
		if remove.From <= add.From {
			removedAt++
			return true, false, remove
		}

		// Else
		addedAt++
		return true, true, add

	}
}

// A line-based diff.Interface implementation
type hashDiff struct {
	first, second []line
	hash          hash.Hash64
}

// Required per diff.Interface
func (h *hashDiff) Equal(i, j int) bool {
	if h.first[i].hash != h.second[j].hash {
		return false
	} else {
		return h.first[i].text == h.second[j].text
	}
}

// Required per diff.Interface
func (h *hashDiff) Len() (int, int) {
	return len(h.first), len(h.second)
}

// A helper method for getting a line slice
func (h *hashDiff) line(first bool) []line {
	if first {
		return h.first
	}
	return h.second
}

// Holds a text line and its hash
type line struct {
	hash uint64
	text string
}

// Reads all lines in a file and returns a line entry for each
func read(name string, h hash.Hash64) []line {
	var f *os.File
	var e error

	f, e = os.Open(name)
	fatal(e)

	var result []line = lines(f, h)
	fatal(f.Close())

	return result
}

// Reads all lines and returns a line entry for each
func lines(r io.Reader, h hash.Hash64) []line {
	var scanner *bufio.Scanner = bufio.NewScanner(r)
	var result []line = make([]line, 0)
	for scanner.Scan() {
		h.Reset()
		h.Write(scanner.Bytes())
		result = append(result, line{hash: h.Sum64(), text: scanner.Text()})
	}
	return result
}

// Write an error to stderr
func stderr(e error) {
	if e != nil {
		os.Stderr.WriteString(e.Error())
	}
}

// Write an error to stderr and exit
func fatal(e error) {
	if e != nil {
		stderr(e)
		os.Exit(1)
	}
}
