// Copyright 2021 Vitaliy Filippov
//
// 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 readbenchfs

import (
	"golang.org/x/net/context"
	"io"
	"math/rand"
	"os"

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

type readBenchFS struct {
	fuseutil.NotImplementedFileSystem
	buf             []byte
	useVectoredRead bool
}

// 1 TB
const fileSize = 1024 * 1024 * 1024 * 1024

var _ fuseutil.FileSystem = &readBenchFS{}

func NewReadBenchServer(useVectoredRead bool) (server fuse.Server, err error) {
	// 1 GB of random data to exceed CPU cache
	buf := make([]byte, 1024*1024*1024)
	rand.Read(buf)
	server = fuseutil.NewFileSystemServer(&readBenchFS{
		buf:             buf,
		useVectoredRead: useVectoredRead,
	})
	return
}

func (fs *readBenchFS) StatFS(ctx context.Context, op *fuseops.StatFSOp) error {
	return nil
}

func (fs *readBenchFS) LookUpInode(ctx context.Context, op *fuseops.LookUpInodeOp) error {
	if op.Name == "test" {
		op.Entry = fuseops.ChildInodeEntry{
			Child: 2,
			Attributes: fuseops.InodeAttributes{
				Size:  fileSize,
				Nlink: 1,
				Mode:  0444,
			},
		}
		return nil
	}
	return fuse.ENOENT
}

func (fs *readBenchFS) GetInodeAttributes(ctx context.Context, op *fuseops.GetInodeAttributesOp) error {
	if op.Inode == 1 {
		op.Attributes = fuseops.InodeAttributes{
			Nlink: 1,
			Mode:  0755 | os.ModeDir,
		}
		return nil
	} else if op.Inode == 2 {
		op.Attributes = fuseops.InodeAttributes{
			Size:  fileSize,
			Nlink: 1,
			Mode:  0444,
		}
		return nil
	}
	return fuse.ENOENT
}

func (fs *readBenchFS) OpenDir(ctx context.Context, op *fuseops.OpenDirOp) error {
	// Allow opening any directory.
	return nil
}

func (fs *readBenchFS) ReadDir(ctx context.Context, op *fuseops.ReadDirOp) error {
	if op.Inode != 1 {
		return fuse.ENOENT
	}
	if op.Offset > 0 {
		return nil
	}
	entries := []fuseutil.Dirent{
		fuseutil.Dirent{
			Offset: 1,
			Inode:  2,
			Name:   "test",
			Type:   fuseutil.DT_File,
		},
	}
	for _, e := range entries[op.Offset:] {
		n := fuseutil.WriteDirent(op.Dst[op.BytesRead:], e)
		if n == 0 {
			break
		}
		op.BytesRead += n
	}
	return nil
}

func (fs *readBenchFS) OpenFile(ctx context.Context, op *fuseops.OpenFileOp) error {
	// Allow opening any file.
	return nil
}

func (fs *readBenchFS) ReadFile(ctx context.Context, op *fuseops.ReadFileOp) error {
	if op.Offset > fileSize {
		return io.EOF
	}
	end := op.Offset + op.Size
	if end > fileSize {
		end = fileSize
	}
	buflen := int64(len(fs.buf))
	for pos := op.Offset; pos < end; {
		s := pos % buflen
		e := buflen
		if e-s > end-pos {
			e = s + end - pos
		}
		if fs.useVectoredRead {
			op.Data = append(op.Data, fs.buf[s:e])
		} else {
			copy(op.Dst[pos-op.Offset:], fs.buf[s:])
		}
		pos = op.Offset + e
	}
	op.BytesRead = int(end - op.Offset)
	return nil
}

func (fs *readBenchFS) ReleaseDirHandle(ctx context.Context, op *fuseops.ReleaseDirHandleOp) error {
	return nil
}

func (fs *readBenchFS) GetXattr(ctx context.Context, op *fuseops.GetXattrOp) error {
	return nil
}

func (fs *readBenchFS) ListXattr(ctx context.Context, op *fuseops.ListXattrOp) error {
	return nil
}

func (fs *readBenchFS) ForgetInode(ctx context.Context, op *fuseops.ForgetInodeOp) error {
	return nil
}

func (fs *readBenchFS) ReleaseFileHandle(ctx context.Context, op *fuseops.ReleaseFileHandleOp) error {
	return nil
}

func (fs *readBenchFS) FlushFile(ctx context.Context, op *fuseops.FlushFileOp) error {
	return nil
}
