package fs

import (
	"os"
	"path"
	"strings"
)

const separator = "/"

type FsFactory func(f File) (Filesystem, error)

var SupportedFactories = map[string]FsFactory{
	".zip": func(f File) (Filesystem, error) {
		return NewArchive(f, f.Size(), &Zip{}), nil
	},
	".rar": func(f File) (Filesystem, error) {
		return NewArchive(f, f.Size(), &Rar{}), nil
	},
	".7z": func(f File) (Filesystem, error) {
		return NewArchive(f, f.Size(), &SevenZip{}), nil
	},
}

type storage struct {
	factories map[string]FsFactory

	files       map[string]File
	filesystems map[string]Filesystem
	children    map[string]map[string]File
}

func newStorage(factories map[string]FsFactory) *storage {
	return &storage{
		files:       make(map[string]File),
		children:    make(map[string]map[string]File),
		filesystems: make(map[string]Filesystem),
		factories:   factories,
	}
}

func (s *storage) Clear() {
	s.files = make(map[string]File)
	s.children = make(map[string]map[string]File)
	s.filesystems = make(map[string]Filesystem)

	s.Add(&Dir{}, "/")
}

func (s *storage) Has(path string) bool {
	path = clean(path)

	f := s.files[path]
	if f != nil {
		return true
	}

	if f, _ := s.getFileFromFs(path); f != nil {
		return true
	}

	return false
}

func (s *storage) AddFS(fs Filesystem, p string) error {
	p = clean(p)
	if s.Has(p) {
		if dir, err := s.Get(p); err == nil {
			if !dir.IsDir() {
				return os.ErrExist
			}
		}

		return nil
	}

	s.filesystems[p] = fs
	return s.createParent(p, &Dir{})
}

func (s *storage) Add(f File, p string) error {
	p = clean(p)
	if s.Has(p) {
		if dir, err := s.Get(p); err == nil {
			if !dir.IsDir() {
				return os.ErrExist
			}
		}

		return nil
	}

	ext := path.Ext(p)
	if ffs := s.factories[ext]; ffs != nil {
		fs, err := ffs(f)
		if err != nil {
			return err
		}

		s.filesystems[p] = fs
	} else {
		s.files[p] = f
	}

	return s.createParent(p, f)
}

func (s *storage) createParent(p string, f File) error {
	base, filename := path.Split(p)
	base = clean(base)

	if err := s.Add(&Dir{}, base); err != nil {
		return err
	}

	if _, ok := s.children[base]; !ok {
		s.children[base] = make(map[string]File)
	}

	if filename != "" {
		s.children[base][filename] = f
	}

	return nil
}

func (s *storage) Children(path string) (map[string]File, error) {
	path = clean(path)

	files, err := s.getDirFromFs(path)
	if err == nil {
		return files, nil
	}

	if !os.IsNotExist(err) {
		return nil, err
	}

	l := make(map[string]File)
	for n, f := range s.children[path] {
		l[n] = f
	}

	return l, nil
}

func (s *storage) Get(path string) (File, error) {
	path = clean(path)
	if !s.Has(path) {
		return nil, os.ErrNotExist
	}

	file, ok := s.files[path]
	if ok {
		return file, nil
	}

	return s.getFileFromFs(path)
}

func (s *storage) getFileFromFs(p string) (File, error) {
	for fsp, fs := range s.filesystems {
		if strings.HasPrefix(p, fsp) {
			return fs.Open(separator + strings.TrimPrefix(p, fsp))
		}
	}

	return nil, os.ErrNotExist
}

func (s *storage) getDirFromFs(p string) (map[string]File, error) {
	for fsp, fs := range s.filesystems {
		if strings.HasPrefix(p, fsp) {
			path := strings.TrimPrefix(p, fsp)
			return fs.ReadDir(path)
		}
	}

	return nil, os.ErrNotExist
}

func clean(p string) string {
	return path.Clean(separator + strings.ReplaceAll(p, "\\", "/"))
}
