// Copyright 2015 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package statfs

import (
	"context"
	"os"
	"sync"

	"github.com/jacobsa/fuse"
	"github.com/jacobsa/fuse/fuseops"
	"github.com/jacobsa/fuse/fuseutil"
)

// A file system that allows orchestrating canned responses to statfs ops, for
// testng out OS-specific statfs behavior.
//
// The file system allows opening and writing to any name that is a child of
// the root inode, and keeps track of the most recent write size delivered by
// the kernel (in order to test statfs response block size effects on write
// size, if any).
//
// Safe for concurrent access.
type FS interface {
	fuseutil.FileSystem

	// Set the canned response to be used for future statfs ops.
	SetStatFSResponse(r fuseops.StatFSOp)

	// Set the canned response to be used for future stat ops.
	SetStatResponse(r fuseops.InodeAttributes)

	// Return the size of the most recent write delivered by the kernel, or -1 if
	// none.
	MostRecentWriteSize() int
}

func New() FS {
	return &statFS{
		cannedStatResponse: fuseops.InodeAttributes{
			Mode: 0666,
		},
		mostRecentWriteSize: -1,
	}
}

const childInodeID = fuseops.RootInodeID + 1

type statFS struct {
	fuseutil.NotImplementedFileSystem

	mu                  sync.Mutex
	cannedResponse      fuseops.StatFSOp        // GUARDED_BY(mu)
	cannedStatResponse  fuseops.InodeAttributes // GUARDED_BY(mu)
	mostRecentWriteSize int                     // GUARDED_BY(mu)
}

////////////////////////////////////////////////////////////////////////
// Helpers
////////////////////////////////////////////////////////////////////////

func dirAttrs() fuseops.InodeAttributes {
	return fuseops.InodeAttributes{
		Mode: os.ModeDir | 0777,
	}
}

func (fs *statFS) fileAttrs() fuseops.InodeAttributes {
	fs.mu.Lock()
	defer fs.mu.Unlock()

	return fs.cannedStatResponse
}

////////////////////////////////////////////////////////////////////////
// Public interface
////////////////////////////////////////////////////////////////////////

// LOCKS_EXCLUDED(fs.mu)
func (fs *statFS) SetStatFSResponse(r fuseops.StatFSOp) {
	fs.mu.Lock()
	defer fs.mu.Unlock()

	fs.cannedResponse = r
}

// LOCKS_EXCLUDED(fs.mu)
func (fs *statFS) SetStatResponse(r fuseops.InodeAttributes) {
	fs.mu.Lock()
	defer fs.mu.Unlock()

	fs.cannedStatResponse = r
}

// LOCKS_EXCLUDED(fs.mu)
func (fs *statFS) MostRecentWriteSize() int {
	fs.mu.Lock()
	defer fs.mu.Unlock()

	return fs.mostRecentWriteSize
}

////////////////////////////////////////////////////////////////////////
// FileSystem methods
////////////////////////////////////////////////////////////////////////

// LOCKS_EXCLUDED(fs.mu)
func (fs *statFS) StatFS(
	ctx context.Context,
	op *fuseops.StatFSOp) error {
	fs.mu.Lock()
	defer fs.mu.Unlock()

	*op = fs.cannedResponse
	return nil
}

func (fs *statFS) LookUpInode(
	ctx context.Context,
	op *fuseops.LookUpInodeOp) error {
	// Only the root has children.
	if op.Parent != fuseops.RootInodeID {
		return fuse.ENOENT
	}

	op.Entry.Child = childInodeID
	op.Entry.Attributes = fs.fileAttrs()

	return nil
}

func (fs *statFS) GetInodeAttributes(
	ctx context.Context,
	op *fuseops.GetInodeAttributesOp) error {
	switch op.Inode {
	case fuseops.RootInodeID:
		op.Attributes = dirAttrs()

	case childInodeID:
		op.Attributes = fs.fileAttrs()

	default:
		return fuse.ENOENT
	}

	return nil
}

func (fs *statFS) SetInodeAttributes(
	ctx context.Context,
	op *fuseops.SetInodeAttributesOp) error {
	// Ignore calls to truncate existing files when opening.
	return nil
}

func (fs *statFS) OpenFile(
	ctx context.Context,
	op *fuseops.OpenFileOp) error {
	return nil
}

// LOCKS_EXCLUDED(fs.mu)
func (fs *statFS) WriteFile(
	ctx context.Context,
	op *fuseops.WriteFileOp) error {
	fs.mu.Lock()
	defer fs.mu.Unlock()

	fs.mostRecentWriteSize = len(op.Data)
	return nil
}
