// Mgmt
// Copyright (C) 2013-2024+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.
//
// Additional permission under GNU GPL version 3 section 7
//
// If you modify this program, or any covered work, by linking or combining it
// with embedded mcl code and modules (and that the embedded mcl code and
// modules which link with this program, contain a copy of their source code in
// the authoritative form) containing parts covered by the terms of any other
// license, the licensors of this program grant you additional permission to
// convey the resulting work. Furthermore, the licensors of this program grant
// the original author, James Shubin, additional permission to update this
// additional permission if he deems it necessary to achieve the goals of this
// additional permission.

package util

import (
	"fmt"
	"io"
	"os"
	"path"

	"github.com/spf13/afero"
)

// FsTree returns a string representation of the file system tree similar to the
// well-known `tree` command.
func FsTree(fs afero.Fs, name string) (string, error) {
	if fs == nil {
		return "", fmt.Errorf("nil fs")
	}
	clean := path.Clean(name)
	str := clean + "/\n" // named dir
	if name == "" {
		str = ".\n" // relative dir
	}
	if name == "/" {
		str = "/\n" // root dir
	}
	s, err := stringify(fs, clean, []bool{})
	if err != nil {
		return "", err
	}
	str += s
	return str, nil
}

func stringify(fs afero.Fs, name string, indent []bool) (string, error) {
	str := ""
	dir, err := fs.Open(name)
	if err != nil {
		return "", err
	}

	fileinfo, err := dir.Readdir(-1)
	if err != nil && err != io.EOF {
		return "", err
	}
	for i, fi := range fileinfo {
		for _, last := range indent {
			if last {
				str += "    "
			} else {
				str += "│   "
			}
		}

		header := "├── "
		var last bool
		if i == len(fileinfo)-1 { // if last
			header = "└── "
			last = true
		}

		p := fi.Name()
		if fi.IsDir() {
			p += "/" // identify as a dir
		}
		str += fmt.Sprintf("%s%s\n", header, p)
		if fi.IsDir() {
			indented := append(indent, last)
			s, err := stringify(fs, path.Join(name, p), indented)
			if err != nil {
				return "", err // TODO: return partial tree?
			}
			str += s
		}
	}
	return str, nil
}

// CopyFs copies a dir from the srcFs to a dir on the dstFs. It expects that the
// dst will be either empty, or that the force flag will be set to true. If the
// dst has a different set of contents in the same location, the behaviour is
// currently undefined.
// TODO: this should be made more rsync like and robust!
func CopyFs(srcFs, dstFs afero.Fs, src, dst string, force bool, all bool) error {
	src = path.Join("/", src)
	dst = path.Join("/", dst)

	// TODO: clean this up with function that gets parent dir?
	src = path.Clean(src)
	parentDir, _ := path.Split(src)
	srcFsLen := len(parentDir)

	walkFn := func(name string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}
		perm := info.Mode() // get file permissions
		p := path.Join(dst, name[srcFsLen:])
		if info.IsDir() {
			if all {
				err = dstFs.MkdirAll(p, perm)
			} else {
				err = dstFs.Mkdir(p, perm)
			}
			if os.IsExist(err) && (name == "/" || force) {
				return nil
			}
			return err
		}

		data, err := afero.ReadFile(srcFs, name)
		if err != nil {
			return err
		}
		// create file
		return afero.WriteFile(dstFs, p, data, perm)
	}

	return afero.Walk(srcFs, src, walkFn)
}

// CopyFsContents copies a dir from the srcFs to a dir on the dstFs. It expects
// that the dst will be either empty, or that the force flag will be set to
// true. If the dst has a different set of contents in the same location, the
// behaviour is currently undefined. Notably, this does not copy the src dir
// itself, and only includes the contents. This is similar to `rsync dir/` vs.
// `rsync dir`. This function does the former, where as CopyFs does the latter.
// TODO: this should be made more rsync like and robust!
func CopyFsContents(srcFs afero.Fs, dstFs afero.Fs, src string, dst string, force bool) error {
	src = path.Join("/", src)
	dst = path.Join("/", dst)

	// TODO: clean this up with function that gets parent dir?
	src = path.Clean(src)
	parentDir, _ := path.Split(src)
	srcFsLen := len(parentDir)

	walkFn := func(name string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}
		perm := info.Mode() // get file permissions
		// basically, we want to go "up" one dir so we don't copy it...
		// this is like `rsync dir/` vs. `rsync dir`
		p, err := RemovePathPrefix(path.Join(dst, name[srcFsLen:]))
		if err != nil {
			return err
		}
		if info.IsDir() {
			err := dstFs.Mkdir(p, perm)
			if os.IsExist(err) && (p == "/" || force) {
				return nil
			}
			return err
		}

		data, err := afero.ReadFile(srcFs, name)
		if err != nil {
			return err
		}
		// create file
		return afero.WriteFile(dstFs, p, data, perm)
	}

	return afero.Walk(srcFs, src, walkFn)
}

// CopyFsToDisk performs exactly as CopyFs, except that the dst fs is our local
// disk os fs.
func CopyFsToDisk(srcFs afero.Fs, src, dst string, force bool) error {
	return CopyFs(srcFs, afero.NewOsFs(), src, dst, force, false)
}

// CopyDiskToFs performs exactly as CopyFs, except that the src fs is our local
// disk os fs.
func CopyDiskToFs(dstFs afero.Fs, src, dst string, force bool) error {
	return CopyFs(afero.NewOsFs(), dstFs, src, dst, force, false)
}

// CopyDiskToFsAll performs exactly as CopyDiskToFs, except that it allows you
// to specify the `all` argument which switches CopyFs to using MkdirAll instead
// of the regular Mkdir.
// TODO: This works around weird copying issues. POSIX is hard.
func CopyDiskToFsAll(dstFs afero.Fs, src, dst string, force, all bool) error {
	return CopyFs(afero.NewOsFs(), dstFs, src, dst, force, all)
}

// CopyFsContentsToDisk performs exactly as CopyFs, except that the dst fs is
// our local disk os fs, and we don't keep the source dir name, we only copy the
// contents.
func CopyFsContentsToDisk(srcFs afero.Fs, src, dst string, force bool) error {
	return CopyFsContents(srcFs, afero.NewOsFs(), src, dst, force)
}

// CopyDiskContentsToFs performs exactly as CopyFs, except that the src fs is
// our local disk os fs, and we don't keep the source dir name, we only copy the
// contents.
func CopyDiskContentsToFs(dstFs afero.Fs, src, dst string, force bool) error {
	return CopyFsContents(afero.NewOsFs(), dstFs, src, dst, force)
}
