package paths

import "fmt"

// DiffChange represents a file change made in a directory.
type DiffChange struct {
	Path string
	Kind DiffKind
}

// String implements Stringer.
func (c DiffChange) String() string {
	return fmt.Sprintf("%v %v", c.Kind, c.Path)
}

// DiffKind represents a type of file change made in a directory.
type DiffKind int

const (
	DiffAdded DiffKind = iota + 1
	DiffModified
	DiffRemoved
	DiffUnchanged
)

// String implements Stringer.
func (k DiffKind) String() string {
	switch k {
	case DiffAdded:
		return "added"
	case DiffModified:
		return "modified"
	case DiffRemoved:
		return "removed"
	case DiffUnchanged:
		return "unchanged"
	default:
		panic(fmt.Sprintf("%d: unknown DiffKind", int(k)))
	}
}

// Symbol returns a single character symbol representing this change.
func (k DiffKind) Symbol() string {
	switch k {
	case DiffAdded:
		return "+"
	case DiffModified:
		return "~"
	case DiffRemoved:
		return "-"
	case DiffUnchanged:
		return "/"
	default:
		panic(fmt.Sprintf("%d: unknown DiffKind", int(k)))
	}
}

// Diff compares two sources of Metadata and report the file changes, using the
// file modification date.
//
// Returns the number of files in the source.
//
// Warning: The Metadata have to be sorted by their Path for the diffing to
// work properly.
func Diff(source, target <-chan Metadata, forceModified bool, callback func(DiffChange) error) (int, error) {
	var err error
	var sourceFile, targetFile Metadata
	var sourceOpened, targetOpened = true, true
	sourceCount := 0
	pair := diffPair{}

	for err == nil && (sourceOpened || targetOpened) {
		if pair.source == nil {
			sourceFile, sourceOpened = <-source
			if sourceOpened {
				sourceCount += 1
				pair.source = &sourceFile
			}
		}
		if pair.target == nil {
			targetFile, targetOpened = <-target
			if targetOpened {
				pair.target = &targetFile
			}
		}
		change := pair.diff(forceModified)
		if change != nil {
			err = callback(*change)
		}
	}

	return sourceCount, err
}

// diffPair holds the current two files to be diffed.
type diffPair struct {
	source *Metadata
	target *Metadata
}

// diff compares the source and target files in the current pair.
//
// If the source and target file are at the same path, we check for any change.
// If the files are different, that means that either the source file was
// added, or the target file was removed.
func (p *diffPair) diff(forceModified bool) *DiffChange {
	var change *DiffChange

	switch {
	case p.source == nil && p.target == nil: // Both channels are closed
		break

	case p.source == nil && p.target != nil: // Source channel is closed
		change = &DiffChange{p.target.Path, DiffRemoved}
		p.target = nil

	case p.source != nil && p.target == nil: // Target channel is closed
		change = &DiffChange{p.source.Path, DiffAdded}
		p.source = nil

	case p.source.Path == p.target.Path: // Same files, compare their modification date.
		if forceModified || p.source.Modified != p.target.Modified {
			change = &DiffChange{p.source.Path, DiffModified}
		} else {
			change = &DiffChange{p.source.Path, DiffUnchanged}
		}
		p.source = nil
		p.target = nil

	default: // Different files, one has been added or removed.
		if p.source.Path < p.target.Path {
			change = &DiffChange{p.source.Path, DiffAdded}
			p.source = nil
		} else {
			change = &DiffChange{p.target.Path, DiffRemoved}
			p.target = nil
		}
	}

	return change
}
