// Copyright 2013 Andreas Koch. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package fswatch

import (
	"fmt"
	"io/ioutil"
	"path/filepath"
	"time"
)

// numberOfFolderWatchers contains the current number of active folder watchers.
var numberOfFolderWatchers int

func init() {
	numberOfFolderWatchers = 0
}

// NumberOfFolderWatchers returns the number of currently active folder watchers.
func NumberOfFolderWatchers() int {
	return numberOfFolderWatchers
}

// A FolderWatcher can be used to watch a folder for modified or moved items.
type FolderWatcher struct {
	changeDetails chan *FolderChange

	modified chan bool
	moved    chan bool
	stopped  chan bool

	recurse  bool
	skipFile func(path string) bool

	folder        string
	running       bool
	wasStopped    bool
	checkInterval time.Duration

	previousEntries []string
}

// NewFolderWatcher creates a new folder watcher for the given folder path.
// The recurse flag indicates whether the watcher shall include sub folders of the the given folder path.
// The skipFile expression can be used to exclude certains files or folders.
// The check interval in seconds defines how often the watcher shall check for changes (recommended: 1 - n seconds).
func NewFolderWatcher(folderPath string, recurse bool, skipFile func(path string) bool, checkIntervalInSeconds int) *FolderWatcher {

	if checkIntervalInSeconds < 1 {
		panic(fmt.Sprintf("Cannot create a folder watcher with a check interval of %v seconds.", checkIntervalInSeconds))
	}

	return &FolderWatcher{

		modified: make(chan bool),
		moved:    make(chan bool),
		stopped:  make(chan bool),

		changeDetails: make(chan *FolderChange),

		recurse:  recurse,
		skipFile: skipFile,

		folder:        folderPath,
		checkInterval: time.Duration(checkIntervalInSeconds),
	}
}

func (folderWatcher *FolderWatcher) String() string {
	return fmt.Sprintf("Folderwatcher %q", folderWatcher.folder)
}

// Modified returns a channel indicating if the current folder has been modified.
func (folderWatcher *FolderWatcher) Modified() chan bool {
	return folderWatcher.modified
}

// Moved returns a channel indicating if the current folder has been moved.
func (folderWatcher *FolderWatcher) Moved() chan bool {
	return folderWatcher.moved
}

// Stopped returns a channel indicating if current folder watcher stopped.
func (folderWatcher *FolderWatcher) Stopped() chan bool {
	return folderWatcher.stopped
}

// ChangeDetails returns a model containing all changed during a given change interval.
func (folderWatcher *FolderWatcher) ChangeDetails() chan *FolderChange {
	return folderWatcher.changeDetails
}

// Start starts the watch process.
func (folderWatcher *FolderWatcher) Start() {
	folderWatcher.running = true
	sleepInterval := time.Second * folderWatcher.checkInterval

	go func() {

		// get existing entries
		var entryList []string
		directory := folderWatcher.folder

		previousEntryList := folderWatcher.getPreviousEntryList()

		if previousEntryList != nil {

			// use the entry list from a previous run
			entryList = previousEntryList

		} else {

			// use a new entry list
			newEntryList, _ := getFolderEntries(directory, folderWatcher.recurse, folderWatcher.skipFile)
			entryList = newEntryList
		}

		// increment watcher count
		numberOfFolderWatchers++

		for folderWatcher.wasStopped == false {

			// get new entries
			updatedEntryList, _ := getFolderEntries(directory, folderWatcher.recurse, folderWatcher.skipFile)

			// check for new items
			newItems := make([]string, 0)
			modifiedItems := make([]string, 0)

			for _, entry := range updatedEntryList {

				if isNewItem := !sliceContainsElement(entryList, entry); isNewItem {
					// entry is new
					newItems = append(newItems, entry)
					continue
				}

				// check if the file changed
				if newModTime, err := getLastModTimeFromFile(entry); err == nil {

					// check if file has been modified
					timeOfLastCheck := time.Now().Add(sleepInterval * -1)

					if timeOfLastCheck.Before(newModTime) {

						// existing entry has been modified
						modifiedItems = append(modifiedItems, entry)
					}

				}
			}

			// check for moved items
			movedItems := make([]string, 0)
			for _, entry := range entryList {
				isMoved := !sliceContainsElement(updatedEntryList, entry)
				if isMoved {
					movedItems = append(movedItems, entry)
				}
			}

			// assign the new list
			entryList = updatedEntryList

			// sleep
			time.Sleep(sleepInterval)

			// check if something happened
			if len(newItems) > 0 || len(movedItems) > 0 || len(modifiedItems) > 0 {

				// send out change
				go func() {
					folderWatcher.modified <- true
				}()

				go func() {
					log("Folder %q changed", directory)
					folderWatcher.changeDetails <- newFolderChange(newItems, movedItems, modifiedItems)
				}()
			} else {
				log("No change in folder %q", directory)
			}
		}

		folderWatcher.running = false

		// capture the entry list for a restart
		folderWatcher.captureEntryList(entryList)

		// inform channel-subscribers
		go func() {
			folderWatcher.stopped <- true
		}()

		// decrement the watch counter
		numberOfFolderWatchers--

		// final log message
		log("Stopped folder watcher %q", folderWatcher.String())
	}()
}

// Stop stops the watch process.
func (folderWatcher *FolderWatcher) Stop() {
	log("Stopping folder watcher %q", folderWatcher.String())
	folderWatcher.wasStopped = true
}

// IsRunning returns a flag indicating whether the watcher is currently running.
func (folderWatcher *FolderWatcher) IsRunning() bool {
	return folderWatcher.running
}

// getPreviousEntryList returns the entry list of the last watcher-run.
func (folderWatcher *FolderWatcher) getPreviousEntryList() []string {
	return folderWatcher.previousEntries
}

// Remember the entry list for a later restart
func (folderWatcher *FolderWatcher) captureEntryList(list []string) {
	folderWatcher.previousEntries = list
}

// getFolderEntries returns a list of all entries for the given direcotry path.
// The recurse flag indicates whether the watcher shall include sub folders of the the given folder path.
// The skipFile expression can be used to exclude certains files or folders.
func getFolderEntries(directory string, recurse bool, skipFile func(path string) bool) ([]string, error) {

	// the return array
	entries := make([]string, 0)

	// read the entries of the specified directory
	directoryEntries, err := ioutil.ReadDir(directory)
	if err != nil {
		return entries, err
	}

	for _, entry := range directoryEntries {

		// get the full path
		subEntryPath := filepath.Join(directory, entry.Name())

		// recurse or append
		if recurse && entry.IsDir() {

			// recurse (ignore errors, unreadable sub directories don't hurt much)
			subFolderEntries, _ := getFolderEntries(subEntryPath, recurse, skipFile)
			entries = append(entries, subFolderEntries...)

		} else {

			// check if the enty shall be ignored
			if skipFile(subEntryPath) {
				continue
			}

			// append entry
			entries = append(entries, subEntryPath)
		}

	}

	return entries, nil
}
