package cache

import (
	"io"
	"sync"
)

type EncoderPool struct {
	encoders []*gobEncoder
	lock     sync.RWMutex
}

func NewEncoderPool() *EncoderPool {
	return &EncoderPool{
		encoders: []*gobEncoder{},
		lock:     sync.RWMutex{},
	}
}

// Encode some value with gob
func (p *EncoderPool) Encode(raw any) ([]byte, error) {
	var encoder *gobEncoder
	// First try to find an available encoder
	// Read only lock should be fine here since locks are atomic i
	//and thus no two goroutines should be able to lock the same encoder at the same time
	// One of those attempts is going to fail and continue looking for another available one
	p.lock.RLock()
	for _, v := range p.encoders {
		// If we can lock one, it's available
		if v.TryLock() {
			// Keep the reference, then break
			encoder = v
			break
		}
	}
	p.lock.RUnlock()
	// Didn't find an available encoder, create new one and add to pool
	if encoder == nil {
		encoder = p.expand()
	}
	// Ensure we free the encoder at the end
	defer encoder.Unlock()
	// Clear the buffer to avoid funky output from previous operations
	encoder.Buffer.Reset()
	if err := encoder.Encoder.Encode(raw); err != nil {
		return nil, err
	}
	data, err := io.ReadAll(encoder.Buffer)
	if err != nil {
		return nil, err
	}
	return data, nil
}

// Expands the pool of available encoders by one and returns a reference to the new one
// The new encoder is already locked and ready for use
func (p *EncoderPool) expand() *gobEncoder {
	enc := newEncoder()
	// Lock everything. First the pool fully since we need to overwrite the encoders slice
	p.lock.Lock()
	// And then the new encoder to make it available for use by the caller
	// so that they don't have to search for it again
	enc.Lock()
	p.encoders = append(p.encoders, &enc)
	p.lock.Unlock()
	return &enc
}

// Prune all encoders not currently used from the pool
func (p *EncoderPool) Prune() {
	stillActiveEncoders := []*gobEncoder{}
	p.lock.Lock()
	for _, v := range p.encoders {
		if !v.TryLock() {
			// Can't lock, encoder in use, keep it
			stillActiveEncoders = append(stillActiveEncoders, v)
			continue
		}
		// If we reach here, the encoder was available (since not locked), unlock and continue
		v.Unlock()
	}
	// Overwrite list of available encoders to only contain the ones we found to still be active
	p.encoders = stillActiveEncoders
	p.lock.Unlock()
}

type DecoderPool struct {
	encoders []*gobDecoder
	lock     sync.RWMutex
}

func NewDecoderPool() *DecoderPool {
	return &DecoderPool{
		encoders: []*gobDecoder{},
		lock:     sync.RWMutex{},
	}
}

// Decode some value with gob
func (p *DecoderPool) Decode(raw []byte, target any) error {
	var encoder *gobDecoder
	// First try to find an available encoder
	// Read only lock should be fine here since locks are atomic i
	//and thus no two goroutines should be able to lock the same encoder at the same time
	// One of those attempts is going to fail and continue looking for another available one
	p.lock.RLock()
	for _, v := range p.encoders {
		// If we can lock one, it's available
		if v.TryLock() {
			// Keep the reference, then break
			encoder = v
			break
		}
	}
	p.lock.RUnlock()
	// Didn't find an available encoder, create new one and add to pool
	if encoder == nil {
		encoder = p.expand()
	}
	// Desure we free the encoder at the end
	defer encoder.Unlock()
	// Clear the buffer to avoid funky output from previous operations
	encoder.Buffer.Reset()
	// Write the raw data to the buffer, then decode it
	// The write will always succeed (or panic)
	_, _ = encoder.Buffer.Write(raw)
	err := encoder.Decoder.Decode(target)
	if err != nil {
		return err
	}
	return nil
}

// Expands the pool of available encoders by one and returns a reference to the new one
// The new encoder is already locked and ready for use
func (p *DecoderPool) expand() *gobDecoder {
	enc := newDecoder()
	// Lock everything. First the pool fully since we need to overwrite the encoders slice
	p.lock.Lock()
	// And then the new encoder to make it available for use by the caller
	// so that they don't have to search for it again
	enc.Lock()
	p.encoders = append(p.encoders, &enc)
	p.lock.Unlock()
	return &enc
}

// Prune all encoders not currently used from the pool
func (p *DecoderPool) Prune() {
	stillActiveDecoders := []*gobDecoder{}
	p.lock.Lock()
	for _, v := range p.encoders {
		if !v.TryLock() {
			// Can't lock, encoder in use, keep it
			stillActiveDecoders = append(stillActiveDecoders, v)
			continue
		}
		// If we reach here, the encoder was available (since not locked), unlock and continue
		v.Unlock()
	}
	// Overwrite list of available encoders to only contain the ones we found to still be active
	p.encoders = stillActiveDecoders
	p.lock.Unlock()
}
