package copy

import (
	"context"
	"io"
	"io/fs"
	"os"

	"golang.org/x/sync/semaphore"
)

// Options specifies optional actions on copying.
type Options struct {

	// OnSymlink can specify what to do on symlink
	OnSymlink func(src string) SymlinkAction

	// OnDirExists can specify what to do when there is a directory already existing in destination.
	OnDirExists func(src, dest string) DirExistsAction

	// OnErr lets called decide whether or not to continue on particular copy error.
	OnError func(src, dest string, err error) error

	// Skip can specify which files should be skipped
	Skip func(srcinfo os.FileInfo, src, dest string) (bool, error)

	// RenameDestination can specify the destination file or dir name if needed to rename.
	RenameDestination func(src, dest string) (string, error)

	// Specials includes special files to be copied. default false.
	Specials bool

	// AddPermission to every entities,
	// NO MORE THAN 0777
	// @OBSOLETE
	// Use `PermissionControl = AddPermission(perm)` instead
	AddPermission os.FileMode

	// PermissionControl can preserve or even add permission to
	// every entries, for example
	//
	//		opt.PermissionControl = AddPermission(0222)
	//
	// See permission_control.go for more detail.
	PermissionControl PermissionControlFunc

	// Sync file after copy.
	// Useful in case when file must be on the disk
	// (in case crash happens, for example),
	// at the expense of some performance penalty
	Sync bool

	// Preserve the atime and the mtime of the entries.
	// On linux we can preserve only up to 1 millisecond accuracy.
	PreserveTimes bool

	// Preserve the uid and the gid of all entries.
	PreserveOwner bool

	// The byte size of the buffer to use for copying files.
	// If zero, the internal default buffer of 32KB is used.
	// See https://golang.org/pkg/io/#CopyBuffer for more information.
	CopyBufferSize uint

	// If you want to add some limitation on reading src file,
	// you can wrap the src and provide new reader,
	// such as `RateLimitReader` in the test case.
	WrapReader func(src io.Reader) io.Reader

	// If given, copy.Copy refers to this fs.FS instead of the OS filesystem.
	// e.g., You can use embed.FS to copy files from embedded filesystem.
	FS fs.FS

	// NumOfWorkers represents the number of workers used for
	// concurrent copying contents of directories.
	// If 0 or 1, it does not use goroutine for copying directories.
	// Please refer to https://pkg.go.dev/golang.org/x/sync/semaphore for more details.
	NumOfWorkers int64

	// PreferConcurrent is a function to determine whether or not
	// to use goroutine for copying contents of directories.
	// If PreferConcurrent is nil, which is default, it does concurrent
	// copying for all directories.
	// If NumOfWorkers is 0 or 1, this function will be ignored.
	PreferConcurrent func(srcdir, destdir string) (bool, error)

	// Internal use only
	intent intent
}

type intent struct {
	src  string
	dest string
	sem  *semaphore.Weighted
	ctx  context.Context
}

// SymlinkAction represents what to do on symlink.
type SymlinkAction int

const (
	// Deep creates hard-copy of contents.
	Deep SymlinkAction = iota
	// Shallow creates new symlink to the dest of symlink.
	Shallow
	// Skip does nothing with symlink.
	Skip
)

// DirExistsAction represents what to do on dest dir.
type DirExistsAction int

const (
	// Merge preserves or overwrites existing files under the dir (default behavior).
	Merge DirExistsAction = iota
	// Replace deletes all contents under the dir and copy src files.
	Replace
	// Untouchable does nothing for the dir, and leaves it as it is.
	Untouchable
)

// getDefaultOptions provides default options,
// which would be modified by usage-side.
func getDefaultOptions(src, dest string) Options {
	return Options{
		OnSymlink: func(string) SymlinkAction {
			return Shallow // Do shallow copy
		},
		OnDirExists:       nil,                // Default behavior is "Merge".
		OnError:           nil,                // Default is "accept error"
		Skip:              nil,                // Do not skip anything
		AddPermission:     0,                  // Add nothing
		PermissionControl: PerservePermission, // Just preserve permission
		Sync:              false,              // Do not sync
		Specials:          false,              // Do not copy special files
		PreserveTimes:     false,              // Do not preserve the modification time
		CopyBufferSize:    0,                  // Do not specify, use default bufsize (32*1024)
		WrapReader:        nil,                // Do not wrap src files, use them as they are.
		intent:            intent{src, dest, nil, nil},
	}
}

// assureOptions struct, should be called only once.
// All optional values MUST NOT BE nil/zero after assured.
func assureOptions(src, dest string, opts ...Options) Options {
	defopt := getDefaultOptions(src, dest)
	if len(opts) == 0 {
		return defopt
	}
	if opts[0].OnSymlink == nil {
		opts[0].OnSymlink = defopt.OnSymlink
	}
	if opts[0].Skip == nil {
		opts[0].Skip = defopt.Skip
	}
	if opts[0].AddPermission > 0 {
		opts[0].PermissionControl = AddPermission(opts[0].AddPermission)
	} else if opts[0].PermissionControl == nil {
		opts[0].PermissionControl = PerservePermission
	}
	opts[0].intent.src = defopt.intent.src
	opts[0].intent.dest = defopt.intent.dest
	return opts[0]
}

func shouldCopyDirectoryConcurrent(opt Options, srcdir, destdir string) (bool, error) {
	if opt.NumOfWorkers <= 1 {
		return false, nil
	}
	if opt.PreferConcurrent == nil {
		return true, nil
	}
	return opt.PreferConcurrent(srcdir, destdir)
}
