// 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 (
	"fmt"
	"io"
	"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:]
}
