// Copyright 2016-2020, Pulumi Corporation.
//
// 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 model

import (
	"github.com/hashicorp/hcl/v2"
	"github.com/hashicorp/hcl/v2/hclsyntax"
	"github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/syntax"
	"github.com/zclconf/go-cty/cty"
)

// Definition represents a single definition in a Scope.
type Definition interface {
	Traversable

	SyntaxNode() hclsyntax.Node
}

// A Keyword is a non-traversable definition that allows scope traversals to bind to arbitrary keywords.
type Keyword string

// Traverse attempts to traverse the keyword, and always fails.
func (kw Keyword) Traverse(traverser hcl.Traverser) (Traversable, hcl.Diagnostics) {
	return DynamicType, hcl.Diagnostics{cannotTraverseKeyword(string(kw), traverser.SourceRange())}
}

// SyntaxNode returns the syntax node for the keyword, which is always syntax.None.
func (kw Keyword) SyntaxNode() hclsyntax.Node {
	return syntax.None
}

// A Variable is a traversable, typed definition that represents a named value.
type Variable struct {
	// The syntax node associated with the variable definition, if any.
	Syntax hclsyntax.Node

	// The name of the variable.
	Name string
	// The type of the variable.
	VariableType Type
}

// Traverse attempts to traverse the variable's type.
func (v *Variable) Traverse(traverser hcl.Traverser) (Traversable, hcl.Diagnostics) {
	return v.VariableType.Traverse(traverser)
}

// SyntaxNode returns the variable's syntax node or syntax.None.
func (v *Variable) SyntaxNode() hclsyntax.Node {
	return syntaxOrNone(v.Syntax)
}

// Type returns the type of the variable.
func (v *Variable) Type() Type {
	return v.VariableType
}

func (v *Variable) Value(context *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
	if value, hasValue := context.Variables[v.Name]; hasValue {
		return value, nil
	}
	return cty.DynamicVal, nil
}

// A Constant is a traversable, typed definition that represents a named constant.
type Constant struct {
	// The syntax node associated with the constant definition, if any.
	Syntax hclsyntax.Node

	// The name of the constant.
	Name string
	// The value of the constant.
	ConstantValue cty.Value

	typ Type
}

// Traverse attempts to traverse the constant's value.
func (c *Constant) Traverse(traverser hcl.Traverser) (Traversable, hcl.Diagnostics) {
	v, diags := traverser.TraversalStep(c.ConstantValue)
	return &Constant{ConstantValue: v}, diags
}

// SyntaxNode returns the constant's syntax node or syntax.None.
func (c *Constant) SyntaxNode() hclsyntax.Node {
	return syntaxOrNone(c.Syntax)
}

// Type returns the type of the constant.
func (c *Constant) Type() Type {
	if c.typ == nil {
		if c.ConstantValue.IsNull() {
			c.typ = NoneType
		} else {
			c.typ = ctyTypeToType(c.ConstantValue.Type(), false)
		}
	}
	return c.typ
}

func (c *Constant) Value(context *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
	return c.ConstantValue, nil
}

// A Scope is used to map names to definitions during expression binding.
//
// A scope has three namespaces:
//   - one that is exclusive to functions
//   - one that is exclusive to output variables
//   - and one that contains config variables, local variables and resource definitions.
//
// When binding a reference, we check `defs` and `outputs`. When binding a function, we check `functions`.
// Definitions within a namespace such as `defs`, `outputs` or `functions` are expected to have a unique identifier
// and cannot be redeclared.
type Scope struct {
	parent    *Scope
	syntax    hclsyntax.Node
	defs      map[string]Definition
	outputs   map[string]Definition
	functions map[string]*Function
}

// NewRootScope returns a new unparented scope associated with the given syntax node.
func NewRootScope(syntax hclsyntax.Node) *Scope {
	return &Scope{
		syntax:    syntax,
		defs:      map[string]Definition{},
		outputs:   map[string]Definition{},
		functions: map[string]*Function{},
	}
}

// Traverse attempts to traverse the scope using the given traverser. If the traverser is a literal string that refers
// to a name defined within the scope or one of its ancestors, the traversal returns the corresponding definition.
func (s *Scope) Traverse(traverser hcl.Traverser) (Traversable, hcl.Diagnostics) {
	name, nameType := GetTraverserKey(traverser)
	if nameType != StringType {
		// TODO(pdg): return a better error here
		return DynamicType, hcl.Diagnostics{undefinedVariable("", traverser.SourceRange(), false)}
	}

	memberName := name.AsString()
	member, hasMember := s.BindReference(memberName)
	if !hasMember {
		return DynamicType, hcl.Diagnostics{undefinedVariable(memberName, traverser.SourceRange(), false)}
	}
	return member, nil
}

// SyntaxNode returns the syntax node associated with the scope, if any.
func (s *Scope) SyntaxNode() hclsyntax.Node {
	if s != nil {
		return syntaxOrNone(s.syntax)
	}
	return syntax.None
}

// BindReference returns the definition that corresponds to the given name, if any. Each parent scope is checked until
// a definition is found or no parent scope remains.
func (s *Scope) BindReference(name string) (Definition, bool) {
	if s != nil {
		// first we check inside defs which include config variables, local variables and resource definitions
		if def, ok := s.defs[name]; ok {
			return def, true
		}

		// then we check the namespace of the outputs.
		// it is unlikely that an output is being referenced by something but still possible
		// that is why we check outputs after checking defs
		if output, ok := s.outputs[name]; ok {
			return output, true
		}

		if s.parent != nil {
			return s.parent.BindReference(name)
		}
	}
	return nil, false
}

// BindFunctionReference returns the function definition that corresponds to the given name, if any. Each parent scope
// is checked until a definition is found or no parent scope remains.
func (s *Scope) BindFunctionReference(name string) (*Function, bool) {
	if s != nil {
		if fn, ok := s.functions[name]; ok {
			return fn, true
		}
		if s.parent != nil {
			return s.parent.BindFunctionReference(name)
		}
	}
	return nil, false
}

// isOutputDefinition returns whether the input definition is an output variable
func isOutputDefinition(def Definition) bool {
	syntax := def.SyntaxNode()
	if syntax != nil {
		switch syntax := syntax.(type) {
		case *hclsyntax.Block:
			return syntax.Type == "output"
		}
	}

	return false
}

// Define maps the given name to the given definition. If the name is already defined in this scope, the existing
// definition is not overwritten and Define returns false.
func (s *Scope) Define(name string, def Definition) bool {
	if s != nil {
		if isOutputDefinition(def) {
			// if the definition we have is an output
			// then we only check inside the outputs namespace
			if _, hasDef := s.outputs[name]; !hasDef {
				s.outputs[name] = def
				return true
			}
		} else {
			if _, hasDef := s.defs[name]; !hasDef {
				s.defs[name] = def
				return true
			}
		}
	}
	return false
}

// DefineFunction maps the given function name to the given function definition. If the function is alreadu defined in
// this scope, the definition is not overwritten and DefineFunction returns false.
func (s *Scope) DefineFunction(name string, def *Function) bool {
	if s != nil {
		if _, hasFunc := s.functions[name]; !hasFunc {
			s.functions[name] = def
			return true
		}
	}
	return false
}

// DefineScope defines a child scope with the given name. If the name is already defined in this scope, the existing
// definition is not overwritten and DefineScope returns false.
func (s *Scope) DefineScope(name string, syntax hclsyntax.Node) (*Scope, bool) {
	if s != nil {
		if _, exists := s.defs[name]; !exists {
			child := &Scope{
				parent:    s,
				syntax:    syntax,
				defs:      map[string]Definition{},
				outputs:   map[string]Definition{},
				functions: map[string]*Function{},
			}
			s.defs[name] = child
			return child, true
		}
	}
	return nil, false
}

// Push defines an anonymous child scope associated with the given syntax node.
func (s *Scope) Push(syntax hclsyntax.Node) *Scope {
	return &Scope{
		parent:    s,
		syntax:    syntax,
		defs:      map[string]Definition{},
		outputs:   map[string]Definition{},
		functions: map[string]*Function{},
	}
}

// Pop returns this scope's parent.
func (s *Scope) Pop() *Scope {
	return s.parent
}

// Scopes is the interface that is used fetch the scope that should be used when binding a block or attribute.
type Scopes interface {
	// GetScopesForBlock returns the Scopes that should be used when binding the given block.
	GetScopesForBlock(block *hclsyntax.Block) (Scopes, hcl.Diagnostics)
	// GetScopeForAttribute returns the *Scope that should be used when binding the given attribute.
	GetScopeForAttribute(attribute *hclsyntax.Attribute) (*Scope, hcl.Diagnostics)
}

type staticScope struct {
	scope *Scope
}

// GetScopesForBlock returns the scopes to use when binding the given block.
func (s staticScope) GetScopesForBlock(block *hclsyntax.Block) (Scopes, hcl.Diagnostics) {
	return s, nil
}

// GetScopeForAttribute returns the scope to use when binding the given attribute.
func (s staticScope) GetScopeForAttribute(attribute *hclsyntax.Attribute) (*Scope, hcl.Diagnostics) {
	return s.scope, nil
}

// StaticScope returns a Scopes that uses the given *Scope for all blocks and attributes.
func StaticScope(scope *Scope) Scopes {
	return staticScope{scope: scope}
}