package hclsyntax

import (
	"fmt"

	"github.com/hashicorp/hcl/v2"
	"github.com/zclconf/go-cty/cty"
	"github.com/zclconf/go-cty/cty/convert"
	"github.com/zclconf/go-cty/cty/function"
	"github.com/zclconf/go-cty/cty/function/stdlib"
)

type Operation struct {
	Impl function.Function
	Type cty.Type
}

var (
	OpLogicalOr = &Operation{
		Impl: stdlib.OrFunc,
		Type: cty.Bool,
	}
	OpLogicalAnd = &Operation{
		Impl: stdlib.AndFunc,
		Type: cty.Bool,
	}
	OpLogicalNot = &Operation{
		Impl: stdlib.NotFunc,
		Type: cty.Bool,
	}

	OpEqual = &Operation{
		Impl: stdlib.EqualFunc,
		Type: cty.Bool,
	}
	OpNotEqual = &Operation{
		Impl: stdlib.NotEqualFunc,
		Type: cty.Bool,
	}

	OpGreaterThan = &Operation{
		Impl: stdlib.GreaterThanFunc,
		Type: cty.Bool,
	}
	OpGreaterThanOrEqual = &Operation{
		Impl: stdlib.GreaterThanOrEqualToFunc,
		Type: cty.Bool,
	}
	OpLessThan = &Operation{
		Impl: stdlib.LessThanFunc,
		Type: cty.Bool,
	}
	OpLessThanOrEqual = &Operation{
		Impl: stdlib.LessThanOrEqualToFunc,
		Type: cty.Bool,
	}

	OpAdd = &Operation{
		Impl: stdlib.AddFunc,
		Type: cty.Number,
	}
	OpSubtract = &Operation{
		Impl: stdlib.SubtractFunc,
		Type: cty.Number,
	}
	OpMultiply = &Operation{
		Impl: stdlib.MultiplyFunc,
		Type: cty.Number,
	}
	OpDivide = &Operation{
		Impl: stdlib.DivideFunc,
		Type: cty.Number,
	}
	OpModulo = &Operation{
		Impl: stdlib.ModuloFunc,
		Type: cty.Number,
	}
	OpNegate = &Operation{
		Impl: stdlib.NegateFunc,
		Type: cty.Number,
	}
)

var binaryOps []map[TokenType]*Operation

func init() {
	// This operation table maps from the operator's token type
	// to the AST operation type. All expressions produced from
	// binary operators are BinaryOp nodes.
	//
	// Binary operator groups are listed in order of precedence, with
	// the *lowest* precedence first. Operators within the same group
	// have left-to-right associativity.
	binaryOps = []map[TokenType]*Operation{
		{
			TokenOr: OpLogicalOr,
		},
		{
			TokenAnd: OpLogicalAnd,
		},
		{
			TokenEqualOp:  OpEqual,
			TokenNotEqual: OpNotEqual,
		},
		{
			TokenGreaterThan:   OpGreaterThan,
			TokenGreaterThanEq: OpGreaterThanOrEqual,
			TokenLessThan:      OpLessThan,
			TokenLessThanEq:    OpLessThanOrEqual,
		},
		{
			TokenPlus:  OpAdd,
			TokenMinus: OpSubtract,
		},
		{
			TokenStar:    OpMultiply,
			TokenSlash:   OpDivide,
			TokenPercent: OpModulo,
		},
	}
}

type BinaryOpExpr struct {
	LHS Expression
	Op  *Operation
	RHS Expression

	SrcRange hcl.Range
}

func (e *BinaryOpExpr) walkChildNodes(w internalWalkFunc) {
	w(e.LHS)
	w(e.RHS)
}

func (e *BinaryOpExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
	impl := e.Op.Impl // assumed to be a function taking exactly two arguments
	params := impl.Params()
	lhsParam := params[0]
	rhsParam := params[1]

	var diags hcl.Diagnostics

	givenLHSVal, lhsDiags := e.LHS.Value(ctx)
	givenRHSVal, rhsDiags := e.RHS.Value(ctx)
	diags = append(diags, lhsDiags...)
	diags = append(diags, rhsDiags...)

	lhsVal, err := convert.Convert(givenLHSVal, lhsParam.Type)
	if err != nil {
		diags = append(diags, &hcl.Diagnostic{
			Severity:    hcl.DiagError,
			Summary:     "Invalid operand",
			Detail:      fmt.Sprintf("Unsuitable value for left operand: %s.", err),
			Subject:     e.LHS.Range().Ptr(),
			Context:     &e.SrcRange,
			Expression:  e.LHS,
			EvalContext: ctx,
		})
	}
	rhsVal, err := convert.Convert(givenRHSVal, rhsParam.Type)
	if err != nil {
		diags = append(diags, &hcl.Diagnostic{
			Severity:    hcl.DiagError,
			Summary:     "Invalid operand",
			Detail:      fmt.Sprintf("Unsuitable value for right operand: %s.", err),
			Subject:     e.RHS.Range().Ptr(),
			Context:     &e.SrcRange,
			Expression:  e.RHS,
			EvalContext: ctx,
		})
	}

	if diags.HasErrors() {
		// Don't actually try the call if we have errors already, since the
		// this will probably just produce a confusing duplicative diagnostic.
		return cty.UnknownVal(e.Op.Type), diags
	}

	args := []cty.Value{lhsVal, rhsVal}
	result, err := impl.Call(args)
	if err != nil {
		diags = append(diags, &hcl.Diagnostic{
			// FIXME: This diagnostic is useless.
			Severity:    hcl.DiagError,
			Summary:     "Operation failed",
			Detail:      fmt.Sprintf("Error during operation: %s.", err),
			Subject:     &e.SrcRange,
			Expression:  e,
			EvalContext: ctx,
		})
		return cty.UnknownVal(e.Op.Type), diags
	}

	return result, diags
}

func (e *BinaryOpExpr) Range() hcl.Range {
	return e.SrcRange
}

func (e *BinaryOpExpr) StartRange() hcl.Range {
	return e.LHS.StartRange()
}

type UnaryOpExpr struct {
	Op  *Operation
	Val Expression

	SrcRange    hcl.Range
	SymbolRange hcl.Range
}

func (e *UnaryOpExpr) walkChildNodes(w internalWalkFunc) {
	w(e.Val)
}

func (e *UnaryOpExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
	impl := e.Op.Impl // assumed to be a function taking exactly one argument
	params := impl.Params()
	param := params[0]

	givenVal, diags := e.Val.Value(ctx)

	val, err := convert.Convert(givenVal, param.Type)
	if err != nil {
		diags = append(diags, &hcl.Diagnostic{
			Severity:    hcl.DiagError,
			Summary:     "Invalid operand",
			Detail:      fmt.Sprintf("Unsuitable value for unary operand: %s.", err),
			Subject:     e.Val.Range().Ptr(),
			Context:     &e.SrcRange,
			Expression:  e.Val,
			EvalContext: ctx,
		})
	}

	if diags.HasErrors() {
		// Don't actually try the call if we have errors already, since the
		// this will probably just produce a confusing duplicative diagnostic.
		return cty.UnknownVal(e.Op.Type), diags
	}

	args := []cty.Value{val}
	result, err := impl.Call(args)
	if err != nil {
		diags = append(diags, &hcl.Diagnostic{
			// FIXME: This diagnostic is useless.
			Severity:    hcl.DiagError,
			Summary:     "Operation failed",
			Detail:      fmt.Sprintf("Error during operation: %s.", err),
			Subject:     &e.SrcRange,
			Expression:  e,
			EvalContext: ctx,
		})
		return cty.UnknownVal(e.Op.Type), diags
	}

	return result, diags
}

func (e *UnaryOpExpr) Range() hcl.Range {
	return e.SrcRange
}

func (e *UnaryOpExpr) StartRange() hcl.Range {
	return e.SymbolRange
}
