/*
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"
	"strings"

	"k8s.io/kube-openapi/pkg/util/proto"
)

// Contains functions for casting openapi interfaces to their underlying types

// getSchemaType returns the string type of the schema - e.g. array, primitive, map, kind, reference
func getSchemaType(schema proto.Schema) string {
	if schema == nil {
		return ""
	}
	visitor := &baseSchemaVisitor{}
	schema.Accept(visitor)
	return visitor.Kind
}

// getKind converts schema to an *proto.Kind object
func getKind(schema proto.Schema) (*proto.Kind, error) {
	if schema == nil {
		return nil, nil
	}
	visitor := &kindSchemaVisitor{}
	schema.Accept(visitor)
	return visitor.Result, visitor.Err
}

// getArray converts schema to an *proto.Array object
func getArray(schema proto.Schema) (*proto.Array, error) {
	if schema == nil {
		return nil, nil
	}
	visitor := &arraySchemaVisitor{}
	schema.Accept(visitor)
	return visitor.Result, visitor.Err
}

// getMap converts schema to an *proto.Map object
func getMap(schema proto.Schema) (*proto.Map, error) {
	if schema == nil {
		return nil, nil
	}
	visitor := &mapSchemaVisitor{}
	schema.Accept(visitor)
	return visitor.Result, visitor.Err
}

// getPrimitive converts schema to an *proto.Primitive object
func getPrimitive(schema proto.Schema) (*proto.Primitive, error) {
	if schema == nil {
		return nil, nil
	}
	visitor := &primitiveSchemaVisitor{}
	schema.Accept(visitor)
	return visitor.Result, visitor.Err
}

type baseSchemaVisitor struct {
	Err  error
	Kind string
}

// VisitArray implements openapi
func (v *baseSchemaVisitor) VisitArray(array *proto.Array) {
	v.Kind = "array"
	v.Err = fmt.Errorf("Array type not expected")
}

// MergeMap implements openapi
func (v *baseSchemaVisitor) VisitMap(*proto.Map) {
	v.Kind = "map"
	v.Err = fmt.Errorf("Map type not expected")
}

// MergePrimitive implements openapi
func (v *baseSchemaVisitor) VisitPrimitive(*proto.Primitive) {
	v.Kind = "primitive"
	v.Err = fmt.Errorf("Primitive type not expected")
}

// VisitKind implements openapi
func (v *baseSchemaVisitor) VisitKind(*proto.Kind) {
	v.Kind = "kind"
	v.Err = fmt.Errorf("Kind type not expected")
}

// VisitReference implements openapi
func (v *baseSchemaVisitor) VisitReference(reference proto.Reference) {
	v.Kind = "reference"
	v.Err = fmt.Errorf("Reference type not expected")
}

type kindSchemaVisitor struct {
	baseSchemaVisitor
	Result *proto.Kind
}

// VisitKind implements openapi
func (v *kindSchemaVisitor) VisitKind(result *proto.Kind) {
	v.Result = result
	v.Kind = "kind"
}

// VisitReference implements openapi
func (v *kindSchemaVisitor) VisitReference(reference proto.Reference) {
	reference.SubSchema().Accept(v)
	if v.Err == nil {
		v.Err = copyExtensions(reference.GetPath().String(), reference.GetExtensions(), v.Result.Extensions)
	}
}

func copyExtensions(field string, from, to map[string]interface{}) error {
	// Copy extensions from field to type for references
	for key, val := range from {
		if curr, found := to[key]; found {
			// Don't allow the same extension to be defined both on the field and on the type
			return fmt.Errorf("Cannot override value for extension %s on field %s from %v to %v",
				key, field, curr, val)
		}
		to[key] = val
	}
	return nil
}

type mapSchemaVisitor struct {
	baseSchemaVisitor
	Result *proto.Map
}

// VisitMap implements openapi
func (v *mapSchemaVisitor) VisitMap(result *proto.Map) {
	v.Result = result
	v.Kind = "map"
}

// VisitReference implements openapi
func (v *mapSchemaVisitor) VisitReference(reference proto.Reference) {
	reference.SubSchema().Accept(v)
	if v.Err == nil {
		v.Err = copyExtensions(reference.GetPath().String(), reference.GetExtensions(), v.Result.Extensions)
	}
}

type arraySchemaVisitor struct {
	baseSchemaVisitor
	Result *proto.Array
}

// VisitArray implements openapi
func (v *arraySchemaVisitor) VisitArray(result *proto.Array) {
	v.Result = result
	v.Kind = "array"
	v.Err = copySubElementPatchStrategy(result.Path.String(), result.GetExtensions(), result.SubType.GetExtensions())
}

// copySubElementPatchStrategy copies the strategies to subelements to the subtype
// e.g. PodTemplate.Volumes is a []Volume with "x-kubernetes-patch-strategy": "merge,retainKeys"
// the "retainKeys" strategy applies to merging Volumes, and must be copied to the sub element
func copySubElementPatchStrategy(field string, from, to map[string]interface{}) error {
	// Check if the parent has a patch strategy extension
	if ext, found := from["x-kubernetes-patch-strategy"]; found {
		strategy, ok := ext.(string)
		if !ok {
			return fmt.Errorf("Expected string value for x-kubernetes-patch-strategy on %s, was %T",
				field, ext)
		}
		// Check of the parent patch strategy has a sub patch strategy, and if so copy to the sub type
		if strings.Contains(strategy, ",") {
			strategies := strings.Split(strategy, ",")
			if len(strategies) != 2 {
				// Only 1 sub strategy is supported
				return fmt.Errorf(
					"Expected between 0 and 2 elements for x-kubernetes-patch-merge-strategy by got %v",
					strategies)
			}
			to["x-kubernetes-patch-strategy"] = strategies[1]
		}
	}
	return nil
}

// VisitReference implements openapi
func (v *arraySchemaVisitor) VisitReference(reference proto.Reference) {
	reference.SubSchema().Accept(v)
	if v.Err == nil {
		v.Err = copyExtensions(reference.GetPath().String(), reference.GetExtensions(), v.Result.Extensions)
	}
}

type primitiveSchemaVisitor struct {
	baseSchemaVisitor
	Result *proto.Primitive
}

// VisitPrimitive implements openapi
func (v *primitiveSchemaVisitor) VisitPrimitive(result *proto.Primitive) {
	v.Result = result
	v.Kind = "primitive"
}

// VisitReference implements openapi
func (v *primitiveSchemaVisitor) VisitReference(reference proto.Reference) {
	reference.SubSchema().Accept(v)
	if v.Err == nil {
		v.Err = copyExtensions(reference.GetPath().String(), reference.GetExtensions(), v.Result.Extensions)
	}
}
