package imagerefchecker

import (
	"sync"

	"github.com/docker/docker/image"
	"github.com/docker/docker/layer"
	"github.com/moby/buildkit/cache"
	digest "github.com/opencontainers/go-digest"
)

// LayerGetter abstracts away the snapshotter
type LayerGetter interface {
	GetLayer(string) (layer.Layer, error)
}

// Opt represents the options needed to create a refchecker
type Opt struct {
	LayerGetter LayerGetter
	ImageStore  image.Store
}

// New creates new image reference checker that can be used to see if a reference
// is being used by any of the images in the image store
func New(opt Opt) cache.ExternalRefCheckerFunc {
	return func() (cache.ExternalRefChecker, error) {
		return &checker{opt: opt, layers: lchain{}, cache: map[string]bool{}}, nil
	}
}

type lchain map[layer.DiffID]lchain

func (c lchain) add(ids []layer.DiffID) {
	if len(ids) == 0 {
		return
	}
	id := ids[0]
	ch, ok := c[id]
	if !ok {
		ch = lchain{}
		c[id] = ch
	}
	ch.add(ids[1:])
}

func (c lchain) has(ids []layer.DiffID) bool {
	if len(ids) == 0 {
		return true
	}
	ch, ok := c[ids[0]]
	return ok && ch.has(ids[1:])
}

type checker struct {
	opt    Opt
	once   sync.Once
	layers lchain
	cache  map[string]bool
}

func (c *checker) Exists(key string, chain []digest.Digest) bool {
	if c.opt.ImageStore == nil {
		return false
	}

	c.once.Do(c.init)

	if b, ok := c.cache[key]; ok {
		return b
	}

	l, err := c.opt.LayerGetter.GetLayer(key)
	if err != nil || l == nil {
		c.cache[key] = false
		return false
	}

	ok := c.layers.has(diffIDs(l))
	c.cache[key] = ok
	return ok
}

func (c *checker) init() {
	imgs := c.opt.ImageStore.Map()

	for _, img := range imgs {
		c.layers.add(img.RootFS.DiffIDs)
	}
}

func diffIDs(l layer.Layer) []layer.DiffID {
	p := l.Parent()
	if p == nil {
		return []layer.DiffID{l.DiffID()}
	}
	return append(diffIDs(p), l.DiffID())
}
