package files

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

// serialFile implements Node, and reads from a path on the OS filesystem.
// No more than one file will be opened at a time.
type serialFile struct {
	path   string
	files  []os.FileInfo
	stat   os.FileInfo
	filter *Filter
}

type serialIterator struct {
	files  []os.FileInfo
	path   string
	filter *Filter

	curName string
	curFile Node

	err error
}

// NewSerialFile takes a filepath, a bool specifying if hidden files should be included,
// and a fileInfo and returns a Node representing file, directory or special file.
func NewSerialFile(path string, includeHidden bool, stat os.FileInfo) (Node, error) {
	filter, err := NewFilter("", nil, includeHidden)
	if err != nil {
		return nil, err
	}
	return NewSerialFileWithFilter(path, filter, stat)
}

// NewSerialFileWith takes a filepath, a filter for determining which files should be
// operated upon if the filepath is a directory, and a fileInfo and returns a
// Node representing file, directory or special file.
func NewSerialFileWithFilter(path string, filter *Filter, stat os.FileInfo) (Node, error) {
	switch mode := stat.Mode(); {
	case mode.IsRegular():
		file, err := os.Open(path)
		if err != nil {
			return nil, err
		}
		return NewReaderPathFile(path, file, stat)
	case mode.IsDir():
		// for directories, stat all of the contents first, so we know what files to
		// open when Entries() is called
		entries, err := os.ReadDir(path)
		if err != nil {
			return nil, err
		}
		contents := make([]fs.FileInfo, 0, len(entries))
		for _, entry := range entries {
			content, err := entry.Info()
			if err != nil {
				return nil, err
			}
			contents = append(contents, content)
		}
		return &serialFile{path, contents, stat, filter}, nil
	case mode&os.ModeSymlink != 0:
		target, err := os.Readlink(path)
		if err != nil {
			return nil, err
		}
		return NewLinkFile(target, stat), nil
	default:
		return nil, fmt.Errorf("unrecognized file type for %s: %s", path, mode.String())
	}
}

func (it *serialIterator) Name() string {
	return it.curName
}

func (it *serialIterator) Node() Node {
	return it.curFile
}

func (it *serialIterator) Next() bool {
	// if there aren't any files left in the root directory, we're done
	if len(it.files) == 0 {
		return false
	}

	stat := it.files[0]
	it.files = it.files[1:]
	for it.filter.ShouldExclude(stat) {
		if len(it.files) == 0 {
			return false
		}

		stat = it.files[0]
		it.files = it.files[1:]
	}

	// open the next file
	filePath := filepath.ToSlash(filepath.Join(it.path, stat.Name()))

	// recursively call the constructor on the next file
	// if it's a regular file, we will open it as a ReaderFile
	// if it's a directory, files in it will be opened serially
	sf, err := NewSerialFileWithFilter(filePath, it.filter, stat)
	if err != nil {
		it.err = err
		return false
	}

	it.curName = stat.Name()
	it.curFile = sf
	return true
}

func (it *serialIterator) Err() error {
	return it.err
}

func (f *serialFile) Entries() DirIterator {
	return &serialIterator{
		path:   f.path,
		files:  f.files,
		filter: f.filter,
	}
}

func (f *serialFile) Close() error {
	return nil
}

func (f *serialFile) Stat() os.FileInfo {
	return f.stat
}

func (f *serialFile) Size() (int64, error) {
	if !f.stat.IsDir() {
		// something went terribly, terribly wrong
		return 0, errors.New("serialFile is not a directory")
	}

	var du int64
	err := filepath.Walk(f.path, func(p string, fi os.FileInfo, err error) error {
		if err != nil || fi == nil {
			return err
		}

		if f.filter.ShouldExclude(fi) {
			if fi.Mode().IsDir() {
				return filepath.SkipDir
			}
		} else if fi.Mode().IsRegular() {
			du += fi.Size()
		}

		return nil
	})

	return du, err
}

func (f *serialFile) Mode() os.FileMode {
	return f.stat.Mode()
}

func (f *serialFile) ModTime() time.Time {
	return f.stat.ModTime()
}

var (
	_ Directory   = &serialFile{}
	_ DirIterator = &serialIterator{}
)
