package jsonschema

import (
	"encoding/json"
	"regexp"

	"github.com/buger/jsonparser"
)

func (s *Schema) Parse(jsonSchema []byte) (*Schema, error) {
	schema := &Schema{raw: jsonSchema, circularThreshold: 3}

	if s == nil {
		schema.pointers = &pointers{}
		schema.refs = &refs{}

	} else {
		schema.parent = s

		// Set the base schema
		if s.baseURI != nil || s.base == nil {
			schema.base = s
		} else if s.base != nil {
			schema.base = s.base
		}

		// Set the root schema
		if s.root != nil {
			schema.root = s.root
		} else {
			schema.root = s
		}
	}

	if b, err := jsonparser.ParseBoolean(jsonSchema); err == nil {
		schema.boolean = &b
		schema.setupValidators()
		return schema, nil
	}

	var errs error

	// Simplest solution for getting ($)id or $ref, which is needed for separation and de-ref'ing
	jsonparser.EachKey(jsonSchema, func(idx int, value []byte, vt jsonparser.ValueType, err error) {
		if err != nil {
			errs = addError(err, errs)
			return
		}

		switch SchemaProp(idx) {
		case PropID:
			schema.ID = NewStringPtr(value)
		case PropIDDraft04:
			schema.IDDraft04 = NewStringPtr(value)
		case PropRef:
			schema.Ref, err = NewRef(value, vt, schema)
			errs = addError(err, errs)
		}
	}, [][]string{PropID: {"$id"}, PropIDDraft04: {"id"}, PropRef: {"$ref"}}...)

	id := schema.GetID()

	if id != "" && schema.Ref == nil {
		if len(id) > 0 && id[:1] == "#" {
			// Do not expand or change base uri
			if s != nil {
				s.setPointer(id, schema)
			}
		} else {
			var err error
			schema.baseURI, err = s.ExpandURI(id)
			if err != nil {
				addError(err, errs)
				return nil, errs
			}

			schema.pointers = &pointers{"#": schema}

			if s != nil {
				s.setPointer(schema.baseURI.String(), schema)
			} else {
				schema.setPointer(schema.baseURI.String(), schema)
			}
		}
	}

	jsonparser.ObjectEach(jsonSchema, func(key []byte, value []byte, vt jsonparser.ValueType, offset int) error {
		var err error

		schemaProp, ok := nameToProp[string(key)]
		if !ok {
			if schema.unknownProps == nil {
				schema.unknownProps = []*NamedValue{}
			}
			newVal, err := NewValue(value, vt)
			schema.unknownProps = append(schema.unknownProps, &NamedValue{Name: string(key), Value: newVal})
			errs = addError(err, errs)
			return errs
		}

		switch schemaProp {
		case PropSchema:
			schema.Schema = NewStringPtr(value)
		case PropComment:
			schema.Comment = NewStringPtr(value)
		case PropTitle:
			schema.Title = NewStringPtr(value)
		case PropDescription:
			schema.Description = NewStringPtr(value)
		case PropType:
			schema.Type, err = NewType(value, vt)
			errs = addError(err, errs)
		case PropEnum:
			schema.Enum, err = NewEnum(value, vt)
			errs = addError(err, errs)
		case PropDefault:
			schema.Default, err = NewValue(value, vt)
			errs = addError(err, errs)
		case PropExamples:
			schema.Examples, err = NewValues(value, vt)
			errs = addError(err, errs)
		case PropConst:
			schema.Const, err = NewValue(value, vt)
			errs = addError(err, errs)
		case PropReadOnly:
			tmpBool, err := jsonparser.ParseBoolean(value)
			schema.ReadOnly = &tmpBool
			errs = addError(err, errs)
		case PropWriteOnly:
			tmpBool, err := jsonparser.ParseBoolean(value)
			schema.WriteOnly = &tmpBool
			errs = addError(err, errs)
		case PropDefinitions:
			schema.Definitions, err = NewProperties(value, vt, schema)
			errs = addError(err, errs)
		case PropIf:
			schema.If, err = schema.Parse(value)
			errs = addError(err, errs)
		case PropThen:
			schema.Then, err = schema.Parse(value)
			errs = addError(err, errs)
		case PropElse:
			schema.Else, err = schema.Parse(value)
			errs = addError(err, errs)
		case PropAllOf:
			schema.AllOf, err = NewSubSchemas(value, vt, schema)
			errs = addError(err, errs)
		case PropAnyOf:
			schema.AnyOf, err = NewSubSchemas(value, vt, schema)
			errs = addError(err, errs)
		case PropOneOf:
			schema.OneOf, err = NewSubSchemas(value, vt, schema)
			errs = addError(err, errs)
		case PropNot:
			schema.Not, err = schema.Parse(value)
			errs = addError(err, errs)
		case PropContentEncoding:
			schema.ContentEncoding = NewStringPtr(value)
		case PropContentMediaType:
			schema.ContentMediaType = NewStringPtr(value)
		case PropProperties:
			schema.Properties, err = NewProperties(value, vt, schema)
			errs = addError(err, errs)
		case PropRequired:
			schema.Required, err = NewStrings(value, vt)
			errs = addError(err, errs)
		case PropMaxProperties:
			tmpInt, err := jsonparser.ParseInt(value)
			schema.MaxProperties = &tmpInt
			errs = addError(err, errs)
		case PropMinProperties:
			tmpInt, err := jsonparser.ParseInt(value)
			schema.MinProperties = &tmpInt
			errs = addError(err, errs)
		case PropDependencies:
			schema.Dependencies, err = NewDependencies(value, vt, schema)
			errs = addError(err, errs)
		case PropPatternProperties:
			schema.PatternProperties, err = NewProperties(value, vt, schema)
			errs = addError(err, errs)
			// Pre-compile the regexps
			if schema.PatternProperties != nil {
				schema.patternPropertiesRegexps = &map[string]*regexp.Regexp{}
				for _, prop := range *schema.PatternProperties {
					reStr := convertRegexp(prop.Name)
					re, err := regexp.Compile(reStr)
					if err != nil {
						errs = addError(err, errs)
					} else {
						(*schema.patternPropertiesRegexps)[prop.Name] = re
					}
				}
			}
		case PropAdditionalProperties:
			schema.AdditionalProperties, err = schema.Parse(value)
			errs = addError(err, errs)
		case PropPropertyNames:
			schema.PropertyNames, err = schema.Parse(value)
			errs = addError(err, errs)
		case PropItems:
			schema.Items, err = NewItems(value, vt, schema)
			errs = addError(err, errs)
		case PropMaxItems:
			tmpInt, err := jsonparser.ParseInt(value)
			schema.MaxItems = &tmpInt
			errs = addError(err, errs)
		case PropMinItems:
			tmpInt, err := jsonparser.ParseInt(value)
			schema.MinItems = &tmpInt
			errs = addError(err, errs)
		case PropUniqueItems:
			tmpBool, err := jsonparser.ParseBoolean(value)
			schema.UniqueItems = &tmpBool
			errs = addError(err, errs)
		case PropAdditionalItems:
			schema.AdditionalItems, err = schema.Parse(value)
			errs = addError(err, errs)
		case PropContains:
			schema.Contains, err = schema.Parse(value)
			errs = addError(err, errs)
		case PropMaxLength:
			tmpInt, err := jsonparser.ParseInt(value)
			schema.MaxLength = &tmpInt
			errs = addError(err, errs)
		case PropMinLength:
			tmpInt, err := jsonparser.ParseInt(value)
			schema.MinLength = &tmpInt
			errs = addError(err, errs)
		case PropFormat:
			schema.Format = NewStringPtr(value)
		case PropPattern:
			schema.Pattern = NewStringPtr(value)
			reStr := convertRegexp(*schema.Pattern)
			re, err := regexp.Compile(reStr)
			if err != nil {
				errs = addError(err, errs)
			} else {
				schema.patternRegexp = re
			}
		case PropMultipleOf:
			tmpNum := json.Number(string(value))
			schema.MultipleOf = &tmpNum
		case PropMaximum:
			schema.Maximum, err = NewValue(value, vt)
			errs = addError(err, errs)
		case PropExclusiveMaximum:
			schema.ExclusiveMaximum, err = NewValue(value, vt)
			errs = addError(err, errs)
		case PropMinimum:
			schema.Minimum, err = NewValue(value, vt)
			errs = addError(err, errs)
		case PropExclusiveMinimum:
			schema.ExclusiveMinimum, err = NewValue(value, vt)
			errs = addError(err, errs)
		}

		return errs
	})

	schema.setupValidators()

	return schema, errs
}

// Cases
// - #/deffintions - no change of base uri
// - #... - no change of base uri
// - http.... - change of base uri
// - anything else - change of base uri
// - also: base uri changes, but id is not expanded
func (s *Schema) setPointer(key string, schema *Schema) {
	if s != nil {
		if len(key) > 7 && (key[0:7] != "http://" || key[0:8] != "https://") && s.root != nil && s.root.pointers != nil {
			(*s.root.pointers)[key] = schema
		} else if s.pointers != nil {
			(*s.pointers)[key] = schema
		} else if s.base != nil && s.base.pointers != nil {
			(*s.base.pointers)[key] = schema
		}
	}
}

func (s *Schema) getPointer(key string) *Schema {
	if s != nil {
		if s.baseURI != nil && s.baseURI.String() == key {
			return s
		}
		if len(key) > 7 && (key[0:7] != "http://" || key[0:8] != "https://") && s.root != nil && s.root.pointers != nil {
			return (*s.root.pointers)[key]
		}
		if s.pointers != nil && (*s.pointers)[key] != nil {
			return (*s.pointers)[key]
		}
		if s.base != nil && s.base.pointers != nil {
			return (*s.base.pointers)[key]
		}
	}
	return nil
}

func (s *Schema) setRef(ref *Ref) {
	if s != nil {
		if s.root != nil {
			(*s.root.refs) = append((*s.root.refs), ref)
		} else {
			(*s.refs) = append((*s.refs), ref)
		}
	}
}

// NOTE: There is probably a lot of performance to gain here,
//       e.g. by excluding everything else if $ref is set
func (s *Schema) setupValidators() {
	s.validators = []validatorFunc{}

	// Always start by validating the value
	s.validators = append(s.validators, validateValue)

	if s.boolean != nil {
		s.validators = append(s.validators, validateBooleanSchema)
	}

	if s.Ref != nil {
		s.validators = append(s.validators, validateRef)
		return
	}

	if s.Items != nil || s.AdditionalItems != nil || s.MaxItems != nil || s.MinItems != nil || s.UniqueItems != nil || s.Contains != nil {
		s.validators = append(s.validators, validateItems)
	}

	if s.Properties != nil || s.PatternProperties != nil || s.AdditionalProperties != nil || s.MaxProperties != nil || s.MinProperties != nil {
		s.validators = append(s.validators, validateProperties)
	}

	if s.PropertyNames != nil {
		s.validators = append(s.validators, validatePropertyNames)
	}

	if s.Type != nil {
		s.validators = append(s.validators, validateType)
	}

	if s.Pattern != nil {
		s.validators = append(s.validators, validatePattern)
	}

	if s.Required != nil {
		s.validators = append(s.validators, validateRequired)
	}

	if s.Dependencies != nil {
		s.validators = append(s.validators, validateDependencies)
	}

	if s.AllOf != nil {
		s.validators = append(s.validators, validateAllOf)
	}

	if s.AnyOf != nil {
		s.validators = append(s.validators, validateAnyOf)
	}

	if s.OneOf != nil {
		s.validators = append(s.validators, validateOneOf)
	}

	if s.Not != nil {
		s.validators = append(s.validators, validateNot)
	}

	if s.MultipleOf != nil {
		s.validators = append(s.validators, validateMultipleOf)
	}

	if s.Maximum != nil || s.ExclusiveMaximum != nil {
		s.validators = append(s.validators, validateMaximum)
	}

	if s.Minimum != nil || s.ExclusiveMinimum != nil {
		s.validators = append(s.validators, validateMinimum)
	}

	if s.MaxLength != nil {
		s.validators = append(s.validators, validateMaxLength)
	}

	if s.MinLength != nil {
		s.validators = append(s.validators, validateMinLength)
	}

	if s.Enum != nil {
		s.validators = append(s.validators, validateEnum)
	}

	if s.Const != nil {
		s.validators = append(s.validators, validateConst)
	}

	if s.If != nil {
		s.validators = append(s.validators, validateIf)
	}

	if s.Format != nil {
		s.validators = append(s.validators, validateFormat)
	}
}
