package cache

import (
	"sync"

	"github.com/evanw/esbuild/internal/logger"
	"github.com/evanw/esbuild/internal/runtime"
)

// This is a cache of the parsed contents of a set of files. The idea is to be
// able to reuse the results of parsing between builds and make subsequent
// builds faster by avoiding redundant parsing work. This only works if:
//
// * The AST information in the cache must be considered immutable. There is
//   no way to enforce this in Go, but please be disciplined about this. The
//   ASTs are shared in between builds. Any information that must be mutated
//   in the AST during a build must be done on a shallow clone of the data if
//   the mutation happens after parsing (i.e. a clone that clones everything
//   that will be mutated and shares only the parts that won't be mutated).
//
// * The information in the cache must not depend at all on the contents of
//   any file other than the file being cached. Invalidating an entry in the
//   cache does not also invalidate any entries that depend on that file, so
//   caching information that depends on other files can result in incorrect
//   results due to reusing stale data. For example, do not "bake in" some
//   value imported from another file.
//
// * Cached ASTs must only be reused if the parsing options are identical
//   between builds. For example, it would be bad if the AST parser depended
//   on options inherited from a nearby "package.json" file but those options
//   were not part of the cache key. Then the cached AST could incorrectly be
//   reused even if the contents of that "package.json" file have changed.
//
type CacheSet struct {
	SourceIndexCache SourceIndexCache
	FSCache          FSCache
	CSSCache         CSSCache
	JSONCache        JSONCache
	JSCache          JSCache
}

func MakeCacheSet() *CacheSet {
	return &CacheSet{
		SourceIndexCache: SourceIndexCache{
			entries:         make(map[sourceIndexKey]uint32),
			nextSourceIndex: runtime.SourceIndex + 1,
		},
		FSCache: FSCache{
			entries: make(map[string]*fsEntry),
		},
		CSSCache: CSSCache{
			entries: make(map[logger.Path]*cssCacheEntry),
		},
		JSONCache: JSONCache{
			entries: make(map[logger.Path]*jsonCacheEntry),
		},
		JSCache: JSCache{
			entries: make(map[logger.Path]*jsCacheEntry),
		},
	}
}

type SourceIndexCache struct {
	mutex           sync.Mutex
	entries         map[sourceIndexKey]uint32
	nextSourceIndex uint32
}

type SourceIndexKind uint8

const (
	SourceIndexNormal SourceIndexKind = iota
	SourceIndexJSStubForCSS
)

type sourceIndexKey struct {
	path logger.Path
	kind SourceIndexKind
}

func (c *SourceIndexCache) LenHint() uint32 {
	c.mutex.Lock()
	defer c.mutex.Unlock()

	// Add some extra room at the end for a new file or two without reallocating
	const someExtraRoom = 16
	return c.nextSourceIndex + someExtraRoom
}

func (c *SourceIndexCache) Get(path logger.Path, kind SourceIndexKind) uint32 {
	key := sourceIndexKey{path: path, kind: kind}
	c.mutex.Lock()
	defer c.mutex.Unlock()
	if sourceIndex, ok := c.entries[key]; ok {
		return sourceIndex
	}
	sourceIndex := c.nextSourceIndex
	c.nextSourceIndex++
	c.entries[key] = sourceIndex
	return sourceIndex
}
