package hclpack

import (
	"fmt"

	"github.com/zclconf/go-cty/cty"
	ctyjson "github.com/zclconf/go-cty/cty/json"

	"github.com/hashicorp/hcl2/hcl"
	"github.com/hashicorp/hcl2/hcl/hclsyntax"
)

// Expression is an implementation of hcl.Expression in terms of some raw
// expression source code. The methods of this type will first parse the
// source code and then pass the call through to the real expression that
// is produced.
type Expression struct {
	// Source is the raw source code of the expression, which should be parsed
	// as the syntax specified by SourceType.
	Source     []byte
	SourceType ExprSourceType

	// Range_ and StartRange_ describe the physical extents of the expression
	// in the original source code. SourceRange_ is its entire range while
	// StartRange is just the tokens that introduce the expression type. For
	// simple expression types, SourceRange and StartRange are identical.
	Range_, StartRange_ hcl.Range
}

var _ hcl.Expression = (*Expression)(nil)

// Value implements the Value method of hcl.Expression but with the additional
// step of first parsing the expression source code. This implementation is
// unusual in that it can potentially return syntax errors, whereas other
// Value implementations usually work with already-parsed expressions.
func (e *Expression) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
	expr, diags := e.Parse()
	if diags.HasErrors() {
		return cty.DynamicVal, diags
	}

	val, moreDiags := expr.Value(ctx)
	diags = append(diags, moreDiags...)
	return val, diags
}

// Variables implements the Variables method of hcl.Expression but with the
// additional step of first parsing the expression source code.
//
// Since this method cannot return errors, it will return a nil slice if
// parsing fails, indicating that no variables are present. This is okay in
// practice because a subsequent call to Value would fail with syntax errors
// regardless of what variables are in the context.
func (e *Expression) Variables() []hcl.Traversal {
	expr, diags := e.Parse()
	if diags.HasErrors() {
		return nil
	}
	return expr.Variables()
}

// UnwrapExpression parses and returns the underlying expression, if possible.
//
// This is essentially the same as Parse but without the ability to return an
// error; it is here only to support the static analysis facilities in the
// main HCL package (ExprList, ExprMap, etc). If any error is encountered
// during parsing, the result is a static expression that always returns
// cty.DynamicVal.
//
// This function does not impose any further conversions on the underlying
// expression, so the result may still not be suitable for the static analysis
// functions, depending on the source type of the expression and thus what
// type of physical expression it becomes after decoding.
func (e *Expression) UnwrapExpression() hcl.Expression {
	expr, diags := e.Parse()
	if diags.HasErrors() {
		return hcl.StaticExpr(cty.DynamicVal, e.Range_)
	}
	return expr
}

func (e *Expression) Range() hcl.Range {
	return e.Range_
}

func (e *Expression) StartRange() hcl.Range {
	return e.StartRange_
}

// Parse attempts to parse the source code of the receiving expression using
// its indicated source type, returning the expression if possible and any
// diagnostics produced during parsing.
func (e *Expression) Parse() (hcl.Expression, hcl.Diagnostics) {
	switch e.SourceType {
	case ExprNative:
		return hclsyntax.ParseExpression(e.Source, e.Range_.Filename, e.Range_.Start)
	case ExprTemplate:
		return hclsyntax.ParseTemplate(e.Source, e.Range_.Filename, e.Range_.Start)
	case ExprLiteralJSON:
		ty, err := ctyjson.ImpliedType(e.Source)
		if err != nil {
			return nil, hcl.Diagnostics{
				{
					Severity: hcl.DiagError,
					Summary:  "Invalid JSON value",
					Detail:   fmt.Sprintf("The JSON representation of this expression is invalid: %s.", err),
					Subject:  &e.Range_,
				},
			}
		}
		val, err := ctyjson.Unmarshal(e.Source, ty)
		if err != nil {
			return nil, hcl.Diagnostics{
				{
					Severity: hcl.DiagError,
					Summary:  "Invalid JSON value",
					Detail:   fmt.Sprintf("The JSON representation of this expression is invalid: %s.", err),
					Subject:  &e.Range_,
				},
			}
		}
		return hcl.StaticExpr(val, e.Range_), nil
	default:
		// This should never happen for a valid Expression.
		return nil, hcl.Diagnostics{
			{
				Severity: hcl.DiagError,
				Summary:  "Invalid expression source type",
				Detail:   fmt.Sprintf("Packed version of this expression has an invalid source type %s. This is always a bug.", e.SourceType),
				Subject:  &e.Range_,
			},
		}
	}
}

func (e *Expression) addRanges(rngs map[hcl.Range]struct{}) {
	rngs[e.Range_] = struct{}{}
	rngs[e.StartRange_] = struct{}{}
}

// ExprSourceType defines the syntax type used for an expression's source code,
// which is then used to select a suitable parser for it when evaluating.
type ExprSourceType rune

//go:generate stringer -type ExprSourceType

const (
	// ExprNative indicates that an expression must be parsed as native
	// expression syntax, with hclsyntax.ParseExpression.
	ExprNative ExprSourceType = 'N'

	// ExprTemplate indicates that an expression must be parsed as native
	// template syntax, with hclsyntax.ParseTemplate.
	ExprTemplate ExprSourceType = 'T'

	// ExprLiteralJSON indicates that an expression must be parsed as JSON and
	// treated literally, using cty/json. This can be used when populating
	// literal attribute values from a non-HCL source.
	ExprLiteralJSON ExprSourceType = 'L'
)