// Licensed to the LF AI & Data foundation under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you 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 deletebuffer

import (
	"sort"
	"sync"

	"github.com/cockroachdb/errors"
)

var (
	errBufferFull = errors.New("buffer full")
)

type timed interface {
	Timestamp() uint64
	Size() int64
}

// DeleteBuffer is the interface for delete buffer.
type DeleteBuffer[T timed] interface {
	Put(T)
	ListAfter(uint64) []T
	SafeTs() uint64
}

func NewDoubleCacheDeleteBuffer[T timed](startTs uint64, maxSize int64) DeleteBuffer[T] {
	return &doubleCacheBuffer[T]{
		head:    newDoubleCacheItem[T](startTs, maxSize),
		maxSize: maxSize,
		ts:      startTs,
	}
}

// doubleCacheBuffer implements DeleteBuffer with fixed sized double cache.
type doubleCacheBuffer[T timed] struct {
	mut        sync.RWMutex
	head, tail *doubleCacheItem[T]
	maxSize    int64
	ts         uint64
}

func (c *doubleCacheBuffer[T]) SafeTs() uint64 {
	return c.ts
}

// Put implements DeleteBuffer.
func (c *doubleCacheBuffer[T]) Put(entry T) {
	c.mut.Lock()
	defer c.mut.Unlock()

	err := c.head.Put(entry)
	if errors.Is(err, errBufferFull) {
		c.evict(entry.Timestamp())
		c.head.Put(entry)
	}
}

// ListAfter implements DeleteBuffer.
func (c *doubleCacheBuffer[T]) ListAfter(ts uint64) []T {
	c.mut.RLock()
	defer c.mut.RUnlock()
	var result []T
	if c.tail != nil {
		result = append(result, c.tail.ListAfter(ts)...)
	}
	if c.head != nil {
		result = append(result, c.head.ListAfter(ts)...)
	}
	return result
}

// evict sets head as tail and evicts tail.
func (c *doubleCacheBuffer[T]) evict(newTs uint64) {
	c.tail = c.head
	c.head = newDoubleCacheItem[T](newTs, c.maxSize/2)
	c.ts = c.tail.headTs
}

func newDoubleCacheItem[T timed](ts uint64, maxSize int64) *doubleCacheItem[T] {
	return &doubleCacheItem[T]{
		headTs:  ts,
		maxSize: maxSize,
	}
}

type doubleCacheItem[T timed] struct {
	mut     sync.RWMutex
	headTs  uint64
	size    int64
	maxSize int64

	data []T
}

// Cache adds entry into cache item.
// returns error if item is full
func (c *doubleCacheItem[T]) Put(entry T) error {
	c.mut.Lock()
	defer c.mut.Unlock()

	if c.size+entry.Size() > c.maxSize {
		return errBufferFull
	}

	c.data = append(c.data, entry)
	c.size += entry.Size()
	return nil
}

// ListAfter returns entries of which ts after provided value.
func (c *doubleCacheItem[T]) ListAfter(ts uint64) []T {
	c.mut.RLock()
	defer c.mut.RUnlock()
	idx := sort.Search(len(c.data), func(idx int) bool {
		return c.data[idx].Timestamp() >= ts
	})
	// not found
	if idx == len(c.data) {
		return nil
	}
	return c.data[idx:]
}
