// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.

package gfsnotify

import (
	"context"
	"github.com/gogf/gf/v2/errors/gcode"
	"github.com/gogf/gf/v2/errors/gerror"

	"github.com/gogf/gf/v2/container/glist"
	"github.com/gogf/gf/v2/internal/intlog"
)

// watchLoop starts the loop for event listening from underlying inotify monitor.
func (w *Watcher) watchLoop() {
	go func() {
		for {
			select {
			// Close event.
			case <-w.closeChan:
				return

			// Event listening.
			case ev := <-w.watcher.Events:
				// Filter the repeated event in custom duration.
				_, err := w.cache.SetIfNotExist(
					context.Background(),
					ev.String(),
					func(ctx context.Context) (value interface{}, err error) {
						w.events.Push(&Event{
							event:   ev,
							Path:    ev.Name,
							Op:      Op(ev.Op),
							Watcher: w,
						})
						return struct{}{}, nil
					}, repeatEventFilterDuration,
				)
				if err != nil {
					intlog.Errorf(context.TODO(), `%+v`, err)
				}

			case err := <-w.watcher.Errors:
				intlog.Errorf(context.TODO(), `%+v`, err)
			}
		}
	}()
}

// eventLoop is the core event handler.
func (w *Watcher) eventLoop() {
	go func() {
		for {
			if v := w.events.Pop(); v != nil {
				event := v.(*Event)
				// If there's no any callback of this path, it removes it from monitor.
				callbacks := w.getCallbacks(event.Path)
				if len(callbacks) == 0 {
					_ = w.watcher.Remove(event.Path)
					continue
				}
				switch {
				case event.IsRemove():
					// It should check again the existence of the path.
					// It adds it back to the monitor if it still exists.
					if fileExists(event.Path) {
						// It adds the path back to monitor.
						// We need no worry about the repeat adding.
						if err := w.watcher.Add(event.Path); err != nil {
							intlog.Errorf(context.TODO(), `%+v`, err)
						} else {
							intlog.Printf(context.TODO(), "fake remove event, watcher re-adds monitor for: %s", event.Path)
						}
						// Change the event to RENAME, which means it renames itself to its origin name.
						event.Op = RENAME
					}

				case event.IsRename():
					// It should check again the existence of the path.
					// It adds it back to the monitor if it still exists.
					// Especially Some editors might do RENAME and then CHMOD when it's editing file.
					if fileExists(event.Path) {
						// It might lost the monitoring for the path, so we add the path back to monitor.
						// We need no worry about the repeat adding.
						if err := w.watcher.Add(event.Path); err != nil {
							intlog.Errorf(context.TODO(), `%+v`, err)
						} else {
							intlog.Printf(context.TODO(), "fake rename event, watcher re-adds monitor for: %s", event.Path)
						}
						// Change the event to CHMOD.
						event.Op = CHMOD
					}

				case event.IsCreate():
					// =========================================
					// Note that it here just adds the path to monitor without any callback registering,
					// because its parent already has the callbacks.
					// =========================================
					if fileIsDir(event.Path) {
						// If it's a folder, it then does adding recursively to monitor.
						for _, subPath := range fileAllDirs(event.Path) {
							if fileIsDir(subPath) {
								if err := w.watcher.Add(subPath); err != nil {
									intlog.Errorf(context.TODO(), `%+v`, err)
								} else {
									intlog.Printf(context.TODO(), "folder creation event, watcher adds monitor for: %s", subPath)
								}
							}
						}
					} else {
						// If it's a file, it directly adds it to monitor.
						if err := w.watcher.Add(event.Path); err != nil {
							intlog.Errorf(context.TODO(), `%+v`, err)
						} else {
							intlog.Printf(context.TODO(), "file creation event, watcher adds monitor for: %s", event.Path)
						}
					}
				}
				// Calling the callbacks in order.
				for _, callback := range callbacks {
					go func(callback *Callback) {
						defer func() {
							if err := recover(); err != nil {
								switch err {
								case callbackExitEventPanicStr:
									w.RemoveCallback(callback.Id)
								default:
									if e, ok := err.(error); ok {
										panic(gerror.WrapCode(gcode.CodeInternalPanic, e))
									}
									panic(err)
								}
							}
						}()
						callback.Func(event)
					}(callback)
				}
			} else {
				break
			}
		}
	}()
}

// getCallbacks searches and returns all callbacks with given `path`.
// It also searches its parents for callbacks if they're recursive.
func (w *Watcher) getCallbacks(path string) (callbacks []*Callback) {
	// Firstly add the callbacks of itself.
	if v := w.callbacks.Get(path); v != nil {
		for _, v := range v.(*glist.List).FrontAll() {
			callback := v.(*Callback)
			callbacks = append(callbacks, callback)
		}
	}
	// Secondly searches its direct parent for callbacks.
	// It is special handling here, which is the different between `recursive` and `not recursive` logic
	// for direct parent folder of `path` that events are from.
	dirPath := fileDir(path)
	if v := w.callbacks.Get(dirPath); v != nil {
		for _, v := range v.(*glist.List).FrontAll() {
			callback := v.(*Callback)
			callbacks = append(callbacks, callback)
		}
	}
	// Lastly searches all the parents of directory of `path` recursively for callbacks.
	for {
		parentDirPath := fileDir(dirPath)
		if parentDirPath == dirPath {
			break
		}
		if v := w.callbacks.Get(parentDirPath); v != nil {
			for _, v := range v.(*glist.List).FrontAll() {
				callback := v.(*Callback)
				if callback.recursive {
					callbacks = append(callbacks, callback)
				}
			}
		}
		dirPath = parentDirPath
	}
	return
}
