/*
Copyright 2017 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package apply

import (
	"fmt"
)

// Element contains the record, local, and remote value for a field in an object
// and metadata about the field read from openapi.
// Calling Merge on an element will apply the passed in strategy to Element -
// e.g. either replacing the whole element with the local copy or merging each
// of the recorded, local and remote fields of the element.
type Element interface {
	// FieldMeta specifies which merge strategy to use for this element
	FieldMeta

	// Merge merges the recorded, local and remote values in the element using the Strategy
	// provided as an argument.  Calls the type specific method on the Strategy - following the
	// "Accept" method from the "Visitor" pattern.
	// e.g. Merge on a ListElement will call Strategy.MergeList(self)
	// Returns the Result of the merged elements
	Merge(Strategy) (Result, error)

	// HasRecorded returns true if the field was explicitly
	// present in the recorded source.  This is to differentiate between
	// undefined and set to null
	HasRecorded() bool

	// GetRecorded returns the field value from the recorded source of the object
	GetRecorded() interface{}

	// HasLocal returns true if the field was explicitly
	// present in the local source.  This is to differentiate between
	// undefined and set to null
	HasLocal() bool

	// GetLocal returns the field value from the local source of the object
	GetLocal() interface{}

	// HasRemote returns true if the field was explicitly
	// present in the remote source.  This is to differentiate between
	// undefined and set to null
	HasRemote() bool

	// GetRemote returns the field value from the remote source of the object
	GetRemote() interface{}
}

// FieldMeta defines the strategy used to apply a Patch for an element
type FieldMeta interface {
	// GetFieldMergeType returns the type of merge strategy to use for this field
	// maybe "merge", "replace" or "retainkeys"
	// TODO: There maybe multiple strategies, so this may need to be a slice, map, or struct
	// Address this in a follow up in the PR to introduce retainkeys strategy
	GetFieldMergeType() string

	// GetFieldMergeKeys returns the merge key to use when the MergeType is "merge" and underlying type is a list
	GetFieldMergeKeys() MergeKeys

	// GetFieldType returns the openapi field type - e.g. primitive, array, map, type, reference
	GetFieldType() string
}

// FieldMetaImpl implements FieldMeta
type FieldMetaImpl struct {
	// MergeType is the type of merge strategy to use for this field
	// maybe "merge", "replace" or "retainkeys"
	MergeType string

	// MergeKeys are the merge keys to use when the MergeType is "merge" and underlying type is a list
	MergeKeys MergeKeys

	// Type is the openapi type of the field - "list", "primitive", "map"
	Type string

	// Name contains name of the field
	Name string
}

// GetFieldMergeType implements FieldMeta.GetFieldMergeType
func (s FieldMetaImpl) GetFieldMergeType() string {
	return s.MergeType
}

// GetFieldMergeKeys implements FieldMeta.GetFieldMergeKeys
func (s FieldMetaImpl) GetFieldMergeKeys() MergeKeys {
	return s.MergeKeys
}

// GetFieldType implements FieldMeta.GetFieldType
func (s FieldMetaImpl) GetFieldType() string {
	return s.Type
}

// MergeKeyValue records the value of the mergekey for an item in a list
type MergeKeyValue map[string]string

// Equal returns true if the MergeKeyValues share the same value,
// representing the same item in a list
func (v MergeKeyValue) Equal(o MergeKeyValue) bool {
	if len(v) != len(o) {
		return false
	}

	for key, v1 := range v {
		if v2, found := o[key]; !found || v1 != v2 {
			return false
		}
	}

	return true
}

// MergeKeys is the set of fields on an object that uniquely identify
// and is used when merging lists to identify the "same" object
// independent of the ordering of the objects
type MergeKeys []string

// GetMergeKeyValue parses the MergeKeyValue from an item in a list
func (mk MergeKeys) GetMergeKeyValue(i interface{}) (MergeKeyValue, error) {
	result := MergeKeyValue{}
	if len(mk) <= 0 {
		return result, fmt.Errorf("merge key must have at least 1 value to merge")
	}
	m, ok := i.(map[string]interface{})
	if !ok {
		return result, fmt.Errorf("cannot use mergekey %v for primitive item in list %v", mk, i)
	}
	for _, field := range mk {
		if value, found := m[field]; !found {
			result[field] = ""
		} else {
			result[field] = fmt.Sprintf("%v", value)
		}
	}
	return result, nil
}

// CombinedPrimitiveSlice implements a slice of primitives
type CombinedPrimitiveSlice struct {
	Items []*PrimitiveListItem
}

// PrimitiveListItem represents a single value in a slice of primitives
type PrimitiveListItem struct {
	// Value is the value of the primitive, should match recorded, local and remote
	Value interface{}

	RawElementData
}

// Contains returns true if the slice contains the l
func (s *CombinedPrimitiveSlice) lookup(l interface{}) *PrimitiveListItem {
	val := fmt.Sprintf("%v", l)
	for _, i := range s.Items {
		if fmt.Sprintf("%v", i.Value) == val {
			return i
		}
	}
	return nil
}

func (s *CombinedPrimitiveSlice) upsert(l interface{}) *PrimitiveListItem {
	// Return the item if it exists
	if item := s.lookup(l); item != nil {
		return item
	}

	// Otherwise create a new item and append to the list
	item := &PrimitiveListItem{
		Value: l,
	}
	s.Items = append(s.Items, item)
	return item
}

// UpsertRecorded adds l to the slice.  If there is already a value of l in the
// slice for either the local or remote, set on that value as the recorded value
// Otherwise append a new item to the list with the recorded value.
func (s *CombinedPrimitiveSlice) UpsertRecorded(l interface{}) {
	v := s.upsert(l)
	v.recorded = l
	v.recordedSet = true
}

// UpsertLocal adds l to the slice.  If there is already a value of l in the
// slice for either the recorded or remote, set on that value as the local value
// Otherwise append a new item to the list with the local value.
func (s *CombinedPrimitiveSlice) UpsertLocal(l interface{}) {
	v := s.upsert(l)
	v.local = l
	v.localSet = true
}

// UpsertRemote adds l to the slice.  If there is already a value of l in the
// slice for either the local or recorded, set on that value as the remote value
// Otherwise append a new item to the list with the remote value.
func (s *CombinedPrimitiveSlice) UpsertRemote(l interface{}) {
	v := s.upsert(l)
	v.remote = l
	v.remoteSet = true
}

// ListItem represents a single value in a slice of maps or types
type ListItem struct {
	// KeyValue is the merge key value of the item
	KeyValue MergeKeyValue

	// RawElementData contains the field values
	RawElementData
}

// CombinedMapSlice is a slice of maps or types with merge keys
type CombinedMapSlice struct {
	Items []*ListItem
}

// Lookup returns the ListItem matching the merge key, or nil if not found.
func (s *CombinedMapSlice) lookup(v MergeKeyValue) *ListItem {
	for _, i := range s.Items {
		if i.KeyValue.Equal(v) {
			return i
		}
	}
	return nil
}

func (s *CombinedMapSlice) upsert(key MergeKeys, l interface{}) (*ListItem, error) {
	// Get the identity of the item
	val, err := key.GetMergeKeyValue(l)
	if err != nil {
		return nil, err
	}

	// Return the item if it exists
	if item := s.lookup(val); item != nil {
		return item, nil
	}

	// Otherwise create a new item and append to the list
	item := &ListItem{
		KeyValue: val,
	}
	s.Items = append(s.Items, item)
	return item, nil
}

// UpsertRecorded adds l to the slice.  If there is already a value of l sharing
// l's merge key in the slice for either the local or remote, set l the recorded value
// Otherwise append a new item to the list with the recorded value.
func (s *CombinedMapSlice) UpsertRecorded(key MergeKeys, l interface{}) error {
	item, err := s.upsert(key, l)
	if err != nil {
		return err
	}
	item.SetRecorded(l)
	return nil
}

// UpsertLocal adds l to the slice.  If there is already a value of l sharing
// l's merge key in the slice for either the recorded or remote, set l the local value
// Otherwise append a new item to the list with the local value.
func (s *CombinedMapSlice) UpsertLocal(key MergeKeys, l interface{}) error {
	item, err := s.upsert(key, l)
	if err != nil {
		return err
	}
	item.SetLocal(l)
	return nil
}

// UpsertRemote adds l to the slice.  If there is already a value of l sharing
// l's merge key in the slice for either the recorded or local, set l the remote value
// Otherwise append a new item to the list with the remote value.
func (s *CombinedMapSlice) UpsertRemote(key MergeKeys, l interface{}) error {
	item, err := s.upsert(key, l)
	if err != nil {
		return err
	}
	item.SetRemote(l)
	return nil
}

// IsDrop returns true if the field represented by e should be dropped from the merged object
func IsDrop(e Element) bool {
	// Specified in the last value recorded value and since deleted from the local
	removed := e.HasRecorded() && !e.HasLocal()

	// Specified locally and explicitly set to null
	setToNil := e.HasLocal() && e.GetLocal() == nil

	return removed || setToNil
}

// IsAdd returns true if the field represented by e should have the local value directly
// added to the merged object instead of merging the recorded, local and remote values
func IsAdd(e Element) bool {
	// If it isn't already present in the remote value and is present in the local value
	return e.HasLocal() && !e.HasRemote()
}

// NewRawElementData returns a new RawElementData, setting IsSet to true for
// non-nil values, and leaving IsSet false for nil values.
// Note: use this only when you want a nil-value to be considered "unspecified"
// (ignore) and not "unset" (deleted).
func NewRawElementData(recorded, local, remote interface{}) RawElementData {
	data := RawElementData{}
	if recorded != nil {
		data.SetRecorded(recorded)
	}
	if local != nil {
		data.SetLocal(local)
	}
	if remote != nil {
		data.SetRemote(remote)
	}
	return data
}

// RawElementData contains the raw recorded, local and remote data
// and metadata about whethere or not each was set
type RawElementData struct {
	HasElementData

	recorded interface{}
	local    interface{}
	remote   interface{}
}

// SetRecorded sets the recorded value
func (b *RawElementData) SetRecorded(value interface{}) {
	b.recorded = value
	b.recordedSet = true
}

// SetLocal sets the local value
func (b *RawElementData) SetLocal(value interface{}) {
	b.local = value
	b.localSet = true
}

// SetRemote sets the remote value
func (b *RawElementData) SetRemote(value interface{}) {
	b.remote = value
	b.remoteSet = true
}

// GetRecorded implements Element.GetRecorded
func (b RawElementData) GetRecorded() interface{} {
	// https://golang.org/doc/faq#nil_error
	if b.recorded == nil {
		return nil
	}
	return b.recorded
}

// GetLocal implements Element.GetLocal
func (b RawElementData) GetLocal() interface{} {
	// https://golang.org/doc/faq#nil_error
	if b.local == nil {
		return nil
	}
	return b.local
}

// GetRemote implements Element.GetRemote
func (b RawElementData) GetRemote() interface{} {
	// https://golang.org/doc/faq#nil_error
	if b.remote == nil {
		return nil
	}
	return b.remote
}

// HasElementData contains whether a field was set in the recorded, local and remote sources
type HasElementData struct {
	recordedSet bool
	localSet    bool
	remoteSet   bool
}

// HasRecorded implements Element.HasRecorded
func (e HasElementData) HasRecorded() bool {
	return e.recordedSet
}

// HasLocal implements Element.HasLocal
func (e HasElementData) HasLocal() bool {
	return e.localSet
}

// HasRemote implements Element.HasRemote
func (e HasElementData) HasRemote() bool {
	return e.remoteSet
}

// ConflictDetector defines the capability to detect conflict. An element can examine remote/recorded value to detect conflict.
type ConflictDetector interface {
	HasConflict() error
}
