package main

import (
	"fmt"
	"io"
	"os"
	"path/filepath"
	"slices"
	"strconv"
	"strings"

	"github.com/djherbis/times"
)

type ProgressWriter struct {
	writer io.Writer
	nums   chan<- int64
}

func NewProgressWriter(writer io.Writer, nums chan<- int64) *ProgressWriter {
	return &ProgressWriter{
		writer: writer,
		nums:   nums,
	}
}

func (progressWriter *ProgressWriter) Write(b []byte) (int, error) {
	n, err := progressWriter.writer.Write(b)
	progressWriter.nums <- int64(n)
	return n, err
}

func copySize(srcs []string) (int64, error) {
	var total int64

	for _, src := range srcs {
		_, err := os.Lstat(src)
		if os.IsNotExist(err) {
			return total, fmt.Errorf("src does not exist: %q", src)
		}

		err = filepath.Walk(src, func(_ string, info os.FileInfo, err error) error {
			if err != nil {
				return fmt.Errorf("walk: %w", err)
			}
			total += info.Size()
			return nil
		})
		if err != nil {
			return total, err
		}
	}

	return total, nil
}

func copyFile(src, dst string, preserve []string, info os.FileInfo, nums chan<- int64, errs chan<- error) {
	r, err := os.Open(src)
	if err != nil {
		errs <- err
		return
	}
	defer r.Close()

	var dstMode os.FileMode = 0o666
	if slices.Contains(preserve, "mode") {
		dstMode = info.Mode()
	}
	w, err := os.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, dstMode)
	if err != nil {
		errs <- err
		return
	}

	if _, err := io.Copy(NewProgressWriter(w, nums), r); err != nil {
		errs <- err
		w.Close()
		if err = os.Remove(dst); err != nil {
			errs <- err
		}
		return
	}

	if err := w.Close(); err != nil {
		errs <- err
		if err = os.Remove(dst); err != nil {
			errs <- err
		}
		return
	}

	if slices.Contains(preserve, "timestamps") {
		atime := times.Get(info).AccessTime()
		mtime := info.ModTime()
		if err := os.Chtimes(dst, atime, mtime); err != nil {
			errs <- err
		}
	}
}

func copyAll(srcs []string, dstDir string, preserve []string) (nums chan int64, errs chan error) {
	nums = make(chan int64, 1024)
	errs = make(chan error, 1024)

	go func() {
		dirInfos := make(map[string]os.FileInfo)

		for _, src := range srcs {
			file := filepath.Base(src)
			dst := filepath.Join(dstDir, file)

			if lstat, err := os.Lstat(dst); err == nil {
				ext := getFileExtension(lstat)
				basename := file[:len(file)-len(ext)]
				var newPath string
				for i := 1; !os.IsNotExist(err); i++ {
					file = strings.ReplaceAll(gOpts.dupfilefmt, "%f", basename+ext)
					file = strings.ReplaceAll(file, "%b", basename)
					file = strings.ReplaceAll(file, "%e", ext)
					file = strings.ReplaceAll(file, "%n", strconv.Itoa(i))
					newPath = filepath.Join(dstDir, file)
					_, err = os.Lstat(newPath)
				}
				dst = newPath
			}

			if rel, err := filepath.Rel(src, dst); err == nil && rel != "." && filepath.IsLocal(rel) {
				errs <- fmt.Errorf("cannot copy %s into a subdirectory of itself", src)
				continue
			}

			err := filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
				if err != nil {
					errs <- fmt.Errorf("walk: %w", err)
					return nil
				}
				rel, err := filepath.Rel(src, path)
				if err != nil {
					errs <- fmt.Errorf("relative: %w", err)
					return nil
				}
				newPath := filepath.Join(dst, rel)
				switch {
				case info.IsDir():
					dstMode := os.ModePerm
					if slices.Contains(preserve, "mode") {
						dstMode = info.Mode()
					}
					if err := os.MkdirAll(newPath, dstMode); err != nil {
						errs <- fmt.Errorf("mkdir: %w", err)
					}
					if slices.Contains(preserve, "timestamps") {
						dirInfos[newPath] = info
					}
					nums <- info.Size()
				case info.Mode()&os.ModeSymlink != 0:
					if rlink, err := os.Readlink(path); err != nil {
						errs <- fmt.Errorf("symlink: %w", err)
					} else {
						if err := os.Symlink(rlink, newPath); err != nil {
							errs <- fmt.Errorf("symlink: %w", err)
						}
					}
					nums <- info.Size()
				default:
					copyFile(path, newPath, preserve, info, nums, errs)
				}
				return nil
			})
			if err != nil {
				errs <- fmt.Errorf("walk: %w", err)
			}
		}

		for path, info := range dirInfos {
			atime := times.Get(info).AccessTime()
			mtime := info.ModTime()
			if err := os.Chtimes(path, atime, mtime); err != nil {
				errs <- err
			}
		}

		close(errs)
	}()

	return nums, errs
}
