package main

import (
	"io/fs"
	"os"
	"path/filepath"
	"time"

	"github.com/fxamacker/cbor/v2"
)

const (
	Dirs       = 0x001
	Files      = 0x002
	Drives     = 0x004
	NoSymLinks = 0x008
	AllEntries = Dirs | Files | Drives
	TypeMask   = 0x00f

	Readable       = 0x010
	Writable       = 0x020
	Executable     = 0x040
	PermissionMask = 0x070

	Modified = 0x080
	Hidden   = 0x100
	System   = 0x200

	AccessMask = 0x3F0

	AllDirs        = 0x400
	CaseSensitive  = 0x800
	NoDot          = 0x2000
	NoDotDot       = 0x4000
	NoDotAndDotDot = NoDot | NoDotDot

	NoFilter = -1
)

const (
	NoIteratorFlags = 0x0
	FollowSymlinks  = 0x1
	Subdirectories  = 0x2
)

type find struct {
	Directory     string
	NameFilters   []string
	FileFilters   int
	IteratorFlags int
}

type finddata struct {
	Type    string
	Id      int
	Path    string
	Size    int64
	Mode    fs.FileMode
	IsDir   bool
	ModTime time.Time
}

func processFind(cmd command, out chan<- []byte) {
	// Register a cancelable context so a "cancel" command for this Id can stop
	// the walk early instead of streaming the whole tree to a client that has
	// already stopped listening.
	ctx := registerCancelable(cmd.Id)
	defer unregisterCancelable(cmd.Id)

	writer := NewChannelWriter(out)

	// The device root has no real directory to walk on Windows (there is no single "/"); browsing
	// it should list the drive roots (C:/, D:/, ...). systemDrives() is empty off Windows, where
	// "/" is a normal directory, so this only affects Windows.
	if dir := cmd.Find.Directory; dir == "" || dir == "/" || dir == "\\" {
		if drives := systemDrives(); len(drives) > 0 {
			wantDirs := cmd.Find.FileFilters == NoFilter || cmd.Find.FileFilters&Dirs != 0
			for _, drive := range drives {
				if ctx.Err() != nil {
					break
				}
				if !wantDirs {
					continue
				}
				result, err := cbor.Marshal(finddata{
					Type:    "finddata",
					Id:      cmd.Id,
					Path:    drive,
					Size:    0,
					Mode:    fs.ModeDir | 0555,
					IsDir:   true,
					ModTime: time.Now(),
				})
				if err == nil {
					writer.Write(result)
				}
			}
			result, _ := cbor.Marshal(finddata{Type: "findend", Id: cmd.Id})
			writer.Write(result)
			writer.Flush()
			return
		}
	}

	fileSystem := os.DirFS(cmd.Find.Directory)

	fs.WalkDir(fileSystem, ".", func(path string, dirEntry fs.DirEntry, err error) error {
		// Abort as soon as the client cancels the search.
		if ctx.Err() != nil {
			return ctx.Err()
		}

		if err != nil {
			return nil
		}

		if path == "." {
			return nil
		}

		var skipdir error = nil

		fullPath := filepath.Join(cmd.Find.Directory,path)


		if dirEntry.IsDir() && path != "." && (cmd.Find.IteratorFlags&Subdirectories == 0)  {
			skipdir = filepath.SkipDir
		}

		info, err := dirEntry.Info()
		if err != nil {
			return nil
		}

		if info.Mode()&fs.ModeSymlink != 0 {
			linkedpath, err := filepath.EvalSymlinks(fullPath)
			if err == nil {
				newinfo, err := os.Stat(linkedpath)
				if err == nil {
					info = newinfo
				}
			}
		}

		// TODO: fs.WalkDir does not follow symlinks, so we need to implement this ourselves
		//if (cmd.Find.IteratorFlags&FollowSymlinks == 0) && (info.Mode()&fs.ModeSymlink != 0) && info.IsDir() {
		//	skipdir = filepath.SkipDir
		//}

		if cmd.Find.FileFilters != NoFilter {
			if (cmd.Find.FileFilters&Drives) == 0 && ((info.Mode()&fs.ModeDevice != 0) || (info.Mode()&fs.ModeCharDevice != 0)) {
				return skipdir
			}
			if (cmd.Find.FileFilters&Dirs) == 0 && info.IsDir() {
				return skipdir
			}
			if (cmd.Find.FileFilters&Files) == 0 && !info.IsDir() {
				return skipdir
			}
			if (cmd.Find.FileFilters&NoSymLinks) != 0 && info.Mode()&fs.ModeSymlink != 0 {
				return skipdir
			}
			if (cmd.Find.FileFilters&Readable) != 0 && !isReadable(fullPath) {
				return skipdir
			}
			if (cmd.Find.FileFilters&Writable) != 0 && !isWritable(fullPath) {
				return skipdir
			}
			if (cmd.Find.FileFilters&Executable) != 0 && !isExecutable(fullPath) {
				return skipdir
			}
		}
		hasMatch := true

		if len(cmd.Find.NameFilters) > 0 {
			hasMatch = false
			for _, filter := range cmd.Find.NameFilters {
				match, _ := filepath.Match(filter, path);
				if match {
					hasMatch = true
					break
				}
			}
		}

		if !hasMatch {
			return skipdir
		}

		result, err := cbor.Marshal(finddata{
			Type:    "finddata",
			Id:      cmd.Id,
			Path:    filepath.Join(cmd.Find.Directory,path),
			Size:    info.Size(),
			Mode:    info.Mode(),
			IsDir:   info.IsDir(),
			ModTime: info.ModTime(),
		})

		if err == nil {
			writer.Write(result)
		}

		return skipdir
	})

	result, _ := cbor.Marshal(finddata{
		Type: "findend",
		Id:   cmd.Id,
	})

	writer.Write(result)
	writer.Flush()
}
