package fs

import (
	"os"
	"path/filepath"
	"sort"
	"strings"
	"sync"

	"github.com/photoprism/photoprism/pkg/fs/fastwalk"
)

// OriginalPaths lists default Originals search paths.
var OriginalPaths = []string{
	"/photoprism/storage/media/originals",
	"/photoprism/media/originals",
	"/photoprism/originals",
	"/srv/photoprism/storage/media/originals",
	"/srv/photoprism/media/originals",
	"/srv/photoprism/originals",
	"/opt/photoprism/storage/media/originals",
	"/opt/photoprism/media/originals",
	"/opt/photoprism/originals",
	"/media/originals",
	"/storage/originals",
	"/originals",
	"media/originals",
	"storage/originals",
	"photoprism/originals",
	"PhotoPrism/Originals",
	"photoprism/original",
	"PhotoPrism/Original",
	"pictures/originals",
	"Pictures/Originals",
	"pictures/original",
	"Pictures/Original",
	"photos/originals",
	"Photos/Originals",
	"photos/original",
	"Photos/Original",
	"originals",
	"Originals",
	"original",
	"Original",
	"pictures",
	"Pictures",
	"photos",
	"Photos",
	"images",
	"Images",
	"bilder",
	"Bilder",
	"fotos",
	"Fotos",
	"~/photoprism/originals",
	"~/PhotoPrism/Originals",
	"~/photoprism/original",
	"~/PhotoPrism/Original",
	"~/pictures/originals",
	"~/Pictures/Originals",
	"~/pictures/original",
	"~/Pictures/Original",
	"~/photos/originals",
	"~/Photos/Originals",
	"~/photos/original",
	"~/Photos/Original",
	"~/pictures",
	"~/Pictures",
	"~/photos",
	"~/Photos",
	"~/images",
	"~/Images",
	"~/bilder",
	"~/Bilder",
	"~/fotos",
	"~/Fotos",
	"/var/lib/photoprism/originals",
}

// ImportPaths lists default Import search paths.
var ImportPaths = []string{
	"/photoprism/storage/media/import",
	"/photoprism/media/import",
	"/photoprism/import",
	"/srv/photoprism/storage/media/import",
	"/srv/photoprism/media/import",
	"/srv/photoprism/import",
	"/opt/photoprism/storage/media/import",
	"/opt/photoprism/media/import",
	"/opt/photoprism/import",
	"/media/import",
	"/storage/import",
	"/import",
	"media/import",
	"storage/import",
	"photoprism/import",
	"PhotoPrism/Import",
	"pictures/import",
	"Pictures/Import",
	"photos/import",
	"Photos/Import",
	"import",
	"Import",
	"~/pictures/import",
	"~/Pictures/Import",
	"~/photoprism/import",
	"~/PhotoPrism/Import",
	"~/photos/import",
	"~/Photos/Import",
	"~/import",
	"~/Import",
	"/var/lib/photoprism/import",
}

// AssetPaths lists default asset paths.
var AssetPaths = []string{
	"/opt/photoprism/assets",
	"/photoprism/assets",
	"~/.photoprism/assets",
	"~/photoprism/assets",
	"photoprism/assets",
	"assets",
	"/var/lib/photoprism/assets",
}

// ModelsPaths lists default model lookup paths.
var ModelsPaths = []string{
	"/opt/photoprism/assets/models",
	"/photoprism/assets/models",
	"~/.photoprism/assets/models",
	"~/photoprism/assets/models",
	"photoprism/assets/models",
	"assets/models",
	"/var/lib/photoprism/assets/models",
}

// Dirs returns a slice of directories in a path, optional recursively and with symlinks.
//
// Warning: Following symlinks can make the result non-deterministic and hard to test!
func Dirs(root string, recursive bool, followLinks bool) (result []string, err error) {
	result = []string{}
	mutex := sync.Mutex{}

	// Ignore hidden folders as well as those listed in an optional ".ppignore" file.
	ignore := NewIgnoreList(PPIgnoreFilename, true, false)

	symlinks := make(map[string]bool)
	symlinksMutex := sync.Mutex{}

	// appendResult adds the relative path of a subdirectory to the results.
	appendResult := func(dir string) {
		mutex.Lock()
		defer mutex.Unlock()
		result = append(result, strings.Replace(dir, root, "", 1))
	}

	err = fastwalk.Walk(root, func(dir string, mode os.FileMode) error {
		if mode.IsDir() || mode == os.ModeSymlink && followLinks {
			// Skip if symlink does not point to existing directory.
			if mode == os.ModeSymlink {
				if info, statErr := os.Stat(dir); statErr != nil || !info.IsDir() {
					return filepath.SkipDir
				}
			}

			// Skip if directory should be ignored.
			if _ = ignore.Path(dir); ignore.Ignore(dir) {
				return filepath.SkipDir
			} else if FileExists(filepath.Join(dir, PPStorageFilename)) {
				return filepath.SkipDir
			}

			// Only add subdirectories.
			if dir != root {
				if !recursive {
					appendResult(dir)

					return filepath.SkipDir
				} else if mode != os.ModeSymlink {
					appendResult(dir)

					return nil
				} else if resolved, resolveErr := Resolve(dir); resolveErr == nil {
					symlinksMutex.Lock()
					defer symlinksMutex.Unlock()

					if _, ok := symlinks[resolved]; ok {
						return filepath.SkipDir
					} else {
						symlinks[resolved] = true
						appendResult(dir)
					}

					return fastwalk.ErrTraverseLink
				}
			}
		}

		return nil
	})

	sort.Strings(result)

	return result, err
}

// FindDir checks if any of the specified directories exist and returns the absolute path of the first directory found.
func FindDir(dirs []string) string {
	for _, dir := range dirs {
		absDir := Abs(dir)
		if PathExists(absDir) {
			return absDir
		}
	}

	return ""
}

// MkdirAll creates a directory including all parent directories that might not yet exist.
// No error is returned if the directory already exists.
func MkdirAll(dir string) error {
	return os.MkdirAll(dir, ModeDir)
}
