// Copyright 2017 Google LLC. All Rights Reserved.
//
// 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 jsonschema

import (
	"fmt"
	"log"
	"strings"
)

//
// OPERATIONS
// The following methods perform operations on Schemas.
//

// IsEmpty returns true if no members of the Schema are specified.
func (schema *Schema) IsEmpty() bool {
	return (schema.Schema == nil) &&
		(schema.ID == nil) &&
		(schema.MultipleOf == nil) &&
		(schema.Maximum == nil) &&
		(schema.ExclusiveMaximum == nil) &&
		(schema.Minimum == nil) &&
		(schema.ExclusiveMinimum == nil) &&
		(schema.MaxLength == nil) &&
		(schema.MinLength == nil) &&
		(schema.Pattern == nil) &&
		(schema.AdditionalItems == nil) &&
		(schema.Items == nil) &&
		(schema.MaxItems == nil) &&
		(schema.MinItems == nil) &&
		(schema.UniqueItems == nil) &&
		(schema.MaxProperties == nil) &&
		(schema.MinProperties == nil) &&
		(schema.Required == nil) &&
		(schema.AdditionalProperties == nil) &&
		(schema.Properties == nil) &&
		(schema.PatternProperties == nil) &&
		(schema.Dependencies == nil) &&
		(schema.Enumeration == nil) &&
		(schema.Type == nil) &&
		(schema.AllOf == nil) &&
		(schema.AnyOf == nil) &&
		(schema.OneOf == nil) &&
		(schema.Not == nil) &&
		(schema.Definitions == nil) &&
		(schema.Title == nil) &&
		(schema.Description == nil) &&
		(schema.Default == nil) &&
		(schema.Format == nil) &&
		(schema.Ref == nil)
}

// IsEqual returns true if two schemas are equal.
func (schema *Schema) IsEqual(schema2 *Schema) bool {
	return schema.String() == schema2.String()
}

// SchemaOperation represents a function that can be applied to a Schema.
type SchemaOperation func(schema *Schema, context string)

// Applies a specified function to a Schema and all of the Schemas that it contains.
func (schema *Schema) applyToSchemas(operation SchemaOperation, context string) {

	if schema.AdditionalItems != nil {
		s := schema.AdditionalItems.Schema
		if s != nil {
			s.applyToSchemas(operation, "AdditionalItems")
		}
	}

	if schema.Items != nil {
		if schema.Items.SchemaArray != nil {
			for _, s := range *(schema.Items.SchemaArray) {
				s.applyToSchemas(operation, "Items.SchemaArray")
			}
		} else if schema.Items.Schema != nil {
			schema.Items.Schema.applyToSchemas(operation, "Items.Schema")
		}
	}

	if schema.AdditionalProperties != nil {
		s := schema.AdditionalProperties.Schema
		if s != nil {
			s.applyToSchemas(operation, "AdditionalProperties")
		}
	}

	if schema.Properties != nil {
		for _, pair := range *(schema.Properties) {
			s := pair.Value
			s.applyToSchemas(operation, "Properties")
		}
	}
	if schema.PatternProperties != nil {
		for _, pair := range *(schema.PatternProperties) {
			s := pair.Value
			s.applyToSchemas(operation, "PatternProperties")
		}
	}

	if schema.Dependencies != nil {
		for _, pair := range *(schema.Dependencies) {
			schemaOrStringArray := pair.Value
			s := schemaOrStringArray.Schema
			if s != nil {
				s.applyToSchemas(operation, "Dependencies")
			}
		}
	}

	if schema.AllOf != nil {
		for _, s := range *(schema.AllOf) {
			s.applyToSchemas(operation, "AllOf")
		}
	}
	if schema.AnyOf != nil {
		for _, s := range *(schema.AnyOf) {
			s.applyToSchemas(operation, "AnyOf")
		}
	}
	if schema.OneOf != nil {
		for _, s := range *(schema.OneOf) {
			s.applyToSchemas(operation, "OneOf")
		}
	}
	if schema.Not != nil {
		schema.Not.applyToSchemas(operation, "Not")
	}

	if schema.Definitions != nil {
		for _, pair := range *(schema.Definitions) {
			s := pair.Value
			s.applyToSchemas(operation, "Definitions")
		}
	}

	operation(schema, context)
}

// CopyProperties copies all non-nil properties from the source Schema to the schema Schema.
func (schema *Schema) CopyProperties(source *Schema) {
	if source.Schema != nil {
		schema.Schema = source.Schema
	}
	if source.ID != nil {
		schema.ID = source.ID
	}
	if source.MultipleOf != nil {
		schema.MultipleOf = source.MultipleOf
	}
	if source.Maximum != nil {
		schema.Maximum = source.Maximum
	}
	if source.ExclusiveMaximum != nil {
		schema.ExclusiveMaximum = source.ExclusiveMaximum
	}
	if source.Minimum != nil {
		schema.Minimum = source.Minimum
	}
	if source.ExclusiveMinimum != nil {
		schema.ExclusiveMinimum = source.ExclusiveMinimum
	}
	if source.MaxLength != nil {
		schema.MaxLength = source.MaxLength
	}
	if source.MinLength != nil {
		schema.MinLength = source.MinLength
	}
	if source.Pattern != nil {
		schema.Pattern = source.Pattern
	}
	if source.AdditionalItems != nil {
		schema.AdditionalItems = source.AdditionalItems
	}
	if source.Items != nil {
		schema.Items = source.Items
	}
	if source.MaxItems != nil {
		schema.MaxItems = source.MaxItems
	}
	if source.MinItems != nil {
		schema.MinItems = source.MinItems
	}
	if source.UniqueItems != nil {
		schema.UniqueItems = source.UniqueItems
	}
	if source.MaxProperties != nil {
		schema.MaxProperties = source.MaxProperties
	}
	if source.MinProperties != nil {
		schema.MinProperties = source.MinProperties
	}
	if source.Required != nil {
		schema.Required = source.Required
	}
	if source.AdditionalProperties != nil {
		schema.AdditionalProperties = source.AdditionalProperties
	}
	if source.Properties != nil {
		schema.Properties = source.Properties
	}
	if source.PatternProperties != nil {
		schema.PatternProperties = source.PatternProperties
	}
	if source.Dependencies != nil {
		schema.Dependencies = source.Dependencies
	}
	if source.Enumeration != nil {
		schema.Enumeration = source.Enumeration
	}
	if source.Type != nil {
		schema.Type = source.Type
	}
	if source.AllOf != nil {
		schema.AllOf = source.AllOf
	}
	if source.AnyOf != nil {
		schema.AnyOf = source.AnyOf
	}
	if source.OneOf != nil {
		schema.OneOf = source.OneOf
	}
	if source.Not != nil {
		schema.Not = source.Not
	}
	if source.Definitions != nil {
		schema.Definitions = source.Definitions
	}
	if source.Title != nil {
		schema.Title = source.Title
	}
	if source.Description != nil {
		schema.Description = source.Description
	}
	if source.Default != nil {
		schema.Default = source.Default
	}
	if source.Format != nil {
		schema.Format = source.Format
	}
	if source.Ref != nil {
		schema.Ref = source.Ref
	}
}

// TypeIs returns true if the Type of a Schema includes the specified type
func (schema *Schema) TypeIs(typeName string) bool {
	if schema.Type != nil {
		// the schema Type is either a string or an array of strings
		if schema.Type.String != nil {
			return (*(schema.Type.String) == typeName)
		} else if schema.Type.StringArray != nil {
			for _, n := range *(schema.Type.StringArray) {
				if n == typeName {
					return true
				}
			}
		}
	}
	return false
}

// ResolveRefs resolves "$ref" elements in a Schema and its children.
// But if a reference refers to an object type, is inside a oneOf, or contains a oneOf,
// the reference is kept and we expect downstream tools to separately model these
// referenced schemas.
func (schema *Schema) ResolveRefs() {
	rootSchema := schema
	count := 1
	for count > 0 {
		count = 0
		schema.applyToSchemas(
			func(schema *Schema, context string) {
				if schema.Ref != nil {
					resolvedRef, err := rootSchema.resolveJSONPointer(*(schema.Ref))
					if err != nil {
						log.Printf("%+v", err)
					} else if resolvedRef.TypeIs("object") {
						// don't substitute for objects, we'll model the referenced schema with a class
					} else if context == "OneOf" {
						// don't substitute for references inside oneOf declarations
					} else if resolvedRef.OneOf != nil {
						// don't substitute for references that contain oneOf declarations
					} else if resolvedRef.AdditionalProperties != nil {
						// don't substitute for references that look like objects
					} else {
						schema.Ref = nil
						schema.CopyProperties(resolvedRef)
						count++
					}
				}
			}, "")
	}
}

// resolveJSONPointer resolves JSON pointers.
// This current implementation is very crude and custom for OpenAPI 2.0 schemas.
// It panics for any pointer that it is unable to resolve.
func (schema *Schema) resolveJSONPointer(ref string) (result *Schema, err error) {
	parts := strings.Split(ref, "#")
	if len(parts) == 2 {
		documentName := parts[0] + "#"
		if documentName == "#" && schema.ID != nil {
			documentName = *(schema.ID)
		}
		path := parts[1]
		document := schemas[documentName]
		pathParts := strings.Split(path, "/")

		// we currently do a very limited (hard-coded) resolution of certain paths and log errors for missed cases
		if len(pathParts) == 1 {
			return document, nil
		} else if len(pathParts) == 3 {
			switch pathParts[1] {
			case "definitions":
				dictionary := document.Definitions
				for _, pair := range *dictionary {
					if pair.Name == pathParts[2] {
						result = pair.Value
					}
				}
			case "properties":
				dictionary := document.Properties
				for _, pair := range *dictionary {
					if pair.Name == pathParts[2] {
						result = pair.Value
					}
				}
			default:
				break
			}
		}
	}
	if result == nil {
		return nil, fmt.Errorf("unresolved pointer: %+v", ref)
	}
	return result, nil
}

// ResolveAllOfs replaces "allOf" elements by merging their properties into the parent Schema.
func (schema *Schema) ResolveAllOfs() {
	schema.applyToSchemas(
		func(schema *Schema, context string) {
			if schema.AllOf != nil {
				for _, allOf := range *(schema.AllOf) {
					schema.CopyProperties(allOf)
				}
				schema.AllOf = nil
			}
		}, "resolveAllOfs")
}

// ResolveAnyOfs replaces all "anyOf" elements with "oneOf".
func (schema *Schema) ResolveAnyOfs() {
	schema.applyToSchemas(
		func(schema *Schema, context string) {
			if schema.AnyOf != nil {
				schema.OneOf = schema.AnyOf
				schema.AnyOf = nil
			}
		}, "resolveAnyOfs")
}

// return a pointer to a copy of a passed-in string
func stringptr(input string) (output *string) {
	return &input
}

// CopyOfficialSchemaProperty copies a named property from the official JSON Schema definition
func (schema *Schema) CopyOfficialSchemaProperty(name string) {
	*schema.Properties = append(*schema.Properties,
		NewNamedSchema(name,
			&Schema{Ref: stringptr("http://json-schema.org/draft-04/schema#/properties/" + name)}))
}

// CopyOfficialSchemaProperties copies named properties from the official JSON Schema definition
func (schema *Schema) CopyOfficialSchemaProperties(names []string) {
	for _, name := range names {
		schema.CopyOfficialSchemaProperty(name)
	}
}
