// Copyright 2022 The gVisor Authors.
//
// 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 iouringfs

import (
	"fmt"

	"gvisor.dev/gvisor/pkg/safemem"
)

// sharedBuffer represents a memory buffer shared between the sentry and
// userspace. In many cases, this is simply an internal mmap on the underlying
// memory (aka fast mode). However in some cases the mapped region may lie
// across multiple blocks and we need to copy the region into a contiguous
// buffer (aka slow mode). The goal in either case is to present a contiguous
// slice for easy access.
//
// sharedBuffer must be initialized with init before first use.
//
// Example
// =======
/*
var sb sharedBuffer
bs := MapInternal(...)
sb.init(bs)

fetch := true

for !done {
	var err error

	// (Re-)Fetch the view.
	var view []byte
	if fetch {
		view, err = sb.view(128)
	}

	// Use the view slice to access the region, both for read or write.
	someState := dosomething(view[10])
	view[20] = someState & mask

	// Write back the changes.
	fetch, err = sb.writeback(128)
}
*/
// In the above example, in fast mode view returns a slice that points directly
// to the underlying memory and requires no copying. Writeback is a no-op, and
// the view can be reused on subsequent loop iterations (writeback will return
// refetch == false).
//
// In slow mode, view will copy disjoint parts of the region from different
// blocks to a single contiguous slice. Writeback will also required a copy, and
// a new view will have to be fetched on every loop iteration (writeback will
// return refetch == true).
//
// sharedBuffer is *not* thread safe.
type sharedBuffer struct {
	bs safemem.BlockSeq

	// copy is allocated once and reused on subsequent calls to view. We don't
	// use the Task's copy scratch buffer because these buffers may be accessed
	// from a background context.
	copy []byte

	// needsWriteback indicates whether we need to copy out back data from the
	// slice returned by the last view() call.
	needsWriteback bool
}

// init initializes the sharedBuffer, and must be called before first use.
func (b *sharedBuffer) init(bs safemem.BlockSeq) {
	b.bs = bs
}

func (b *sharedBuffer) valid() bool {
	return !b.bs.IsEmpty()
}

// view returns a slice representing the shared buffer. When done, view must be
// released with either writeback{,Window} or drop.
func (b *sharedBuffer) view(n int) ([]byte, error) {
	if uint64(n) > b.bs.NumBytes() {
		// Mapping too short? This is a bug.
		panic(fmt.Sprintf("iouringfs: mapping too short for requested len: mapping length %v, requested %d", b.bs.NumBytes(), n))
	}

	// Fast path: use mapping directly, no copies required.
	h := b.bs.Head()
	if h.Len() <= n && !h.NeedSafecopy() {
		b.needsWriteback = false
		return h.ToSlice()[:n], nil
	}

	// Buffer mapped across multiple blocks, or requires safe copy.
	if len(b.copy) < n {
		b.copy = make([]byte, n)
	}
	dst := safemem.BlockSeqOf(safemem.BlockFromSafeSlice(b.copy[:n]))
	copyN, err := safemem.CopySeq(dst, b.bs)
	if err != nil {
		return nil, err
	}
	if copyN != uint64(n) {
		// Short copy risks exposing stale data from view buffer. This should never happen.
		panic(fmt.Sprintf("iouringfs: short copy for shared buffer view: want %d, got %d", n, copyN))
	}
	b.needsWriteback = true
	return b.copy, nil
}

// writeback writes back the changes to the slice returned by the previous view
// call. On return, writeback indicates if the previous view may be reused, or
// needs to be refetched with a new call to view.
//
// Precondition: Must follow a call to view. n must match the value passed to
// view.
//
// Postcondition: Previous view is invalidated whether writeback is successful
// or not. To attempt another modification, a new view may need to be obtained,
// according to refetch.
func (b *sharedBuffer) writeback(n int) (refetch bool, err error) {
	return b.writebackWindow(0, n)
}

// writebackWindow is like writeback, but only writes back a subregion. Useful
// if the caller knows only a small region has been updated, as it reduces how
// much data need to be copied. writebackWindow still potentially invalidates
// the entire view, caller must check refetch to determine if the view needs to
// be refreshed.
func (b *sharedBuffer) writebackWindow(off, len int) (refetch bool, err error) {
	if uint64(off+len) > b.bs.NumBytes() {
		panic(fmt.Sprintf("iouringfs: requested writeback to shared buffer from offset %d for %d bytes would overflow underlying region of size %d", off, len, b.bs.NumBytes()))
	}

	if !b.needsWriteback {
		return false, nil
	}

	// Existing view invalid after this point.
	b.needsWriteback = false

	src := safemem.BlockSeqOf(safemem.BlockFromSafeSlice(b.copy[off : off+len]))
	dst := b.bs.DropFirst(off)
	copyN, err := safemem.CopySeq(dst, src)
	if err != nil {
		return true, err
	}
	if copyN != uint64(len) {
		panic(fmt.Sprintf("iouringfs: short copy for shared buffer writeback: want %d, got %d", len, copyN))
	}
	return true, nil
}

// drop releases a view without writeback. Returns whether any existing views
// need to be refetched. Useful when caller is done with a view that doesn't
// need to be modified.
func (b *sharedBuffer) drop() bool {
	wb := b.needsWriteback
	b.needsWriteback = false
	return wb
}
