package fileset

import (
	"fmt"
	"io/fs"
	"os"
	"path/filepath"
)

// FileSet facilitates fast recursive file listing of a path.
// It optionally takes into account ignore rules through the [Ignorer] interface.
type FileSet struct {
	root   string
	ignore Ignorer
}

// New returns a [FileSet] for the given root path.
func New(root string) *FileSet {
	return &FileSet{
		root:   filepath.Clean(root),
		ignore: nopIgnorer{},
	}
}

// Ignorer returns the [FileSet]'s current ignorer.
func (w *FileSet) Ignorer() Ignorer {
	return w.ignore
}

// SetIgnorer sets the [Ignorer] interface for this [FileSet].
func (w *FileSet) SetIgnorer(ignore Ignorer) {
	w.ignore = ignore
}

// Return root for fileset.
func (w *FileSet) Root() string {
	return w.root
}

// Return all tracked files for Repo
func (w *FileSet) All() ([]File, error) {
	return w.recursiveListFiles()
}

// Recursively traverses dir in a depth first manner and returns a list of all files
// that are being tracked in the FileSet (ie not being ignored for matching one of the
// patterns in w.ignore)
func (w *FileSet) recursiveListFiles() (fileList []File, err error) {
	err = filepath.WalkDir(w.root, func(path string, d fs.DirEntry, err error) error {
		if err != nil {
			return err
		}

		relPath, err := filepath.Rel(w.root, path)
		if err != nil {
			return err
		}

		// skip symlinks
		info, err := d.Info()
		if err != nil {
			return err
		}
		if info.Mode()&os.ModeSymlink != 0 {
			return nil
		}

		if d.IsDir() {
			ign, err := w.ignore.IgnoreDirectory(relPath)
			if err != nil {
				return fmt.Errorf("cannot check if %s should be ignored: %w", relPath, err)
			}
			if ign {
				return filepath.SkipDir
			}
			return nil
		}

		ign, err := w.ignore.IgnoreFile(relPath)
		if err != nil {
			return fmt.Errorf("cannot check if %s should be ignored: %w", relPath, err)
		}
		if ign {
			return nil
		}

		fileList = append(fileList, File{d, path, relPath})
		return nil
	})
	return
}