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

	"gopkg.in/yaml.v3"
)

const indentation = "  "

func renderMappingNode(node *yaml.Node, indent string) (result string) {
	result = "{\n"
	innerIndent := indent + indentation
	for i := 0; i < len(node.Content); i += 2 {
		// first print the key
		key := node.Content[i].Value
		result += fmt.Sprintf("%s\"%+v\": ", innerIndent, key)
		// then the value
		value := node.Content[i+1]
		switch value.Kind {
		case yaml.ScalarNode:
			if value.Tag == "!!bool" {
				result += value.Value
			} else {
				result += "\"" + value.Value + "\""
			}
		case yaml.MappingNode:
			result += renderMappingNode(value, innerIndent)
		case yaml.SequenceNode:
			result += renderSequenceNode(value, innerIndent)
		default:
			result += fmt.Sprintf("???MapItem(Key:%+v, Value:%T)", value, value)
		}
		if i < len(node.Content)-2 {
			result += ","
		}
		result += "\n"
	}

	result += indent + "}"
	return result
}

func renderSequenceNode(node *yaml.Node, indent string) (result string) {
	result = "[\n"
	innerIndent := indent + indentation
	for i := 0; i < len(node.Content); i++ {
		item := node.Content[i]
		switch item.Kind {
		case yaml.ScalarNode:
			if item.Tag == "!!bool" {
				result += innerIndent + item.Value
			} else {
				result += innerIndent + "\"" + item.Value + "\""
			}
		case yaml.MappingNode:
			result += innerIndent + renderMappingNode(item, innerIndent) + ""
		default:
			result += innerIndent + fmt.Sprintf("???ArrayItem(%+v)", item)
		}
		if i < len(node.Content)-1 {
			result += ","
		}
		result += "\n"
	}
	result += indent + "]"
	return result
}

func renderStringArray(array []string, indent string) (result string) {
	result = "[\n"
	innerIndent := indent + indentation
	for i, item := range array {
		result += innerIndent + "\"" + item + "\""
		if i < len(array)-1 {
			result += ","
		}
		result += "\n"
	}
	result += indent + "]"
	return result
}

// Render renders a yaml.Node as JSON
func Render(node *yaml.Node) string {
	if node.Kind == yaml.DocumentNode {
		if len(node.Content) == 1 {
			return Render(node.Content[0])
		}
	} else if node.Kind == yaml.MappingNode {
		return renderMappingNode(node, "") + "\n"
	} else if node.Kind == yaml.SequenceNode {
		return renderSequenceNode(node, "") + "\n"
	}
	return ""
}

func (object *SchemaNumber) nodeValue() *yaml.Node {
	if object.Integer != nil {
		return nodeForInt64(*object.Integer)
	} else if object.Float != nil {
		return nodeForFloat64(*object.Float)
	} else {
		return nil
	}
}

func (object *SchemaOrBoolean) nodeValue() *yaml.Node {
	if object.Schema != nil {
		return object.Schema.nodeValue()
	} else if object.Boolean != nil {
		return nodeForBoolean(*object.Boolean)
	} else {
		return nil
	}
}

func nodeForStringArray(array []string) *yaml.Node {
	content := make([]*yaml.Node, 0)
	for _, item := range array {
		content = append(content, nodeForString(item))
	}
	return nodeForSequence(content)
}

func nodeForSchemaArray(array []*Schema) *yaml.Node {
	content := make([]*yaml.Node, 0)
	for _, item := range array {
		content = append(content, item.nodeValue())
	}
	return nodeForSequence(content)
}

func (object *StringOrStringArray) nodeValue() *yaml.Node {
	if object.String != nil {
		return nodeForString(*object.String)
	} else if object.StringArray != nil {
		return nodeForStringArray(*(object.StringArray))
	} else {
		return nil
	}
}

func (object *SchemaOrStringArray) nodeValue() *yaml.Node {
	if object.Schema != nil {
		return object.Schema.nodeValue()
	} else if object.StringArray != nil {
		return nodeForStringArray(*(object.StringArray))
	} else {
		return nil
	}
}

func (object *SchemaOrSchemaArray) nodeValue() *yaml.Node {
	if object.Schema != nil {
		return object.Schema.nodeValue()
	} else if object.SchemaArray != nil {
		return nodeForSchemaArray(*(object.SchemaArray))
	} else {
		return nil
	}
}

func (object *SchemaEnumValue) nodeValue() *yaml.Node {
	if object.String != nil {
		return nodeForString(*object.String)
	} else if object.Bool != nil {
		return nodeForBoolean(*object.Bool)
	} else {
		return nil
	}
}

func nodeForNamedSchemaArray(array *[]*NamedSchema) *yaml.Node {
	content := make([]*yaml.Node, 0)
	for _, pair := range *(array) {
		content = appendPair(content, pair.Name, pair.Value.nodeValue())
	}
	return nodeForMapping(content)
}

func nodeForNamedSchemaOrStringArray(array *[]*NamedSchemaOrStringArray) *yaml.Node {
	content := make([]*yaml.Node, 0)
	for _, pair := range *(array) {
		content = appendPair(content, pair.Name, pair.Value.nodeValue())
	}
	return nodeForMapping(content)
}

func nodeForSchemaEnumArray(array *[]SchemaEnumValue) *yaml.Node {
	content := make([]*yaml.Node, 0)
	for _, item := range *array {
		content = append(content, item.nodeValue())
	}
	return nodeForSequence(content)
}

func nodeForMapping(content []*yaml.Node) *yaml.Node {
	return &yaml.Node{
		Kind:    yaml.MappingNode,
		Content: content,
	}
}

func nodeForSequence(content []*yaml.Node) *yaml.Node {
	return &yaml.Node{
		Kind:    yaml.SequenceNode,
		Content: content,
	}
}

func nodeForString(value string) *yaml.Node {
	return &yaml.Node{
		Kind:  yaml.ScalarNode,
		Tag:   "!!str",
		Value: value,
	}
}

func nodeForBoolean(value bool) *yaml.Node {
	return &yaml.Node{
		Kind:  yaml.ScalarNode,
		Tag:   "!!bool",
		Value: fmt.Sprintf("%t", value),
	}
}

func nodeForInt64(value int64) *yaml.Node {
	return &yaml.Node{
		Kind:  yaml.ScalarNode,
		Tag:   "!!int",
		Value: fmt.Sprintf("%d", value),
	}
}

func nodeForFloat64(value float64) *yaml.Node {
	return &yaml.Node{
		Kind:  yaml.ScalarNode,
		Tag:   "!!float",
		Value: fmt.Sprintf("%f", value),
	}
}

func appendPair(nodes []*yaml.Node, name string, value *yaml.Node) []*yaml.Node {
	nodes = append(nodes, nodeForString(name))
	nodes = append(nodes, value)
	return nodes
}

func (schema *Schema) nodeValue() *yaml.Node {
	n := &yaml.Node{Kind: yaml.MappingNode}
	content := make([]*yaml.Node, 0)
	if schema.Title != nil {
		content = appendPair(content, "title", nodeForString(*schema.Title))
	}
	if schema.ID != nil {
		switch strings.TrimSuffix(*schema.Schema, "#") {
		case "http://json-schema.org/draft-04/schema":
			fallthrough
		case "#":
			fallthrough
		case "":
			content = appendPair(content, "id", nodeForString(*schema.ID))
		default:
			content = appendPair(content, "$id", nodeForString(*schema.ID))
		}
	}
	if schema.Schema != nil {
		content = appendPair(content, "$schema", nodeForString(*schema.Schema))
	}
	if schema.ReadOnly != nil && *schema.ReadOnly {
		content = appendPair(content, "readOnly", nodeForBoolean(*schema.ReadOnly))
	}
	if schema.WriteOnly != nil && *schema.WriteOnly {
		content = appendPair(content, "writeOnly", nodeForBoolean(*schema.WriteOnly))
	}
	if schema.Type != nil {
		content = appendPair(content, "type", schema.Type.nodeValue())
	}
	if schema.Items != nil {
		content = appendPair(content, "items", schema.Items.nodeValue())
	}
	if schema.Description != nil {
		content = appendPair(content, "description", nodeForString(*schema.Description))
	}
	if schema.Required != nil {
		content = appendPair(content, "required", nodeForStringArray(*schema.Required))
	}
	if schema.AdditionalProperties != nil {
		content = appendPair(content, "additionalProperties", schema.AdditionalProperties.nodeValue())
	}
	if schema.PatternProperties != nil {
		content = appendPair(content, "patternProperties", nodeForNamedSchemaArray(schema.PatternProperties))
	}
	if schema.Properties != nil {
		content = appendPair(content, "properties", nodeForNamedSchemaArray(schema.Properties))
	}
	if schema.Dependencies != nil {
		content = appendPair(content, "dependencies", nodeForNamedSchemaOrStringArray(schema.Dependencies))
	}
	if schema.Ref != nil {
		content = appendPair(content, "$ref", nodeForString(*schema.Ref))
	}
	if schema.MultipleOf != nil {
		content = appendPair(content, "multipleOf", schema.MultipleOf.nodeValue())
	}
	if schema.Maximum != nil {
		content = appendPair(content, "maximum", schema.Maximum.nodeValue())
	}
	if schema.ExclusiveMaximum != nil {
		content = appendPair(content, "exclusiveMaximum", nodeForBoolean(*schema.ExclusiveMaximum))
	}
	if schema.Minimum != nil {
		content = appendPair(content, "minimum", schema.Minimum.nodeValue())
	}
	if schema.ExclusiveMinimum != nil {
		content = appendPair(content, "exclusiveMinimum", nodeForBoolean(*schema.ExclusiveMinimum))
	}
	if schema.MaxLength != nil {
		content = appendPair(content, "maxLength", nodeForInt64(*schema.MaxLength))
	}
	if schema.MinLength != nil {
		content = appendPair(content, "minLength", nodeForInt64(*schema.MinLength))
	}
	if schema.Pattern != nil {
		content = appendPair(content, "pattern", nodeForString(*schema.Pattern))
	}
	if schema.AdditionalItems != nil {
		content = appendPair(content, "additionalItems", schema.AdditionalItems.nodeValue())
	}
	if schema.MaxItems != nil {
		content = appendPair(content, "maxItems", nodeForInt64(*schema.MaxItems))
	}
	if schema.MinItems != nil {
		content = appendPair(content, "minItems", nodeForInt64(*schema.MinItems))
	}
	if schema.UniqueItems != nil {
		content = appendPair(content, "uniqueItems", nodeForBoolean(*schema.UniqueItems))
	}
	if schema.MaxProperties != nil {
		content = appendPair(content, "maxProperties", nodeForInt64(*schema.MaxProperties))
	}
	if schema.MinProperties != nil {
		content = appendPair(content, "minProperties", nodeForInt64(*schema.MinProperties))
	}
	if schema.Enumeration != nil {
		content = appendPair(content, "enum", nodeForSchemaEnumArray(schema.Enumeration))
	}
	if schema.AllOf != nil {
		content = appendPair(content, "allOf", nodeForSchemaArray(*schema.AllOf))
	}
	if schema.AnyOf != nil {
		content = appendPair(content, "anyOf", nodeForSchemaArray(*schema.AnyOf))
	}
	if schema.OneOf != nil {
		content = appendPair(content, "oneOf", nodeForSchemaArray(*schema.OneOf))
	}
	if schema.Not != nil {
		content = appendPair(content, "not", schema.Not.nodeValue())
	}
	if schema.Definitions != nil {
		content = appendPair(content, "definitions", nodeForNamedSchemaArray(schema.Definitions))
	}
	if schema.Default != nil {
		// m = append(m, yaml.MapItem{Key: "default", Value: *schema.Default})
	}
	if schema.Format != nil {
		content = appendPair(content, "format", nodeForString(*schema.Format))
	}
	n.Content = content
	return n
}

// JSONString returns a json representation of a schema.
func (schema *Schema) JSONString() string {
	node := schema.nodeValue()
	return Render(node)
}
