// Copyright 2019 The LevelDB-Go and Pebble Authors. All rights reserved. Use
// of this source code is governed by a BSD-style license that can be found in
// the LICENSE file.

package vfs

import (
	"io"
	"sort"

	"github.com/cockroachdb/errors/oserror"
)

type cloneOpts struct {
	skip    func(string) bool
	sync    bool
	tryLink bool
}

// A CloneOption configures the behavior of Clone.
type CloneOption func(*cloneOpts)

// CloneSkip configures Clone to skip files for which the provided function
// returns true when passed the file's path.
func CloneSkip(fn func(string) bool) CloneOption {
	return func(co *cloneOpts) { co.skip = fn }
}

// CloneSync configures Clone to sync files and directories.
var CloneSync CloneOption = func(o *cloneOpts) { o.sync = true }

// CloneTryLink configures Clone to link files to the destination if the source and
// destination filesystems are the same. If the source and destination
// filesystems are not the same or the filesystem does not support linking, then
// Clone falls back to copying.
var CloneTryLink CloneOption = func(o *cloneOpts) { o.tryLink = true }

// Clone recursively copies a directory structure from srcFS to dstFS. srcPath
// specifies the path in srcFS to copy from and must be compatible with the
// srcFS path format. dstDir is the target directory in dstFS and must be
// compatible with the dstFS path format. Returns (true,nil) on a successful
// copy, (false,nil) if srcPath does not exist, and (false,err) if an error
// occurred.
func Clone(srcFS, dstFS FS, srcPath, dstPath string, opts ...CloneOption) (bool, error) {
	var o cloneOpts
	for _, opt := range opts {
		opt(&o)
	}

	srcFile, err := srcFS.Open(srcPath)
	if err != nil {
		if oserror.IsNotExist(err) {
			// Ignore non-existent errors. Those will translate into non-existent
			// files in the destination filesystem.
			return false, nil
		}
		return false, err
	}
	defer srcFile.Close()

	stat, err := srcFile.Stat()
	if err != nil {
		return false, err
	}

	if stat.IsDir() {
		if err := dstFS.MkdirAll(dstPath, 0755); err != nil {
			return false, err
		}
		list, err := srcFS.List(srcPath)
		if err != nil {
			return false, err
		}
		// Sort the paths so we get deterministic test output.
		sort.Strings(list)
		for _, name := range list {
			if o.skip != nil && o.skip(srcFS.PathJoin(srcPath, name)) {
				continue
			}
			_, err := Clone(srcFS, dstFS, srcFS.PathJoin(srcPath, name), dstFS.PathJoin(dstPath, name), opts...)
			if err != nil {
				return false, err
			}
		}

		if o.sync {
			dir, err := dstFS.OpenDir(dstPath)
			if err != nil {
				return false, err
			}
			if err := dir.Sync(); err != nil {
				return false, err
			}
			if err := dir.Close(); err != nil {
				return false, err
			}
		}

		return true, nil
	}

	// If the source and destination filesystems are the same and the user
	// specified they'd prefer to link if possible, try to use a hardlink,
	// falling back to copying if it fails.
	if srcFS == dstFS && o.tryLink {
		if err := LinkOrCopy(srcFS, srcPath, dstPath); oserror.IsNotExist(err) {
			// Clone's semantics are such that it returns (false,nil) if the
			// source does not exist.
			return false, nil
		} else if err != nil {
			return false, err
		} else {
			return true, nil
		}
	}

	data, err := io.ReadAll(srcFile)
	if err != nil {
		return false, err
	}
	dstFile, err := dstFS.Create(dstPath, WriteCategoryUnspecified)
	if err != nil {
		return false, err
	}
	if _, err = dstFile.Write(data); err != nil {
		return false, err
	}
	if o.sync {
		if err := dstFile.Sync(); err != nil {
			return false, err
		}
	}
	if err := dstFile.Close(); err != nil {
		return false, err
	}
	return true, nil
}
