// Copyright (c) 2014 The fileutil 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 fileutil collects some file utility functions.
package fileutil // import "modernc.org/fileutil"

import (
	"bufio"
	"fmt"
	"io"
	"io/fs"
	"os"
	"path/filepath"
	"runtime"
	"strconv"
	"sync"
	"time"
)

// GoMFile is a concurrent access safe version of MFile.
type GoMFile struct {
	mfile *MFile
	mutex sync.Mutex
}

// NewGoMFile return a newly created GoMFile.
func NewGoMFile(fname string, flag int, perm os.FileMode, delta_ns int64) (m *GoMFile, err error) {
	m = &GoMFile{}
	if m.mfile, err = NewMFile(fname, flag, perm, delta_ns); err != nil {
		m = nil
	}
	return
}

func (m *GoMFile) File() (file *os.File, err error) {
	m.mutex.Lock()
	defer m.mutex.Unlock()
	return m.mfile.File()
}

func (m *GoMFile) SetChanged() {
	m.mutex.Lock()
	defer m.mutex.Unlock()
	m.mfile.SetChanged()
}

func (m *GoMFile) SetHandler(h MFileHandler) {
	m.mutex.Lock()
	defer m.mutex.Unlock()
	m.mfile.SetHandler(h)
}

// MFileHandler resolves modifications of File.
// Possible File context is expected to be a part of the handler's closure.
type MFileHandler func(*os.File) error

// MFile represents an os.File with a guard/handler on change/modification.
// Example use case is an app with a configuration file which can be modified at any time
// and have to be reloaded in such event prior to performing something configurable by that
// file. The checks are made only on access to the MFile file by
// File() and a time threshold/hysteresis value can be chosen on creating a new MFile.
type MFile struct {
	file    *os.File
	handler MFileHandler
	t0      int64
	delta   int64
	ctime   int64
}

// NewMFile returns a newly created MFile or Error if any.
// The fname, flag and perm parameters have the same meaning as in os.Open.
// For meaning of the delta_ns parameter please see the (m *MFile) File() docs.
func NewMFile(fname string, flag int, perm os.FileMode, delta_ns int64) (m *MFile, err error) {
	m = &MFile{}
	m.t0 = time.Now().UnixNano()
	if m.file, err = os.OpenFile(fname, flag, perm); err != nil {
		return
	}

	var fi os.FileInfo
	if fi, err = m.file.Stat(); err != nil {
		return
	}

	m.ctime = fi.ModTime().UnixNano()
	m.delta = delta_ns
	runtime.SetFinalizer(m, func(m *MFile) {
		m.file.Close()
	})
	return
}

// SetChanged forces next File() to unconditionally handle modification of the wrapped os.File.
func (m *MFile) SetChanged() {
	m.ctime = -1
}

// SetHandler sets a function to be invoked when modification of MFile is to be processed.
func (m *MFile) SetHandler(h MFileHandler) {
	m.handler = h
}

// File returns an os.File from MFile. If time elapsed between the last invocation of this function
// and now is at least delta_ns ns (a parameter of NewMFile) then the file is checked for
// change/modification. For delta_ns == 0 the modification is checked w/o getting os.Time().
// If a change is detected a handler is invoked on the MFile file.
// Any of these steps can produce an Error. If that happens the function returns nil, Error.
func (m *MFile) File() (file *os.File, err error) {
	var now int64

	mustCheck := m.delta == 0
	if !mustCheck {
		now = time.Now().UnixNano()
		mustCheck = now-m.t0 > m.delta
	}

	if mustCheck { // check interval reached
		var fi os.FileInfo
		if fi, err = m.file.Stat(); err != nil {
			return
		}

		if fi.ModTime().UnixNano() != m.ctime { // modification detected
			if m.handler == nil {
				return nil, fmt.Errorf("no handler set for modified file %q", m.file.Name())
			}
			if err = m.handler(m.file); err != nil {
				return
			}

			m.ctime = fi.ModTime().UnixNano()
		}
		m.t0 = now
	}

	return m.file, nil
}

// Read reads buf from r. It will either fill the full buf or fail.
// It wraps the functionality of an io.Reader which may return less bytes than requested,
// but may block if not all data are ready for the io.Reader.
func Read(r io.Reader, buf []byte) (err error) {
	have := 0
	remain := len(buf)
	got := 0
	for remain > 0 {
		if got, err = r.Read(buf[have:]); err != nil {
			return
		}

		remain -= got
		have += got
	}
	return
}

// "os" and/or "syscall" extensions

// FadviseAdvice is used by Fadvise.
type FadviseAdvice int

// FAdviseAdvice values.
const (
	// $ grep FADV /usr/include/bits/fcntl.h
	POSIX_FADV_NORMAL     FadviseAdvice = iota // No further special treatment.
	POSIX_FADV_RANDOM                          // Expect random page references.
	POSIX_FADV_SEQUENTIAL                      // Expect sequential page references.
	POSIX_FADV_WILLNEED                        // Will need these pages.
	POSIX_FADV_DONTNEED                        // Don't need these pages.
	POSIX_FADV_NOREUSE                         // Data will be accessed once.
)

// TempFile creates a new temporary file in the directory dir with a name
// ending with suffix, basename starting with prefix, opens the file for
// reading and writing, and returns the resulting *os.File.  If dir is the
// empty string, TempFile uses the default directory for temporary files (see
// os.TempDir).  Multiple programs calling TempFile simultaneously will not
// choose the same file.  The caller can use f.Name() to find the pathname of
// the file.  It is the caller's responsibility to remove the file when no
// longer needed.
//
// NOTE: This function differs from ioutil.TempFile.
func TempFile(dir, prefix, suffix string) (f *os.File, err error) {
	if dir == "" {
		dir = os.TempDir()
	}

	nconflict := 0
	for i := 0; i < 10000; i++ {
		name := filepath.Join(dir, prefix+nextInfix()+suffix)
		f, err = os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600)
		if os.IsExist(err) {
			if nconflict++; nconflict > 10 {
				randmu.Lock()
				rand = reseed()
				randmu.Unlock()
			}
			continue
		}
		break
	}
	return
}

// Random number state.
// We generate random temporary file names so that there's a good
// chance the file doesn't exist yet - keeps the number of tries in
// TempFile to a minimum.
var rand uint32
var randmu sync.Mutex

func reseed() uint32 {
	return uint32(time.Now().UnixNano() + int64(os.Getpid()))
}

func nextInfix() string {
	randmu.Lock()
	r := rand
	if r == 0 {
		r = reseed()
	}
	r = r*1664525 + 1013904223 // constants from Numerical Recipes
	rand = r
	randmu.Unlock()
	return strconv.Itoa(int(1e9 + r%1e9))[1:]
}

// CopyFile copies src in fsys, to dest in the OS file system, preserving
// permissions and times where/when possible. If canOverwrite is not nil, it is
// consulted whether a destination file can be overwritten. If canOverwrite is
// nil then destination is overwritten if permissions allow that, otherwise the
// function fails.
func CopyFile(fsys fs.FS, dst, src string, canOverwrite func(fn string, fi os.FileInfo) bool) (n int64, rerr error) {
	dstDir := filepath.Dir(dst)
	di, err := os.Stat(dstDir)
	switch {
	case err != nil:
		if !os.IsNotExist(err) {
			return 0, err
		}

		if err := os.MkdirAll(dstDir, 0770); err != nil {
			return 0, err
		}
	case err == nil:
		if !di.IsDir() {
			return 0, fmt.Errorf("cannot create directory, file exists: %s", dst)
		}
	}

	s, err := fsys.Open(src)
	if err != nil {
		return 0, err
	}

	defer s.Close()

	si, err := s.Stat()
	if err != nil {
		return 0, err
	}

	if si.IsDir() {
		return 0, fmt.Errorf("cannot copy a directory: %s", src)
	}

	di, err = os.Stat(dst)
	switch {
	case err != nil && !os.IsNotExist(err):
		return 0, err
	case err == nil:
		if di.IsDir() {
			return 0, fmt.Errorf("cannot overwite a directory: %s", dst)
		}

		if canOverwrite != nil && !canOverwrite(dst, di) {
			return 0, fmt.Errorf("cannot overwite: %s", dst)
		}
	}

	r := bufio.NewReader(s)
	d, err := os.Create(dst)

	defer func() {
		if err := d.Close(); err != nil && rerr == nil {
			rerr = err
			return
		}

		if err := os.Chmod(dst, si.Mode()); err != nil && rerr == nil {
			rerr = err
			return
		}

		if err := os.Chtimes(dst, si.ModTime(), si.ModTime()); err != nil && rerr == nil {
			rerr = err
			return
		}
	}()

	w := bufio.NewWriter(d)

	defer func() {
		if err := w.Flush(); err != nil && rerr == nil {
			rerr = err
		}
	}()

	return io.Copy(w, r)
}

// CopyDir recursively copies src in fsys to dest in the OS file system,
// preserving permissions and times where/when possible. If canOverwrite is not
// nil, it is consulted whether a destination file can be overwritten. If
// canOverwrite is nil then destination is overwritten if permissions allow
// that, otherwise the function fails.
func CopyDir(fsys fs.FS, dst, src string, canOverwrite func(fn string, fi os.FileInfo) bool) (files int, bytes int64, rerr error) {
	s, err := fsys.Open(src)
	if err != nil {
		return 0, 0, err
	}

	si, err := s.Stat()
	if err != nil {
		return 0, 0, err
	}

	if err := s.Close(); err != nil {
		return 0, 0, err
	}

	if !si.IsDir() {
		return 0, 0, fmt.Errorf("cannot copy a file: %s", src)
	}

	return files, bytes, fs.WalkDir(fsys, src, func(path string, info fs.DirEntry, err error) error {
		if err != nil {
			return err
		}

		rel, err := filepath.Rel(src, path)
		if err != nil {
			return err
		}

		if info.IsDir() {
			return os.MkdirAll(filepath.Join(dst, rel), 0770)
		}

		n, err := CopyFile(fsys, filepath.Join(dst, rel), path, canOverwrite)
		if err != nil {
			return err
		}

		files++
		bytes += n
		return nil
	})
}
