// Copyright (c) 2015-2016 The btcsuite developers
// Copyright (c) 2016 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.

// This file is part of the ffldb package rather than the ffldb_test package as
// it is part of the whitebox testing.

package ffldb

import (
	"errors"
	"io"
	"sync"
)

// Errors used for the mock file.
var (
	// errMockFileClosed is used to indicate a mock file is closed.
	errMockFileClosed = errors.New("file closed")

	// errInvalidOffset is used to indicate an offset that is out of range
	// for the file was provided.
	errInvalidOffset = errors.New("invalid offset")

	// errSyncFail is used to indicate simulated sync failure.
	errSyncFail = errors.New("simulated sync failure")
)

// mockFile implements the filer interface and used in order to force failures
// the database code related to reading and writing from the flat block files.
// A maxSize of -1 is unlimited.
type mockFile struct {
	sync.RWMutex
	maxSize      int64
	data         []byte
	forceSyncErr bool
	closed       bool
}

// Close closes the mock file without releasing any data associated with it.
// This allows it to be "reopened" without losing the data.
//
// This is part of the filer implementation.
func (f *mockFile) Close() error {
	f.Lock()
	defer f.Unlock()

	if f.closed {
		return errMockFileClosed
	}
	f.closed = true
	return nil
}

// ReadAt reads len(b) bytes from the mock file starting at byte offset off. It
// returns the number of bytes read and the error, if any.  ReadAt always
// returns a non-nil error when n < len(b). At end of file, that error is
// io.EOF.
//
// This is part of the filer implementation.
func (f *mockFile) ReadAt(b []byte, off int64) (int, error) {
	f.RLock()
	defer f.RUnlock()

	if f.closed {
		return 0, errMockFileClosed
	}
	maxSize := int64(len(f.data))
	if f.maxSize > -1 && maxSize > f.maxSize {
		maxSize = f.maxSize
	}
	if off < 0 || off > maxSize {
		return 0, errInvalidOffset
	}

	// Limit to the max size field, if set.
	numToRead := int64(len(b))
	endOffset := off + numToRead
	if endOffset > maxSize {
		numToRead = maxSize - off
	}

	copy(b, f.data[off:off+numToRead])
	if numToRead < int64(len(b)) {
		return int(numToRead), io.EOF
	}
	return int(numToRead), nil
}

// Truncate changes the size of the mock file.
//
// This is part of the filer implementation.
func (f *mockFile) Truncate(size int64) error {
	f.Lock()
	defer f.Unlock()

	if f.closed {
		return errMockFileClosed
	}
	maxSize := int64(len(f.data))
	if f.maxSize > -1 && maxSize > f.maxSize {
		maxSize = f.maxSize
	}
	if size > maxSize {
		return errInvalidOffset
	}

	f.data = f.data[:size]
	return nil
}

// Write writes len(b) bytes to the mock file. It returns the number of bytes
// written and an error, if any.  Write returns a non-nil error any time
// n != len(b).
//
// This is part of the filer implementation.
func (f *mockFile) WriteAt(b []byte, off int64) (int, error) {
	f.Lock()
	defer f.Unlock()

	if f.closed {
		return 0, errMockFileClosed
	}
	maxSize := f.maxSize
	if maxSize < 0 {
		maxSize = 100 * 1024 // 100KiB
	}
	if off < 0 || off > maxSize {
		return 0, errInvalidOffset
	}

	// Limit to the max size field, if set, and grow the slice if needed.
	numToWrite := int64(len(b))
	if off+numToWrite > maxSize {
		numToWrite = maxSize - off
	}
	if off+numToWrite > int64(len(f.data)) {
		newData := make([]byte, off+numToWrite)
		copy(newData, f.data)
		f.data = newData
	}

	copy(f.data[off:], b[:numToWrite])
	if numToWrite < int64(len(b)) {
		return int(numToWrite), io.EOF
	}
	return int(numToWrite), nil
}

// Sync doesn't do anything for mock files.  However, it will return an error if
// the mock file's forceSyncErr flag is set.
//
// This is part of the filer implementation.
func (f *mockFile) Sync() error {
	if f.forceSyncErr {
		return errSyncFail
	}

	return nil
}

// Ensure the mockFile type implements the filer interface.
var _ filer = (*mockFile)(nil)
