package gaper

import (
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"time"

	zglob "github.com/mattn/go-zglob"
)

// Watcher is a interface for the watch process
type Watcher interface {
	Watch()
	Errors() chan error
	Events() chan string
}

// watcher is a interface for the watch process
type watcher struct {
	defaultIgnore     bool
	pollInterval      int
	watchItems        map[string]bool
	ignoreItems       map[string]bool
	allowedExtensions map[string]bool
	events            chan string
	errors            chan error
}

// WatcherConfig defines the settings available for the watcher
type WatcherConfig struct {
	DefaultIgnore bool
	PollInterval  int
	WatchItems    []string
	IgnoreItems   []string
	Extensions    []string
}

// NewWatcher creates a new watcher
func NewWatcher(cfg WatcherConfig) (Watcher, error) {
	if cfg.PollInterval == 0 {
		cfg.PollInterval = DefaultPoolInterval
	}

	if len(cfg.Extensions) == 0 {
		cfg.Extensions = DefaultExtensions
	}

	allowedExts := make(map[string]bool)
	for _, ext := range cfg.Extensions {
		allowedExts["."+ext] = true
	}

	watchPaths, err := resolvePaths(cfg.WatchItems, allowedExts)
	if err != nil {
		return nil, err
	}

	ignorePaths, err := resolvePaths(cfg.IgnoreItems, allowedExts)
	if err != nil {
		return nil, err
	}

	logger.Debugf("Resolved watch paths: %v", watchPaths)
	logger.Debugf("Resolved ignore paths: %v", ignorePaths)
	return &watcher{
		events:            make(chan string),
		errors:            make(chan error),
		defaultIgnore:     cfg.DefaultIgnore,
		pollInterval:      cfg.PollInterval,
		watchItems:        watchPaths,
		ignoreItems:       ignorePaths,
		allowedExtensions: allowedExts,
	}, nil
}

var startTime = time.Now()
var errDetectedChange = errors.New("done")

// Watch starts watching for file changes
func (w *watcher) Watch() {
	for {
		for watchPath := range w.watchItems {
			fileChanged, err := w.scanChange(watchPath)
			if err != nil {
				w.errors <- err
				return
			}

			if fileChanged != "" {
				w.events <- fileChanged
				startTime = time.Now()
			}
		}

		time.Sleep(time.Duration(w.pollInterval) * time.Millisecond)
	}
}

// Events get events occurred during the watching
// these events are emitted only a file changing is detected
func (w *watcher) Events() chan string {
	return w.events
}

// Errors get errors occurred during the watching
func (w *watcher) Errors() chan error {
	return w.errors
}

func (w *watcher) scanChange(watchPath string) (string, error) {
	logger.Debug("Watching ", watchPath)

	var fileChanged string

	err := filepath.Walk(watchPath, func(path string, info os.FileInfo, err error) error {
		if w.ignoreFile(path, info) {
			return skipFile(info)
		}

		ext := filepath.Ext(path)
		if _, ok := w.allowedExtensions[ext]; ok && info.ModTime().After(startTime) {
			fileChanged = path
			return errDetectedChange
		}

		return nil
	})

	if err != nil && err != errDetectedChange {
		return "", err
	}

	return fileChanged, nil
}

func (w *watcher) ignoreFile(path string, info os.FileInfo) bool {
	// check if preset ignore is enabled
	if w.defaultIgnore {
		// check for hidden files and directories
		if name := info.Name(); name[0] == '.' && name != "." {
			return true
		}

		// check if it is a Go testing file
		if strings.HasSuffix(path, "_test.go") {
			return true
		}

		// check if it is the vendor folder
		if info.IsDir() && info.Name() == "vendor" {
			return true
		}
	}

	if _, ignored := w.ignoreItems[path]; ignored {
		return true
	}
	return false
}

func resolvePaths(paths []string, extensions map[string]bool) (map[string]bool, error) {
	result := map[string]bool{}

	for _, path := range paths {
		matches := []string{path}

		isGlob := strings.Contains(path, "*")
		if isGlob {
			var err error
			matches, err = zglob.Glob(path)
			if err != nil {
				return nil, fmt.Errorf("couldn't resolve glob path \"%s\": %v", path, err)
			}
		}

		for _, match := range matches {
			// don't care for extension filter right now for non glob paths
			// since they could be a directory
			if isGlob {
				if _, ok := extensions[filepath.Ext(path)]; !ok {
					continue
				}
			}

			if _, ok := result[match]; !ok {
				result[match] = true
			}
		}
	}

	removeOverlappedPaths(result)

	return result, nil
}

// remove overlapped paths so it makes the scan for changes later faster and simpler
func removeOverlappedPaths(mapPaths map[string]bool) {
	for p1 := range mapPaths {
		// skip to next item if this path has already been checked
		if v, ok := mapPaths[p1]; ok && !v {
			continue
		}

		for p2 := range mapPaths {
			if p1 == p2 {
				continue
			}

			if strings.HasPrefix(p2, p1) {
				mapPaths[p2] = false
			} else if strings.HasPrefix(p1, p2) {
				mapPaths[p1] = false
			}
		}
	}

	// cleanup path list
	for p := range mapPaths {
		if !mapPaths[p] {
			delete(mapPaths, p)
		}
	}
}

func skipFile(info os.FileInfo) error {
	if info.IsDir() {
		return filepath.SkipDir
	}
	return nil
}