// Package readonlyfs provides an immutable filesystem backed by owned bytes.
package readonlyfs

import (
	"bytes"
	"errors"
	"fmt"
	"io"
	"io/fs"
	"path"
	"sort"
	"time"
)

// FS is an immutable filesystem constructed from a map of file contents.
type FS struct {
	files map[string][]byte
	dirs  map[string][]fs.DirEntry
}

var _ fs.FS = (*FS)(nil)
var _ fs.ReadFileFS = (*FS)(nil)
var _ fs.ReadDirFS = (*FS)(nil)

// New constructs an immutable filesystem. File names must be valid io/fs
// paths and the root path may not be used as a file.
func New(files map[string][]byte) (*FS, error) {
	owned := make(map[string][]byte, len(files))
	directories := map[string]struct{}{".": {}}
	for name, data := range files {
		if name == "." || !fs.ValidPath(name) {
			return nil, fmt.Errorf("invalid file path %q", name)
		}
		owned[name] = append([]byte(nil), data...)
		for directory := path.Dir(name); directory != "."; directory = path.Dir(directory) {
			directories[directory] = struct{}{}
		}
	}
	for directory := range directories {
		if _, exists := owned[directory]; exists {
			return nil, fmt.Errorf("file path %q conflicts with an inferred directory", directory)
		}
	}

	dirs := make(map[string][]fs.DirEntry, len(directories))
	for directory := range directories {
		dirs[directory] = make([]fs.DirEntry, 0)
	}
	for directory := range directories {
		if directory == "." {
			continue
		}
		parent := path.Dir(directory)
		dirs[parent] = append(dirs[parent], dirEntry{name: path.Base(directory), dir: true})
	}
	for name, data := range owned {
		parent := path.Dir(name)
		dirs[parent] = append(dirs[parent], dirEntry{name: path.Base(name), size: int64(len(data))})
	}
	for directory := range dirs {
		sort.Slice(dirs[directory], func(left, right int) bool {
			return dirs[directory][left].Name() < dirs[directory][right].Name()
		})
	}
	return &FS{files: owned, dirs: dirs}, nil
}

func (f *FS) Open(name string) (fs.File, error) {
	if err := validOpenPath(name); err != nil {
		return nil, pathError("open", name, err)
	}
	if data, ok := f.files[name]; ok {
		return &file{reader: bytes.NewReader(data), info: fileInfo{name: path.Base(name), size: int64(len(data))}}, nil
	}
	if entries, ok := f.dirs[name]; ok {
		return &dir{name: path.Base(name), entries: entries}, nil
	}
	return nil, pathError("open", name, fs.ErrNotExist)
}

func (f *FS) ReadFile(name string) ([]byte, error) {
	if err := validOpenPath(name); err != nil {
		return nil, pathError("readfile", name, err)
	}
	data, ok := f.files[name]
	if !ok {
		if _, isDirectory := f.dirs[name]; isDirectory {
			return nil, pathError("readfile", name, errIsDirectory)
		}
		return nil, pathError("readfile", name, fs.ErrNotExist)
	}
	return append([]byte(nil), data...), nil
}

func (f *FS) ReadDir(name string) ([]fs.DirEntry, error) {
	if err := validOpenPath(name); err != nil {
		return nil, pathError("readdir", name, err)
	}
	entries, ok := f.dirs[name]
	if !ok {
		if _, isFile := f.files[name]; isFile {
			return nil, pathError("readdir", name, errNotDirectory)
		}
		return nil, pathError("readdir", name, fs.ErrNotExist)
	}
	return append([]fs.DirEntry(nil), entries...), nil
}

func validOpenPath(name string) error {
	if name == "." || fs.ValidPath(name) {
		return nil
	}
	return fs.ErrInvalid
}

func pathError(operation, name string, err error) error {
	return &fs.PathError{Op: operation, Path: name, Err: err}
}

var (
	errIsDirectory  = errors.New("is a directory")
	errNotDirectory = errors.New("not a directory")
)

type file struct {
	reader *bytes.Reader
	info   fileInfo
}

func (f *file) Stat() (fs.FileInfo, error) { return f.info, nil }
func (f *file) Read(data []byte) (int, error) {
	return f.reader.Read(data)
}
func (f *file) Close() error { return nil }

type dir struct {
	name    string
	offset  int
	entries []fs.DirEntry
}

func (d *dir) Stat() (fs.FileInfo, error) { return fileInfo{name: d.name, dir: true}, nil }
func (d *dir) Read([]byte) (int, error)   { return 0, errIsDirectory }
func (d *dir) Close() error               { return nil }
func (d *dir) ReadDir(count int) ([]fs.DirEntry, error) {
	if count <= 0 {
		entries := append([]fs.DirEntry(nil), d.entries[d.offset:]...)
		d.offset = len(d.entries)
		return entries, nil
	}
	if d.offset >= len(d.entries) {
		return nil, io.EOF
	}
	end := len(d.entries)
	if d.offset+count < end {
		end = d.offset + count
	}
	entries := append([]fs.DirEntry(nil), d.entries[d.offset:end]...)
	d.offset = end
	return entries, nil
}

type dirEntry struct {
	name string
	dir  bool
	size int64
}

func (e dirEntry) Name() string      { return e.name }
func (e dirEntry) IsDir() bool       { return e.dir }
func (e dirEntry) Type() fs.FileMode { return e.mode().Type() }
func (e dirEntry) Info() (fs.FileInfo, error) {
	return fileInfo{name: e.name, dir: e.dir, size: e.size}, nil
}
func (e dirEntry) mode() fs.FileMode {
	if e.dir {
		return fs.ModeDir | 0o555
	}
	return 0o444
}

type fileInfo struct {
	name string
	dir  bool
	size int64
}

func (i fileInfo) Name() string       { return i.name }
func (i fileInfo) Size() int64        { return i.size }
func (i fileInfo) Mode() fs.FileMode  { return dirEntry{dir: i.dir}.mode() }
func (i fileInfo) ModTime() time.Time { return time.Time{} }
func (i fileInfo) IsDir() bool        { return i.dir }
func (i fileInfo) Sys() any           { return nil }
