// Copyright 2023 The Ebitengine 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 graphics

import (
	"runtime"
	"sync"
)

// ManagedBytes is a managed byte slice.
// The internal byte alice are managed in a pool.
// ManagedBytes is useful when its lifetime is explicit, as the underlying byte slice can be reused for another ManagedBytes later.
// This can reduce allocations and GCs.
type ManagedBytes struct {
	bytes []byte
	pool  *bytesPool
}

// Len returns the length of the slice.
func (m *ManagedBytes) Len() int {
	return len(m.bytes)
}

// Read reads the byte slice's content to dst.
func (m *ManagedBytes) Read(dst []byte, from, to int) {
	copy(dst, m.bytes[from:to])
}

// Clone creates a new ManagedBytes with the same content.
func (m *ManagedBytes) Clone() *ManagedBytes {
	return NewManagedBytes(len(m.bytes), func(bs []byte) {
		copy(bs, m.bytes)
	})
}

// GetAndRelease returns the raw byte slice and a finalizer.
// A finalizer should be called when you can ensure that the slice is no longer used,
// e.g. when a graphics command using this slice is sent and executed.
//
// After GetAndRelease is called, the underlying byte slice is no longer available.
func (m *ManagedBytes) GetAndRelease() ([]byte, func()) {
	bs := m.bytes
	m.bytes = nil
	return bs, func() {
		m.pool.put(bs)
		runtime.SetFinalizer(m, nil)
	}
}

// NewManagedBytes returns a managed byte slice initialized by the given constructor f.
//
// The byte slice is not zero-cleared at the constructor.
func NewManagedBytes(size int, f func([]byte)) *ManagedBytes {
	bs := theBytesPool.get(size)
	f(bs.bytes)
	return bs
}

type bytesPool struct {
	pool [][]byte

	m sync.Mutex
}

var theBytesPool bytesPool

func (b *bytesPool) get(size int) *ManagedBytes {
	bs := b.getFromCache(size)
	if bs == nil {
		bs = make([]byte, size)
	}
	m := &ManagedBytes{
		bytes: bs,
		pool:  b,
	}
	runtime.SetFinalizer(m, func(m *ManagedBytes) {
		b.put(m.bytes)
	})
	return m
}

func (b *bytesPool) getFromCache(size int) []byte {
	b.m.Lock()
	defer b.m.Unlock()

	for i, bs := range b.pool {
		if cap(bs) < size {
			continue
		}

		copy(b.pool[i:], b.pool[i+1:])
		b.pool[len(b.pool)-1] = nil
		b.pool = b.pool[:len(b.pool)-1]
		return bs[:size]
	}

	return nil
}

func (b *bytesPool) put(bs []byte) {
	if len(bs) == 0 {
		return
	}

	b.m.Lock()
	defer b.m.Unlock()

	b.pool = append(b.pool, bs)

	// GC the pool. The size limitation is arbitrary.
	for len(b.pool) >= 32 || b.totalSize() >= 1024*1024*1024 {
		b.pool = b.pool[1:]
	}
}

func (b *bytesPool) totalSize() int {
	var s int
	for _, bs := range b.pool {
		s += len(bs)
	}
	return s
}