package store

import (
	"errors"
	"strings"

	memdb "github.com/hashicorp/go-memdb"
	"github.com/moby/swarmkit/v2/api"
)

const tableExtension = "extension"

func init() {
	register(ObjectStoreConfig{
		Table: &memdb.TableSchema{
			Name: tableExtension,
			Indexes: map[string]*memdb.IndexSchema{
				indexID: {
					Name:    indexID,
					Unique:  true,
					Indexer: extensionIndexerByID{},
				},
				indexName: {
					Name:    indexName,
					Unique:  true,
					Indexer: extensionIndexerByName{},
				},
				indexCustom: {
					Name:         indexCustom,
					Indexer:      extensionCustomIndexer{},
					AllowMissing: true,
				},
			},
		},
		Save: func(tx ReadTx, snapshot *api.StoreSnapshot) error {
			var err error
			snapshot.Extensions, err = FindExtensions(tx, All)
			return err
		},
		Restore: func(tx Tx, snapshot *api.StoreSnapshot) error {
			toStoreObj := make([]api.StoreObject, len(snapshot.Extensions))
			for i, x := range snapshot.Extensions {
				toStoreObj[i] = extensionEntry{x}
			}
			return RestoreTable(tx, tableExtension, toStoreObj)
		},
		ApplyStoreAction: func(tx Tx, sa api.StoreAction) error {
			switch v := sa.Target.(type) {
			case *api.StoreAction_Extension:
				obj := v.Extension
				switch sa.Action {
				case api.StoreActionKindCreate:
					return CreateExtension(tx, obj)
				case api.StoreActionKindUpdate:
					return UpdateExtension(tx, obj)
				case api.StoreActionKindRemove:
					return DeleteExtension(tx, obj.ID)
				}
			}
			return errUnknownStoreAction
		},
	})
}

type extensionEntry struct {
	*api.Extension
}

func (e extensionEntry) CopyStoreObject() api.StoreObject {
	return extensionEntry{Extension: e.Extension.Copy()}
}

// ensure that when update events are emitted, we unwrap extensionEntry
func (e extensionEntry) EventUpdate(oldObject api.StoreObject) api.Event {
	if oldObject != nil {
		return api.EventUpdateExtension{Extension: e.Extension, OldExtension: oldObject.(extensionEntry).Extension}
	}
	return api.EventUpdateExtension{Extension: e.Extension}
}

// CreateExtension adds a new extension to the store.
// Returns ErrExist if the ID is already taken.
func CreateExtension(tx Tx, e *api.Extension) error {
	// Ensure the name is not already in use.
	if tx.lookup(tableExtension, indexName, strings.ToLower(e.Annotations.Name)) != nil {
		return ErrNameConflict
	}

	// It can't conflict with built-in kinds either.
	if _, ok := schema.Tables[e.Annotations.Name]; ok {
		return ErrNameConflict
	}

	return tx.create(tableExtension, extensionEntry{e})
}

// UpdateExtension updates an existing extension in the store.
// Returns ErrNotExist if the object doesn't exist.
func UpdateExtension(tx Tx, e *api.Extension) error {
	// TODO(aaronl): For the moment, extensions are immutable
	return errors.New("extensions are immutable")
}

// DeleteExtension removes an extension from the store.
// Returns ErrNotExist if the object doesn't exist.
func DeleteExtension(tx Tx, id string) error {
	e := tx.get(tableExtension, id)
	if e == nil {
		return ErrNotExist
	}

	resources, err := FindResources(tx, ByKind(e.(extensionEntry).Annotations.Name))
	if err != nil {
		return err
	}

	if len(resources) != 0 {
		return errors.New("cannot delete extension because objects of this type exist in the data store")
	}

	return tx.delete(tableExtension, id)
}

// GetExtension looks up an extension by ID.
// Returns nil if the object doesn't exist.
func GetExtension(tx ReadTx, id string) *api.Extension {
	e := tx.get(tableExtension, id)
	if e == nil {
		return nil
	}
	return e.(extensionEntry).Extension
}

// FindExtensions selects a set of extensions and returns them.
func FindExtensions(tx ReadTx, by By) ([]*api.Extension, error) {
	checkType := func(by By) error {
		switch by.(type) {
		case byIDPrefix, byName, byCustom, byCustomPrefix:
			return nil
		default:
			return ErrInvalidFindBy
		}
	}

	extensionList := []*api.Extension{}
	appendResult := func(o api.StoreObject) {
		extensionList = append(extensionList, o.(extensionEntry).Extension)
	}

	err := tx.find(tableExtension, by, checkType, appendResult)
	return extensionList, err
}

type extensionIndexerByID struct{}

func (indexer extensionIndexerByID) FromArgs(args ...interface{}) ([]byte, error) {
	return api.ExtensionIndexerByID{}.FromArgs(args...)
}
func (indexer extensionIndexerByID) PrefixFromArgs(args ...interface{}) ([]byte, error) {
	return api.ExtensionIndexerByID{}.PrefixFromArgs(args...)
}
func (indexer extensionIndexerByID) FromObject(obj interface{}) (bool, []byte, error) {
	return api.ExtensionIndexerByID{}.FromObject(obj.(extensionEntry).Extension)
}

type extensionIndexerByName struct{}

func (indexer extensionIndexerByName) FromArgs(args ...interface{}) ([]byte, error) {
	return api.ExtensionIndexerByName{}.FromArgs(args...)
}
func (indexer extensionIndexerByName) PrefixFromArgs(args ...interface{}) ([]byte, error) {
	return api.ExtensionIndexerByName{}.PrefixFromArgs(args...)
}
func (indexer extensionIndexerByName) FromObject(obj interface{}) (bool, []byte, error) {
	return api.ExtensionIndexerByName{}.FromObject(obj.(extensionEntry).Extension)
}

type extensionCustomIndexer struct{}

func (indexer extensionCustomIndexer) FromArgs(args ...interface{}) ([]byte, error) {
	return api.ExtensionCustomIndexer{}.FromArgs(args...)
}
func (indexer extensionCustomIndexer) PrefixFromArgs(args ...interface{}) ([]byte, error) {
	return api.ExtensionCustomIndexer{}.PrefixFromArgs(args...)
}
func (indexer extensionCustomIndexer) FromObject(obj interface{}) (bool, [][]byte, error) {
	return api.ExtensionCustomIndexer{}.FromObject(obj.(extensionEntry).Extension)
}
