package d2raster

import (
	
	
	
	
	
	
	
	
	

	
	
)

const (
	assetDigestChunkBytes   = 32 * 1024
	cacheEntryOverheadBytes = int64(256)
)

// RenderSessionOptions bounds reusable parsed and decoded asset state. Every
// field must be positive. MaxCacheEntries independently caps asset entries and
// immutable-source key memos, so a session may retain up to twice that many
// entries. MaxCacheBytes jointly bounds both groups. Entries charge source and
// MIME bytes for fonts, validated decoded footprint and MIME bytes for raster
// images, or the encoded source length for key memos, plus
// cacheEntryOverheadBytes each.
type RenderSessionOptions struct {
	MaxCacheEntries    int
	MaxCacheBytes      int64
	MaxConcurrentLoads int
}

// RenderSessionStats is an atomic snapshot of cache activity. Hits, Misses,
// and Waits describe parsed-font and decoded-raster lookups. MemoHits,
// MemoMisses, and MemoWaits describe content-key lookups; Hashes counts full
// source scans. SkippedOversize and MemoSkipped count successful results that
// could not be retained under MaxCacheBytes.
type RenderSessionStats struct {
	Hits            uint64
	Misses          uint64
	Waits           uint64
	Evictions       uint64
	SkippedOversize uint64
	Entries         int
	Bytes           int64
	MemoHits        uint64
	MemoMisses      uint64
	MemoWaits       uint64
	Hashes          uint64
	MemoEvictions   uint64
	MemoSkipped     uint64
	MemoEntries     int
	MemoBytes       int64
	RetainedBytes   int64
	ActiveLoads     int
}

// RenderSession reuses immutable parsed fonts and decoded raster pixels
// across renders. Scene validation and every per-document resource limit are
// still applied independently on every Render call. Callers may replace an
// asset value, but must not mutate an existing Data backing allocation, as
// required by d2scene.Asset.
type RenderSession struct {
	options   RenderSessionOptions
	loadSlots chan struct{}

	mu          sync.Mutex
	entries     map[assetCacheKey]*list.Element
	lru         list.List
	flights     map[assetCacheKey]*assetCacheFlight
	bytes       int64
	memos       map[assetMemoIdentity]*list.Element
	memoLRU     list.List
	memoFlights map[assetMemoIdentity]*assetMemoFlight
	memoBytes   int64
	stats       RenderSessionStats
}

type cachedAssetKind uint8

const (
	cachedFontAsset cachedAssetKind = iota + 1
	cachedRasterAsset
)

type assetCacheKey struct {
	kind          cachedAssetKind
	digest        [sha256.Size]byte
	mimeType      string
	fontFaceIndex uint16
	pixelWidth    int
	pixelHeight   int
	decodedBytes  int64
}

type cachedAssetValue struct {
	fontFace     *fontface.ParsedFace
	fontSource   *fontface.BundledFaceSource
	raster       *preparedRasterAsset
	decodedBytes int64
}

type assetCacheEntry struct {
	key    assetCacheKey
	value  cachedAssetValue
	charge int64
}

type assetCacheFlight struct {
	done  chan struct{}
	value cachedAssetValue
	err   error
}

type assetMemoIdentity struct {
	document      weak.Pointer[d2scene.Document]
	assetID       d2scene.AssetID
	kind          cachedAssetKind
	mimeType      string
	fontFaceIndex uint16
	pixelWidth    int
	pixelHeight   int
	decodedBytes  int64
	dataPointer   weak.Pointer[byte]
	dataLength    int
}

type assetMemoEntry struct {
	identity assetMemoIdentity
	key      assetCacheKey
	charge   int64
}

type assetMemoFlight struct {
	done chan struct{}
	key  assetCacheKey
	err  error
}

// NewRenderSession creates a bounded, concurrent-safe render session.
func ( RenderSessionOptions) (*RenderSession, error) {
	if .MaxCacheEntries <= 0 || .MaxCacheBytes <= 0 || .MaxConcurrentLoads <= 0 {
		return nil, fmt.Errorf("d2raster: every render session limit must be positive")
	}
	return &RenderSession{
		options:     ,
		loadSlots:   make(chan struct{}, .MaxConcurrentLoads),
		entries:     make(map[assetCacheKey]*list.Element),
		flights:     make(map[assetCacheKey]*assetCacheFlight),
		memos:       make(map[assetMemoIdentity]*list.Element),
		memoFlights: make(map[assetMemoIdentity]*assetMemoFlight),
	}, nil
}

// Stats returns a race-free snapshot of cache counters and current retained
// charged storage.
func ( *RenderSession) () RenderSessionStats {
	if  == nil {
		return RenderSessionStats{}
	}
	.mu.Lock()
	defer .mu.Unlock()
	 := .stats
	.Entries = len(.entries)
	.Bytes = .bytes
	.MemoEntries = len(.memos)
	.MemoBytes = .memoBytes
	.RetainedBytes = .bytes + .memoBytes
	return 
}

func digestAssetBytes( context.Context,  []byte) ([sha256.Size]byte, error) {
	if  := .Err();  != nil {
		return [sha256.Size]byte{}, 
	}
	 := sha256.New()
	for  := 0;  < len();  += assetDigestChunkBytes {
		if  := .Err();  != nil {
			return [sha256.Size]byte{}, 
		}
		 :=  + assetDigestChunkBytes
		if  > len() {
			 = len()
		}
		_, _ = .Write([:])
	}
	if  := .Err();  != nil {
		return [sha256.Size]byte{}, 
	}
	var  [sha256.Size]byte
	copy([:], .Sum(nil))
	return , nil
}

func ( *RenderSession) ( context.Context,  *d2scene.Document,  d2scene.AssetID,  cachedAssetKind,  string,  []byte,  uint16, ,  int,  int64) (assetCacheKey, error) {
	return .memoizedCacheKeyWithDigest(, , , , , , , , , , nil)
}

func ( *RenderSession) ( context.Context,  *d2scene.Document,  d2scene.AssetID,  cachedAssetKind,  string,  []byte,  uint16, ,  int,  int64,  *[sha256.Size]byte) (assetCacheKey, error) {
	 := assetMemoIdentity{
		document: weak.Make(), assetID: , kind: , mimeType: ,
		fontFaceIndex: , pixelWidth: , pixelHeight: , decodedBytes: ,
		dataPointer: weak.Make(unsafe.SliceData()), dataLength: len(),
	}
	for {
		if  := .Err();  != nil {
			return assetCacheKey{}, 
		}
		.mu.Lock()
		if ,  := .memos[];  {
			.memoLRU.MoveToFront()
			.stats.MemoHits++
			 := .Value.(*assetMemoEntry).key
			.mu.Unlock()
			return , nil
		}
		if ,  := .memoFlights[];  {
			.stats.MemoWaits++
			.mu.Unlock()
			, ,  := waitForMemoFlight(, )
			if  != nil {
				return assetCacheKey{}, 
			}
			if  {
				continue
			}
			return , nil
		}
		.mu.Unlock()

		if  := .acquireLoad();  != nil {
			return assetCacheKey{}, 
		}
		.mu.Lock()
		if ,  := .memos[];  {
			.memoLRU.MoveToFront()
			.stats.MemoHits++
			 := .Value.(*assetMemoEntry).key
			.mu.Unlock()
			.releaseLoad()
			return , nil
		}
		if ,  := .memoFlights[];  {
			.stats.MemoWaits++
			.mu.Unlock()
			.releaseLoad()
			, ,  := waitForMemoFlight(, )
			if  != nil {
				return assetCacheKey{}, 
			}
			if  {
				continue
			}
			return , nil
		}
		 := &assetMemoFlight{done: make(chan struct{})}
		.memoFlights[] = 
		.stats.MemoMisses++
		.mu.Unlock()

		var  [sha256.Size]byte
		var  error
		if  == nil {
			,  = digestAssetBytes(, )
		} else {
			 = *
			 = .Err()
		}
		 := assetCacheKey{
			kind: , digest: , mimeType: , fontFaceIndex: ,
			pixelWidth: , pixelHeight: , decodedBytes: ,
		}
		.mu.Lock()
		if  == nil {
			 = .Err()
		}
		delete(.memoFlights, )
		.key = 
		.err = 
		if  == nil {
			if  == nil {
				.stats.Hashes++
			}
			.insertMemoLocked(, , memoEntryCharge(, , ))
		}
		close(.done)
		.mu.Unlock()
		.releaseLoad()
		return , 
	}
}

func waitForMemoFlight( context.Context,  *assetMemoFlight) (assetCacheKey, bool, error) {
	select {
	case <-.Done():
		return assetCacheKey{}, false, .Err()
	case <-.done:
		if  := .Err();  != nil {
			return assetCacheKey{}, false, 
		}
		if .err == nil {
			return .key, false, nil
		}
		return assetCacheKey{}, true, nil
	}
}

func cacheEntryCharge( int64,  string) int64 {
	if  < 0 ||  > math.MaxInt64-cacheEntryOverheadBytes {
		return math.MaxInt64
	}
	 :=  + cacheEntryOverheadBytes
	if int64(len()) > math.MaxInt64- {
		return math.MaxInt64
	}
	return  + int64(len())
}

func memoEntryCharge( []byte,  d2scene.AssetID,  string) int64 {
	 := cacheEntryCharge(int64(len()), )
	if int64(len()) > math.MaxInt64- {
		return math.MaxInt64
	}
	return  + int64(len())
}

func ( *RenderSession) ( assetCacheKey) (cachedAssetValue, bool) {
	.mu.Lock()
	defer .mu.Unlock()
	,  := .entries[]
	if ! {
		return cachedAssetValue{}, false
	}
	.lru.MoveToFront()
	.stats.Hits++
	return .Value.(*assetCacheEntry).value, true
}

func ( *RenderSession) ( context.Context,  assetCacheKey,  int64,  func(context.Context) (cachedAssetValue, error)) (cachedAssetValue, error) {
	for {
		if  := .Err();  != nil {
			return cachedAssetValue{}, 
		}
		.mu.Lock()
		if ,  := .entries[];  {
			.lru.MoveToFront()
			.stats.Hits++
			 := .Value.(*assetCacheEntry).value
			.mu.Unlock()
			return , nil
		}
		if ,  := .flights[];  {
			.stats.Waits++
			.mu.Unlock()
			, ,  := waitForAssetFlight(, )
			if  != nil {
				return cachedAssetValue{}, 
			}
			if  {
				continue
			}
			return , nil
		}
		.mu.Unlock()

		// Admission precedes flight creation so the session retains at most
		// MaxConcurrentLoads unique in-flight keys and result closures.
		if  := .acquireLoad();  != nil {
			return cachedAssetValue{}, 
		}
		.mu.Lock()
		if ,  := .entries[];  {
			.lru.MoveToFront()
			.stats.Hits++
			 := .Value.(*assetCacheEntry).value
			.mu.Unlock()
			.releaseLoad()
			return , nil
		}
		if ,  := .flights[];  {
			.stats.Waits++
			.mu.Unlock()
			.releaseLoad()
			, ,  := waitForAssetFlight(, )
			if  != nil {
				return cachedAssetValue{}, 
			}
			if  {
				continue
			}
			return , nil
		}
		 := &assetCacheFlight{done: make(chan struct{})}
		.flights[] = 
		.stats.Misses++
		.mu.Unlock()

		,  := ()
		.mu.Lock()
		if  == nil {
			 = .Err()
		}
		delete(.flights, )
		.value = 
		.err = 
		if  == nil {
			.insertLocked(, , )
		}
		close(.done)
		.mu.Unlock()
		.releaseLoad()
		return , 
	}
}

func waitForAssetFlight( context.Context,  *assetCacheFlight) (cachedAssetValue, bool, error) {
	select {
	case <-.Done():
		return cachedAssetValue{}, false, .Err()
	case <-.done:
		if  := .Err();  != nil {
			return cachedAssetValue{}, false, 
		}
		if .err == nil {
			return .value, false, nil
		}
		// Failed and canceled loads are never cached. A waiter retries with
		// its own context so one owner cannot poison other callers.
		return cachedAssetValue{}, true, nil
	}
}

func ( *RenderSession) ( context.Context) error {
	select {
	case .loadSlots <- struct{}{}:
		.mu.Lock()
		.stats.ActiveLoads++
		.mu.Unlock()
		return nil
	case <-.Done():
		return .Err()
	}
}

func ( *RenderSession) () {
	<-.loadSlots
	.mu.Lock()
	.stats.ActiveLoads--
	.mu.Unlock()
}

func ( *RenderSession) ( assetCacheKey,  cachedAssetValue,  int64) {
	if  > .options.MaxCacheBytes {
		.stats.SkippedOversize++
		return
	}
	for len(.entries) >= .options.MaxCacheEntries {
		.evictAssetLocked()
	}
	for  > .options.MaxCacheBytes-.bytes-.memoBytes {
		if .memoLRU.Back() != nil {
			.evictMemoLocked()
		} else {
			.evictAssetLocked()
		}
	}
	.mimeType = strings.Clone(.mimeType)
	 := &assetCacheEntry{key: , value: , charge: }
	 := .lru.PushFront()
	.entries[] = 
	.bytes += 
}

func ( *RenderSession) ( assetMemoIdentity,  assetCacheKey,  int64) {
	if  > .options.MaxCacheBytes {
		.stats.MemoSkipped++
		return
	}
	for len(.memos) >= .options.MaxCacheEntries {
		.evictMemoLocked()
	}
	for  > .options.MaxCacheBytes-.bytes-.memoBytes {
		if .memoLRU.Back() == nil {
			.stats.MemoSkipped++
			return
		}
		.evictMemoLocked()
	}
	.assetID = d2scene.AssetID(strings.Clone(string(.assetID)))
	.mimeType = strings.Clone(.mimeType)
	.mimeType = .mimeType
	 := &assetMemoEntry{identity: , key: , charge: }
	 := .memoLRU.PushFront()
	.memos[] = 
	.memoBytes += 
}

func ( *RenderSession) () {
	 := .lru.Back()
	if  == nil {
		return
	}
	 := .Value.(*assetCacheEntry)
	delete(.entries, .key)
	.lru.Remove()
	.bytes -= .charge
	.stats.Evictions++
}

func ( *RenderSession) () {
	 := .memoLRU.Back()
	if  == nil {
		return
	}
	 := .Value.(*assetMemoEntry)
	delete(.memos, .identity)
	.memoLRU.Remove()
	.memoBytes -= .charge
	.stats.MemoEvictions++
}

func copyAssetBytes( context.Context,  []byte) ([]byte, error) {
	if  := .Err();  != nil {
		return nil, 
	}
	 := make([]byte, len())
	for  := 0;  < len();  += assetDigestChunkBytes {
		if  := .Err();  != nil {
			return nil, 
		}
		 :=  + assetDigestChunkBytes
		if  > len() {
			 = len()
		}
		copy([:], [:])
	}
	if  := .Err();  != nil {
		return nil, 
	}
	return , nil
}

func ( *RenderSession) ( context.Context,  *d2scene.Document,  d2scene.AssetID,  d2scene.FontAsset) (*preparedFont, error) {
	var  assetCacheKey
	,  := fontface.RegisteredBundledFaceBackingDigest(.Data)
	if ! {
		,  = fontface.RegisteredBundledNotoColorEmojiBackingDigest(.Data)
	}
	var  error
	if  {
		,  = .memoizedCacheKeyWithDigest(, , , cachedFontAsset, .MIMEType, .Data, .FaceIndex, 0, 0, 0, &)
	} else {
		,  = .memoizedCacheKey(, , , cachedFontAsset, .MIMEType, .Data, .FaceIndex, 0, 0, 0)
	}
	if  != nil {
		return nil, 
	}
	,  := .getOrLoad(, , cacheEntryCharge(int64(len(.Data)), .MIMEType), func( context.Context) (cachedAssetValue, error) {
		if  := .Err();  != nil {
			return cachedAssetValue{}, 
		}
		, ,  := fontface.RegisteredBundledFaceDigest(.Data, .FaceIndex, .digest)
		if  != nil {
			return cachedAssetValue{}, 
		}
		if  {
			return cachedAssetValue{fontSource: }, nil
		}
		, ,  := fontface.RegisteredBundledNotoColorEmoji(.Data, .FaceIndex)
		if  != nil {
			return cachedAssetValue{}, 
		}
		if  {
			return cachedAssetValue{fontSource: }, nil
		}
		,  := copyAssetBytes(, .Data)
		if  != nil {
			return cachedAssetValue{}, 
		}
		,  := parsePreparedFont(, .FaceIndex)
		if  != nil {
			return cachedAssetValue{}, 
		}
		return cachedAssetValue{fontFace: .face}, nil
	})
	if  != nil {
		return nil, 
	}
	if .fontSource != nil {
		return newPreparedBundledFont(.fontSource)
	}
	 := new(preparedFont)
	 = .fontFace.CloneInto(&.lazyFace)
	if  != nil {
		return nil, 
	}
	.face = &.lazyFace
	.outline = .lazyFace.Outline
	.shaping = .lazyFace.Shaping
	return , nil
}

func ( *RenderSession) ( context.Context,  *d2scene.Document,  d2scene.AssetID,  d2scene.RasterAsset,  int64) (*preparedRasterAsset, int64, error) {
	,  := .memoizedCacheKey(, , , cachedRasterAsset, .MIMEType, .Data, 0, .PixelWidth, .PixelHeight, .DecodedBytes)
	if  != nil {
		return nil, 0, 
	}
	if ,  := .lookup();  {
		if  := validateDecodedAssetBudget(, .decodedBytes, );  != nil {
			return nil, 0, 
		}
		return .raster, .decodedBytes, nil
	}

	,  := validateRasterAsset(, , , )
	if  != nil {
		return nil, 0, 
	}
	,  := .getOrLoad(, , cacheEntryCharge(.decodedBytes, .MIMEType), func( context.Context) (cachedAssetValue, error) {
		,  := decodeRasterAsset(, , , )
		if  != nil {
			return cachedAssetValue{}, 
		}
		return cachedAssetValue{raster: , decodedBytes: .decodedBytes}, nil
	})
	if  != nil {
		return nil, 0, 
	}
	// A concurrent owner may have validated the same content under a different
	// remaining document budget. Charge this caller independently regardless.
	if  := validateDecodedAssetBudget(, .decodedBytes, );  != nil {
		return nil, 0, 
	}
	return .raster, .decodedBytes, nil
}