package filesystem

// |@@| C

import (
	"context"
	"fmt"
	"gardening/src/lib"
	erro "gardening/src/lib/error"
	"gardening/src/lib/operating_system"
	"os"
	path2 "path"
	"path/filepath"
)

type Filesystem struct {
	OperatingSystemInterface operating_system.OperatingSystemInterface
}

type FilesystemMod struct {
	Perm os.FileMode
}

func (this *Filesystem) ReadFile(ctx context.Context, name string) (string, error) {
	b, err := this.OperatingSystemInterface.ReadFile(ctx, name)
	return string(b), err
}

func (this *Filesystem) Remove(ctx context.Context, name string) error {
	return this.OperatingSystemInterface.Remove(ctx, name)
}

func (this *Filesystem) MustRemove(ctx context.Context, name string) {
	err := this.OperatingSystemInterface.Remove(ctx, name)
	if err != nil {
		panic(err)
	}
}

func (this *Filesystem) WriteFile(ctx context.Context, name string, data string, mod *FilesystemMod) error {
	perm := this.permFromMod(mod)

	err := this.OperatingSystemInterface.WriteFile(ctx, name, []byte(data), perm)
	if err != nil {
		return erro.N("Cannot write file").
			KVT("name", name, "filesystem:filepath").
			KVT("perm", perm.String())
	}
	return nil
}

func (this *Filesystem) MustWriteFile(ctx context.Context, name string, data string, mod *FilesystemMod) {
	err := this.WriteFile(ctx, name, data, mod)
	lib.Poe(err)
}

func (this *Filesystem) permFromMod(mod *FilesystemMod) os.FileMode {
	if mod == nil || mod.Perm == 0 {
		return 0644
	}
	return mod.Perm
}

func (this *Filesystem) MustReadFile(ctx context.Context, name string) string {
	b, err := this.ReadFile(ctx, name)
	if err != nil {
		lib.Poe(err)
	}
	return b
}

func (this *Filesystem) ReadDir(ctx context.Context, dir string) ([]string, error) {
	files, err := this.OperatingSystemInterface.ReadDir(ctx, dir)
	if err != nil {
		return nil, err
	}

	var filePaths []string
	for _, file := range files {
		filePaths = append(filePaths, fmt.Sprintf("%s/%s", dir, file.Name()))
	}
	return filePaths, nil
}

func (this *Filesystem) MustReadDir(ctx context.Context, dir string) []string {
	files, err := this.ReadDir(ctx, dir)
	if err != nil {
		panic(err)
	}
	return files
}

func (this *Filesystem) ShouldExist(ctx context.Context, name string) error {
	if _, err := this.OperatingSystemInterface.Stat(ctx, name); this.OperatingSystemInterface.IsNotExist(ctx, err) {
		return erro.N("File does not exist").KVT("name", name, "filesystem:filepath")
	}
	return nil
}

func (this *Filesystem) IsExist(ctx context.Context, name string) bool {
	return !this.IsNotExist(ctx, name)
}

func (this *Filesystem) IsNotExist(ctx context.Context, name string) bool {
	if _, err := this.OperatingSystemInterface.Stat(ctx, name); this.OperatingSystemInterface.IsNotExist(ctx, err) {
		return true
	}
	return false
}

func (this *Filesystem) Copy(ctx context.Context, src string, dest string) error {
	b, err := this.OperatingSystemInterface.ReadFile(ctx, src)
	if err != nil {
		return erro.W("Cannot copy", err).KV("src", src).KV("dest", dest)
	}

	srcInfo, err := this.Stat(ctx, src)
	if err != nil {
		return erro.W("Cannot stat source file", err).KV("src", src)
	}

	srcPerm := srcInfo.Mode().Perm()

	// If destination file exists and is read-only, make it writable first
	if destInfo, err := this.Stat(ctx, dest); err == nil {
		if destInfo.Mode().Perm()&0200 == 0 { // No write permission
			err = this.OperatingSystemInterface.Chmod(ctx, dest, 0644)
			if err != nil {
				return erro.W("Cannot make destination writable", err).KV("dest", dest)
			}
		}
	}

	err = this.OperatingSystemInterface.WriteFile(ctx, dest, b, 0644)
	if err != nil {
		return erro.W("Cannot copy", err).KV("dest", dest)
	}

	err = this.OperatingSystemInterface.Chmod(ctx, dest, srcPerm)
	if err != nil {
		return erro.W("Cannot set permissions", err).KV("dest", dest).KV("perm", srcPerm.String())
	}
	return nil
}

func (this *Filesystem) MustCopy(ctx context.Context, src string, dest string) {
	b, err := this.OperatingSystemInterface.ReadFile(ctx, src)
	lib.WPoe(err, erro.W("Cannot read file", err).KV("filepath", src))

	srcInfo, err := this.Stat(ctx, src)
	lib.WPoe(err, erro.W("Cannot stat source file", err).KV("src", src))

	srcPerm := srcInfo.Mode().Perm()

	// If destination file exists and is read-only, make it writable first
	if destInfo, err := this.Stat(ctx, dest); err == nil {
		if destInfo.Mode().Perm()&0200 == 0 { // No write permission
			err = this.OperatingSystemInterface.Chmod(ctx, dest, 0644)
			lib.WPoe(err, erro.W("Cannot make destination writable", err).KV("dest", dest))
		}
	}

	err = this.OperatingSystemInterface.WriteFile(ctx, dest, b, 0644)
	lib.WPoe(err, erro.W("Cannot write file", err).KV("filepath", dest))

	err = this.OperatingSystemInterface.Chmod(ctx, dest, srcPerm)
	lib.WPoe(err, erro.W("Cannot set permissions", err).KV("dest", dest).KV("perm", srcPerm.String()))
}

func (this *Filesystem) dirCheck(ctx context.Context, dir string) {
	if path2.Ext(dir) != "" {
		panic(erro.N("Invalid dir").KV("dir", dir))
	}
}

func (this *Filesystem) MkdirAll(ctx context.Context, dir string) error {
	return this.OperatingSystemInterface.MkdirAll(ctx, dir, 0744)
}

func (this *Filesystem) MkdirAllWithPerm(ctx context.Context, dir string, perm os.FileMode) error {
	return this.OperatingSystemInterface.MkdirAll(ctx, dir, perm)
}

func (this *Filesystem) MustMkdirAll(ctx context.Context, dir string) {
	if this.IsExist(ctx, dir) {
		return
	}
	this.dirCheck(ctx, dir)
	err := this.MkdirAll(ctx, dir)
	lib.WPoe(err, erro.W("Cannot mkdir all", err).KV("dir", dir).KV("perm", "0744"))
}

func (this *Filesystem) MustMkdirAllForFile(ctx context.Context, filePath string) {
	dir := filepath.Dir(filePath)
	err := this.MkdirAllForFile(ctx, filePath)
	lib.WPoe(err, erro.W("Cannot mkdir all for file", err).KV("dir", dir).KV("filepath", filePath))
}

func (this *Filesystem) MkdirAllForFile(ctx context.Context, filePath string) error {
	dir := filepath.Dir(filePath)
	if this.IsExist(ctx, dir) {
		return nil
	}
	this.dirCheck(ctx, dir)
	err := this.OperatingSystemInterface.MkdirAll(ctx, dir, 0744)
	return err
}

func (this *Filesystem) IsDir(ctx context.Context, path string) bool {
	fileInfo, err := this.OperatingSystemInterface.Stat(ctx, path)
	if err != nil {
		lib.Poe(erro.W("Cannot stat the file", err).KV("path", path))
	}
	if fileInfo.IsDir() {
		return true
	}
	return false
}

func (this *Filesystem) RemoveAll(ctx context.Context, path string) error {
	return this.OperatingSystemInterface.RemoveAll(ctx, path)
}

func (this *Filesystem) MustRemoveAll(ctx context.Context, path string) {
	err := this.OperatingSystemInterface.RemoveAll(ctx, path)
	if err != nil {
		lib.Poe(err)
	}
}

func (this *Filesystem) MustExist(ctx context.Context, name string) {
	exist := this.IsExist(ctx, name)
	if !exist {
		panic(erro.N("File does not exist").KVT("name", name, "filesystem:filepath"))
	}
}

func (this *Filesystem) Rename(ctx context.Context, oldpath, newpath string) error {
	return this.OperatingSystemInterface.Rename(ctx, oldpath, newpath)
}

func (this *Filesystem) MustRename(ctx context.Context, oldpath, newpath string) {
	err := this.Rename(ctx, oldpath, newpath)
	if err != nil {
		panic(err)
	}
}

func (this *Filesystem) Walk(ctx context.Context, root string, fn filepath.WalkFunc) error {
	return this.OperatingSystemInterface.Walk(ctx, root, fn)
}

func (this *Filesystem) MustCopyDir(ctx context.Context, src string, dest string) {
	err := this.CopyDir(ctx, src, dest)
	lib.WPoe(err, erro.W("Cannot copy dir", err).KV("src", src).KV("dest", dest))
}

func (this *Filesystem) CopyDir(ctx context.Context, src string, dest string) error {
	entries, err := this.ReadDir(ctx, src)
	if err != nil {
		return erro.W("Cannot read dir", err).KV("src", src)
	}

	srcInfo, err := this.Stat(ctx, src)
	if err != nil {
		return erro.W("Cannot stat source dir", err).KV("src", src)
	}

	srcPerm := srcInfo.Mode().Perm()
	if err := this.MkdirAllWithPerm(ctx, dest, srcPerm); err != nil {
		return erro.W("Cannot mkdir all", err).KV("dest", dest).KV("perm", srcPerm.String())
	}

	for _, entry := range entries {
		fileInfo, err := this.OperatingSystemInterface.Stat(ctx, entry)
		if err != nil {
			return erro.W("Cannot stat", err).KV("entry", entry)
		}

		srcPath := filepath.Join(src, fileInfo.Name())
		destPath := filepath.Join(dest, fileInfo.Name())

		if fileInfo.IsDir() {
			if err := this.CopyDir(ctx, srcPath, destPath); err != nil {
				return erro.W("Cannot copy dir", err).KV("src", srcPath).KV("dest", destPath)
			}
		} else {
			if err := this.Copy(ctx, srcPath, destPath); err != nil {
				return erro.W("Cannot copy file", err).KV("src", srcPath).KV("dest", destPath)
			}
		}
	}
	return nil
}

func (this *Filesystem) Stat(ctx context.Context, path string) (os.FileInfo, error) {
	return this.OperatingSystemInterface.Stat(ctx, path)
}
