package gojsonschema

import (
	"bytes"
	"sync"
	"text/template"
)

var errorTemplates errorTemplate = errorTemplate{template.New("errors-new"), sync.RWMutex{}}

// template.Template is not thread-safe for writing, so some locking is done
// sync.RWMutex is used for efficiently locking when new templates are created
type errorTemplate struct {
	*template.Template
	sync.RWMutex
}

type (
	// RequiredError. ErrorDetails: property string
	RequiredError struct {
		ResultErrorFields
	}

	// InvalidTypeError. ErrorDetails: expected, given
	InvalidTypeError struct {
		ResultErrorFields
	}

	// NumberAnyOfError. ErrorDetails: -
	NumberAnyOfError struct {
		ResultErrorFields
	}

	// NumberOneOfError. ErrorDetails: -
	NumberOneOfError struct {
		ResultErrorFields
	}

	// NumberAllOfError. ErrorDetails: -
	NumberAllOfError struct {
		ResultErrorFields
	}

	// NumberNotError. ErrorDetails: -
	NumberNotError struct {
		ResultErrorFields
	}

	// MissingDependencyError. ErrorDetails: dependency
	MissingDependencyError struct {
		ResultErrorFields
	}

	// InternalError. ErrorDetails: error
	InternalError struct {
		ResultErrorFields
	}

	// ConstError. ErrorDetails: allowed
	ConstError struct {
		ResultErrorFields
	}

	// EnumError. ErrorDetails: allowed
	EnumError struct {
		ResultErrorFields
	}

	// ArrayNoAdditionalItemsError. ErrorDetails: -
	ArrayNoAdditionalItemsError struct {
		ResultErrorFields
	}

	// ArrayMinItemsError. ErrorDetails: min
	ArrayMinItemsError struct {
		ResultErrorFields
	}

	// ArrayMaxItemsError. ErrorDetails: max
	ArrayMaxItemsError struct {
		ResultErrorFields
	}

	// ItemsMustBeUniqueError. ErrorDetails: type, i, j
	ItemsMustBeUniqueError struct {
		ResultErrorFields
	}

	// ArrayContainsError. ErrorDetails:
	ArrayContainsError struct {
		ResultErrorFields
	}

	// ArrayMinPropertiesError. ErrorDetails: min
	ArrayMinPropertiesError struct {
		ResultErrorFields
	}

	// ArrayMaxPropertiesError. ErrorDetails: max
	ArrayMaxPropertiesError struct {
		ResultErrorFields
	}

	// AdditionalPropertyNotAllowedError. ErrorDetails: property
	AdditionalPropertyNotAllowedError struct {
		ResultErrorFields
	}

	// InvalidPropertyPatternError. ErrorDetails: property, pattern
	InvalidPropertyPatternError struct {
		ResultErrorFields
	}

	// InvalidPopertyNameError. ErrorDetails: property
	InvalidPropertyNameError struct {
		ResultErrorFields
	}

	// StringLengthGTEError. ErrorDetails: min
	StringLengthGTEError struct {
		ResultErrorFields
	}

	// StringLengthLTEError. ErrorDetails: max
	StringLengthLTEError struct {
		ResultErrorFields
	}

	// DoesNotMatchPatternError. ErrorDetails: pattern
	DoesNotMatchPatternError struct {
		ResultErrorFields
	}

	// DoesNotMatchFormatError. ErrorDetails: format
	DoesNotMatchFormatError struct {
		ResultErrorFields
	}

	// MultipleOfError. ErrorDetails: multiple
	MultipleOfError struct {
		ResultErrorFields
	}

	// NumberGTEError. ErrorDetails: min
	NumberGTEError struct {
		ResultErrorFields
	}

	// NumberGTError. ErrorDetails: min
	NumberGTError struct {
		ResultErrorFields
	}

	// NumberLTEError. ErrorDetails: max
	NumberLTEError struct {
		ResultErrorFields
	}

	// NumberLTError. ErrorDetails: max
	NumberLTError struct {
		ResultErrorFields
	}

	// ConditionThenError. ErrorDetails: -
	ConditionThenError struct {
		ResultErrorFields
	}

	// ConditionElseError. ErrorDetails: -
	ConditionElseError struct {
		ResultErrorFields
	}
)

// newError takes a ResultError type and sets the type, context, description, details, value, and field
func newError(err ResultError, context *JsonContext, value interface{}, locale locale, details ErrorDetails) {
	var t string
	var d string
	switch err.(type) {
	case *RequiredError:
		t = "required"
		d = locale.Required()
	case *InvalidTypeError:
		t = "invalid_type"
		d = locale.InvalidType()
	case *NumberAnyOfError:
		t = "number_any_of"
		d = locale.NumberAnyOf()
	case *NumberOneOfError:
		t = "number_one_of"
		d = locale.NumberOneOf()
	case *NumberAllOfError:
		t = "number_all_of"
		d = locale.NumberAllOf()
	case *NumberNotError:
		t = "number_not"
		d = locale.NumberNot()
	case *MissingDependencyError:
		t = "missing_dependency"
		d = locale.MissingDependency()
	case *InternalError:
		t = "internal"
		d = locale.Internal()
	case *ConstError:
		t = "const"
		d = locale.Const()
	case *EnumError:
		t = "enum"
		d = locale.Enum()
	case *ArrayNoAdditionalItemsError:
		t = "array_no_additional_items"
		d = locale.ArrayNoAdditionalItems()
	case *ArrayMinItemsError:
		t = "array_min_items"
		d = locale.ArrayMinItems()
	case *ArrayMaxItemsError:
		t = "array_max_items"
		d = locale.ArrayMaxItems()
	case *ItemsMustBeUniqueError:
		t = "unique"
		d = locale.Unique()
	case *ArrayContainsError:
		t = "contains"
		d = locale.ArrayContains()
	case *ArrayMinPropertiesError:
		t = "array_min_properties"
		d = locale.ArrayMinProperties()
	case *ArrayMaxPropertiesError:
		t = "array_max_properties"
		d = locale.ArrayMaxProperties()
	case *AdditionalPropertyNotAllowedError:
		t = "additional_property_not_allowed"
		d = locale.AdditionalPropertyNotAllowed()
	case *InvalidPropertyPatternError:
		t = "invalid_property_pattern"
		d = locale.InvalidPropertyPattern()
	case *InvalidPropertyNameError:
		t = "invalid_property_name"
		d = locale.InvalidPropertyName()
	case *StringLengthGTEError:
		t = "string_gte"
		d = locale.StringGTE()
	case *StringLengthLTEError:
		t = "string_lte"
		d = locale.StringLTE()
	case *DoesNotMatchPatternError:
		t = "pattern"
		d = locale.DoesNotMatchPattern()
	case *DoesNotMatchFormatError:
		t = "format"
		d = locale.DoesNotMatchFormat()
	case *MultipleOfError:
		t = "multiple_of"
		d = locale.MultipleOf()
	case *NumberGTEError:
		t = "number_gte"
		d = locale.NumberGTE()
	case *NumberGTError:
		t = "number_gt"
		d = locale.NumberGT()
	case *NumberLTEError:
		t = "number_lte"
		d = locale.NumberLTE()
	case *NumberLTError:
		t = "number_lt"
		d = locale.NumberLT()
	case *ConditionThenError:
		t = "condition_then"
		d = locale.ConditionThen()
	case *ConditionElseError:
		t = "condition_else"
		d = locale.ConditionElse()
	}

	err.SetType(t)
	err.SetContext(context)
	err.SetValue(value)
	err.SetDetails(details)
	err.SetDescriptionFormat(d)
	details["field"] = err.Field()

	if _, exists := details["context"]; !exists && context != nil {
		details["context"] = context.String()
	}

	err.SetDescription(formatErrorDescription(err.DescriptionFormat(), details))
}

// formatErrorDescription takes a string in the default text/template
// format and converts it to a string with replacements. The fields come
// from the ErrorDetails struct and vary for each type of error.
func formatErrorDescription(s string, details ErrorDetails) string {

	var tpl *template.Template
	var descrAsBuffer bytes.Buffer
	var err error

	errorTemplates.RLock()
	tpl = errorTemplates.Lookup(s)
	errorTemplates.RUnlock()

	if tpl == nil {
		errorTemplates.Lock()
		tpl = errorTemplates.New(s)

		if ErrorTemplateFuncs != nil {
			tpl.Funcs(ErrorTemplateFuncs)
		}

		tpl, err = tpl.Parse(s)
		errorTemplates.Unlock()

		if err != nil {
			return err.Error()
		}
	}

	err = tpl.Execute(&descrAsBuffer, details)
	if err != nil {
		return err.Error()
	}

	return descrAsBuffer.String()
}
