/*
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 parse

import (
	"fmt"
	"reflect"

	"k8s.io/kube-openapi/pkg/util/proto"
	"k8s.io/kubernetes/pkg/kubectl/apply"
	"k8s.io/kubernetes/pkg/kubectl/cmd/util/openapi"
)

// Factory creates an Element by combining object values from recorded, local and remote sources with
// the metadata from an openapi schema.
type Factory struct {
	// Resources contains the openapi field metadata for the object models
	Resources openapi.Resources
}

// CreateElement returns an Element by collating the recorded, local and remote field values
func (b *Factory) CreateElement(recorded, local, remote map[string]interface{}) (apply.Element, error) {
	// Create an Item from the 3 values.  Use empty name for field
	visitor := &ElementBuildingVisitor{b.Resources}

	gvk, err := getCommonGroupVersionKind(recorded, local, remote)
	if err != nil {
		return nil, err
	}

	// Get the openapi object metadata
	s := visitor.resources.LookupResource(gvk)
	oapiKind, err := getKind(s)
	if err != nil {
		return nil, err
	}

	data := apply.NewRawElementData(recorded, local, remote)
	fieldName := ""
	item, err := visitor.getItem(oapiKind, fieldName, data)
	if err != nil {
		return nil, err
	}

	// Collate each field of the item into a combined Element
	return item.CreateElement(visitor)
}

// getItem returns the appropriate Item based on the underlying type of the arguments
func (v *ElementBuildingVisitor) getItem(s proto.Schema, name string, data apply.RawElementData) (Item, error) {
	kind, err := getType(data.GetRecorded(), data.GetLocal(), data.GetRemote())
	if err != nil {
		return nil, err
	}
	if kind == nil {
		// All of the items values are nil.
		return &emptyItem{Name: name}, nil
	}

	// Create an item matching the type
	switch kind.Kind() {
	case reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint,
		reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Float32, reflect.Float64,
		reflect.String:
		p, err := getPrimitive(s)
		if err != nil {
			return nil, fmt.Errorf("expected openapi Primitive, was %T for %v (%v)", s, kind, err)
		}
		return &primitiveItem{name, p, data}, nil
	case reflect.Array, reflect.Slice:
		a, err := getArray(s)
		if err != nil {
			return nil, fmt.Errorf("expected openapi Array, was %T for %v (%v)", s, kind, err)
		}
		return &listItem{
			Name:  name,
			Array: a,
			ListElementData: apply.ListElementData{
				RawElementData: data,
			},
		}, nil
	case reflect.Map:
		if k, err := getKind(s); err == nil {
			return &typeItem{
				Name: name,
				Type: k,
				MapElementData: apply.MapElementData{
					RawElementData: data,
				},
			}, nil
		}
		// If it looks like a map, and no openapi type is found, default to mapItem
		m, err := getMap(s)
		if err != nil {
			return nil, fmt.Errorf("expected openapi Kind or Map, was %T for %v (%v)", s, kind, err)
		}
		return &mapItem{
			Name: name,
			Map:  m,
			MapElementData: apply.MapElementData{
				RawElementData: data,
			},
		}, nil
	}
	return nil, fmt.Errorf("unsupported type %v", kind)
}
