package rardecode

import (
	"fmt"
	"io"
	"io/fs"
	"path"
	"slices"
	"strings"
	"time"
)

type fileInfo struct {
	h *fileBlockHeader
}

func (f fileInfo) Name() string       { return path.Base(f.h.Name) }
func (f fileInfo) Size() int64        { return f.h.UnPackedSize }
func (f fileInfo) Mode() fs.FileMode  { return f.h.Mode() }
func (f fileInfo) ModTime() time.Time { return f.h.ModificationTime }
func (f fileInfo) IsDir() bool        { return f.h.IsDir }
func (f fileInfo) Sys() any           { return nil }

type dirEntry struct {
	h *fileBlockHeader
}

func (d dirEntry) Name() string               { return path.Base(d.h.Name) }
func (d dirEntry) IsDir() bool                { return d.h.IsDir }
func (d dirEntry) Type() fs.FileMode          { return d.h.Mode().Type() }
func (d dirEntry) Info() (fs.FileInfo, error) { return fileInfo(d), nil }

type dummyDirInfo struct {
	name string
}

func (d dummyDirInfo) Name() string       { return d.name }
func (d dummyDirInfo) Size() int64        { return 0 }
func (d dummyDirInfo) Mode() fs.FileMode  { return 0777 | fs.ModeDir }
func (d dummyDirInfo) ModTime() time.Time { return time.Time{} }
func (d dummyDirInfo) IsDir() bool        { return true }
func (d dummyDirInfo) Sys() any           { return nil }

func newDummyDirInfo(name string) dummyDirInfo {
	return dummyDirInfo{name: path.Base(name)}
}

type dummyDirEntry struct {
	name string
}

func (d dummyDirEntry) Name() string               { return d.name }
func (d dummyDirEntry) IsDir() bool                { return true }
func (d dummyDirEntry) Type() fs.FileMode          { return fs.ModeDir }
func (d dummyDirEntry) Sys() any                   { return nil }
func (d dummyDirEntry) Info() (fs.FileInfo, error) { return dummyDirInfo(d), nil }

func newDummyDirEntry(name string) dummyDirEntry {
	return dummyDirEntry{name: path.Base(name)}
}

type dirFile struct {
	name  string
	info  fs.FileInfo
	files []fs.DirEntry
	index int
}

func (df *dirFile) Read(p []byte) (int, error) { return 0, io.EOF }
func (df *dirFile) ReadByte() (byte, error)    { return 0, io.EOF }
func (df *dirFile) Stat() (fs.FileInfo, error) { return df.info, nil }
func (df *dirFile) Close() error               { return nil }

func (d *dirFile) ReadDir(n int) ([]fs.DirEntry, error) {
	if n <= 0 {
		return d.files, nil
	}
	l := d.files[d.index:]
	d.index += len(l)
	return l, nil
}

type fsNode struct {
	name   string
	blocks *fileBlockList
	files  []*fsNode
}

func (n *fsNode) isDir() bool {
	return n.blocks == nil || n.blocks.isDir()
}

func (n *fsNode) hasFileHash() bool {
	return n.blocks != nil && n.blocks.hasFileHash()
}

func (n *fsNode) firstBlock() *fileBlockHeader {
	if n.blocks == nil {
		return nil
	}
	return n.blocks.firstBlock()
}

func (n *fsNode) fileInfo() fs.FileInfo {
	h := n.firstBlock()
	if h == nil {
		return newDummyDirInfo(n.name)
	}
	return fileInfo{h: h}
}

func (n *fsNode) dirEntry() fs.DirEntry {
	h := n.firstBlock()
	if h == nil {
		return newDummyDirEntry(n.name)
	}
	return dirEntry{h: h}
}

func (n *fsNode) dirEntryList() []fs.DirEntry {
	list := make([]fs.DirEntry, len(n.files))
	for i := range list {
		list[i] = n.files[i].dirEntry()
	}
	slices.SortFunc(list, func(a, b fs.DirEntry) int {
		return strings.Compare(a.Name(), b.Name())
	})
	return list
}

// RarFS implements the fs.FS interface for accessing files in a rar archive.
type RarFS struct {
	vm    *volumeManager
	ftree map[string]*fsNode
}

func (rfs *RarFS) openArchiveFile(blocks *fileBlockList) (fs.File, error) {
	return rfs.vm.openArchiveFile(blocks)
}

// Open opens the named file.
func (rfs *RarFS) Open(name string) (fs.File, error) {
	if !fs.ValidPath(name) {
		return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrInvalid}
	}
	node := rfs.ftree[name]
	if node == nil {
		return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist}
	}
	if node.isDir() {
		return &dirFile{
			name:  name,
			info:  node.fileInfo(),
			files: node.dirEntryList(),
		}, nil
	}
	f, err := rfs.openArchiveFile(node.blocks)
	if err != nil {
		return nil, &fs.PathError{Op: "open", Path: name, Err: err}
	}
	return f, nil
}

// ReadDir reads the named directory and returns a list of directory entries sorted by filename.
func (rfs *RarFS) ReadDir(name string) ([]fs.DirEntry, error) {
	if !fs.ValidPath(name) {
		return nil, &fs.PathError{Op: "readdir", Path: name, Err: fs.ErrInvalid}
	}
	node := rfs.ftree[name]
	if node == nil {
		return nil, &fs.PathError{Op: "readdir", Path: name, Err: fs.ErrNotExist}
	}
	if !node.isDir() {
		return nil, &fs.PathError{Op: "readdir", Path: name, Err: fs.ErrInvalid}
	}
	return node.dirEntryList(), nil
}

// ReadFile reads the named file from the file system fs and returns its contents.
func (rfs *RarFS) ReadFile(name string) ([]byte, error) {
	if !fs.ValidPath(name) {
		return nil, &fs.PathError{Op: "readfile", Path: name, Err: fs.ErrInvalid}
	}
	node := rfs.ftree[name]
	if node == nil {
		return nil, &fs.PathError{Op: "readfile", Path: name, Err: fs.ErrNotExist}
	}
	if node.isDir() {
		return []byte{}, nil
	}

	f, err := rfs.openArchiveFile(node.blocks)
	if err != nil {
		return nil, &fs.PathError{Op: "readfile", Path: name, Err: err}
	}
	defer f.Close()

	h := node.firstBlock()
	if h.UnKnownSize {
		return io.ReadAll(f)
	}
	buf := make([]byte, h.UnPackedSize)
	_, err = io.ReadFull(f, buf)
	return buf, err
}

/*
func (rfs *RarFS) Check(name string) error {
	if !fs.ValidPath(name) {
		return &fs.PathError{Op: "check", Path: name, Err: fs.ErrInvalid}
	}
	node := rfs.ftree[name]
	if node == nil {
		return &fs.PathError{Op: "check", Path: name, Err: fs.ErrNotExist}
	}
	if node.isDir() {
		return &fs.PathError{Op: "check", Path: name, Err: fs.ErrInvalid}
	}
	if !node.hasFileHash() {
		return nil
	}
	f, err := rfs.openArchiveFile(node.blocks)
	if err != nil {
		return &fs.PathError{Op: "check", Path: name, Err: err}
	}
	_, err = io.Copy(io.Discard, f)
	if err != nil {
		return &fs.PathError{Op: "check", Path: name, Err: err}
	}
	return nil
}
*/

// Stat returns a FileInfo describing the named file from the filesystem.
func (rfs *RarFS) Stat(name string) (fs.FileInfo, error) {
	if !fs.ValidPath(name) {
		return nil, &fs.PathError{Op: "stat", Path: name, Err: fs.ErrInvalid}
	}
	node := rfs.ftree[name]
	if node == nil {
		return nil, &fs.PathError{Op: "stat", Path: name, Err: fs.ErrNotExist}
	}
	return node.fileInfo(), nil
}

// Sub returns an FS corresponding to the subtree rooted at fsys's dir.
func (rfs *RarFS) Sub(dir string) (fs.FS, error) {
	if dir == "." {
		return rfs, nil
	}
	if !fs.ValidPath(dir) {
		return nil, &fs.PathError{Op: "sub", Path: dir, Err: fs.ErrInvalid}
	}
	node := rfs.ftree[dir]
	if node == nil {
		return nil, &fs.PathError{Op: "sub", Path: dir, Err: fs.ErrNotExist}
	}
	if !node.isDir() {
		return nil, &fs.PathError{Op: "sub", Path: dir, Err: fs.ErrInvalid}
	}
	newFS := &RarFS{
		ftree: map[string]*fsNode{
			".": {name: ".", files: node.files},
		},
		vm: rfs.vm,
	}
	prefix := dir + "/"
	for k, v := range rfs.ftree {
		if after, ok := strings.CutPrefix(k, prefix); ok {
			newFS.ftree[after] = v
		}
	}
	return newFS, nil
}

func listFileBlocks(name string, opts []Option) (*volumeManager, []*fileBlockList, error) {
	options := getOptions(opts)
	if options.openCheck {
		options.skipCheck = false
	}
	v, err := openVolume(name, options)
	if err != nil {
		return nil, nil, err
	}
	pr := newPackedFileReader(v, options)
	defer v.Close()

	fileBlocks := []*fileBlockList{}
	for {
		blocks, err := pr.nextFile()
		if err != nil {
			if err == io.EOF {
				return v.vm, fileBlocks, nil
			}
			return nil, nil, err
		}
		fileBlocks = append(fileBlocks, blocks)
		if options.openCheck && blocks.hasFileHash() {
			f, err := pr.newArchiveFile(blocks)
			if err != nil {
				return nil, nil, err
			}
			_, err = io.Copy(io.Discard, f)
			if err != nil {
				return nil, nil, err
			}
		}
	}
}

func OpenFS(name string, opts ...Option) (*RarFS, error) {
	vm, fileBlocks, err := listFileBlocks(name, opts)
	if err != nil {
		return nil, err
	}

	rfs := &RarFS{
		ftree: map[string]*fsNode{},
		vm:    vm,
	}
	for _, blocks := range fileBlocks {
		h := blocks.firstBlock()
		fname := strings.TrimPrefix(path.Clean(h.Name), "/")
		if !fs.ValidPath(fname) {
			return nil, fmt.Errorf("rardecode: archived file has invalid path: %s", fname)
		}
		node := rfs.ftree[fname]
		if node != nil {
			if node.blocks == nil || node.firstBlock().Version < h.Version {
				node.blocks = blocks
			}
			continue
		}
		rfs.ftree[fname] = &fsNode{blocks: blocks}
		prev := rfs.ftree[fname]
		// add parent file nodes
		for fname != "." {
			fname = path.Dir(fname)
			node = rfs.ftree[fname]
			if node != nil {
				node.files = append(node.files, prev)
				break
			}
			rfs.ftree[fname] = &fsNode{
				name:  fname,
				files: []*fsNode{prev},
			}
			prev = rfs.ftree[fname]
		}
	}
	return rfs, nil
}
