// 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 (
	"fmt"

	"github.com/hashicorp/hcl/v2"
	"github.com/hashicorp/hcl/v2/hclsyntax"
	"github.com/pkg/errors"
	"github.com/pulumi/pulumi/pkg/v2/codegen/hcl2/syntax"
	"github.com/pulumi/pulumi/sdk/v2/go/common/util/contract"
)

// OpaqueType represents a type that is named by a string.
type OpaqueType struct {
	// Name is the type's name.
	Name string
	// Annotations records any annotations associated with the object type.
	Annotations []interface{}

	s string
}

// The set of opaque types, indexed by name.
var opaqueTypes = map[string]*OpaqueType{}

// GetOpaqueType fetches the opaque type for the given name.
func GetOpaqueType(name string) (*OpaqueType, bool) {
	t, ok := opaqueTypes[name]
	return t, ok
}

// MustNewOpaqueType creates a new opaque type with the given name.
func MustNewOpaqueType(name string, annotations ...interface{}) *OpaqueType {
	t, err := NewOpaqueType(name, annotations...)
	if err != nil {
		panic(err)
	}
	return t
}

// NewOpaqueType creates a new opaque type with the given name.
func NewOpaqueType(name string, annotations ...interface{}) (*OpaqueType, error) {
	if _, ok := opaqueTypes[name]; ok {
		return nil, errors.Errorf("opaque type %s is already defined", name)
	}

	t := &OpaqueType{Name: name, Annotations: annotations}
	opaqueTypes[name] = t
	return t, nil
}

// SyntaxNode returns the syntax node for the type. This is always syntax.None.
func (*OpaqueType) SyntaxNode() hclsyntax.Node {
	return syntax.None
}

// Traverse attempts to traverse the opaque type with the given traverser. The result type of traverse(opaque(name))
// is dynamic if name is "dynamic"; otherwise the traversal fails.
func (t *OpaqueType) Traverse(traverser hcl.Traverser) (Traversable, hcl.Diagnostics) {
	if t == DynamicType {
		return DynamicType, nil
	}

	return DynamicType, hcl.Diagnostics{unsupportedReceiverType(t, traverser.SourceRange())}
}

// Equals returns true if this type has the same identity as the given type.
func (t *OpaqueType) Equals(other Type) bool {
	return t.equals(other, nil)
}

func (t *OpaqueType) equals(other Type, seen map[Type]struct{}) bool {
	return t == other
}

// AssignableFrom returns true if this type is assignable from the indicated source type. A token(name) is assignable
// from token(name).
func (t *OpaqueType) AssignableFrom(src Type) bool {
	return assignableFrom(t, src, func() bool {
		return false
	})
}

func (t *OpaqueType) conversionFromImpl(src Type, unifying, checkUnsafe bool, seen map[Type]struct{}) ConversionKind {
	return conversionFrom(t, src, unifying, seen, func() ConversionKind {
		switch {
		case t == NumberType:
			// src == NumberType is handled by t == src above
			contract.Assert(src != NumberType)

			cki := IntType.conversionFromImpl(src, unifying, false, seen)
			if cki == SafeConversion {
				return SafeConversion
			}
			if cki == UnsafeConversion || checkUnsafe && StringType.conversionFromImpl(src, unifying, false, seen).Exists() {
				return UnsafeConversion
			}
			return NoConversion
		case t == IntType:
			if checkUnsafe && NumberType.conversionFromImpl(src, unifying, true, seen).Exists() {
				return UnsafeConversion
			}
			return NoConversion
		case t == BoolType:
			if checkUnsafe && StringType.conversionFromImpl(src, unifying, false, seen).Exists() {
				return UnsafeConversion
			}
			return NoConversion
		case t == StringType:
			ckb := BoolType.conversionFromImpl(src, unifying, false, seen)
			ckn := NumberType.conversionFromImpl(src, unifying, false, seen)
			if ckb == SafeConversion || ckn == SafeConversion {
				return SafeConversion
			}
			if ckb == UnsafeConversion || ckn == UnsafeConversion {
				return UnsafeConversion
			}
			return NoConversion
		default:
			return NoConversion
		}
	})
}

func (t *OpaqueType) conversionFrom(src Type, unifying bool, seen map[Type]struct{}) ConversionKind {
	return t.conversionFromImpl(src, unifying, true, seen)
}

// ConversionFrom returns the kind of conversion (if any) that is possible from the source type to this type.
//
// In general, an opaque type is only convertible from itself (in addition to the standard dynamic and union
// conversions). However, there are special rules for the builtin types:
//
// - The dynamic type is safely convertible from any other type, and is unsafely convertible _to_ any other type
// - The string type is safely convertible from bool, number, and int
// - The number type is safely convertible from int and unsafely convertible from string
// - The int type is unsafely convertible from string
// - The bool type is unsafely convertible from string
//
func (t *OpaqueType) ConversionFrom(src Type) ConversionKind {
	return t.conversionFrom(src, false, nil)
}

func (t *OpaqueType) String() string {
	if t.s == "" {
		switch t {
		case NumberType:
			t.s = "number"
		case IntType:
			t.s = "int"
		case BoolType:
			t.s = "bool"
		case StringType:
			t.s = "string"
		default:
			if hclsyntax.ValidIdentifier(t.Name) {
				t.s = t.Name
			} else {
				t.s = fmt.Sprintf("type(%s)", t.Name)
			}
		}
	}
	return t.s
}

func (t *OpaqueType) string(_ map[Type]struct{}) string {
	return t.String()
}

var opaquePrecedence = []*OpaqueType{StringType, NumberType, IntType, BoolType}

func (t *OpaqueType) unify(other Type) (Type, ConversionKind) {
	return unify(t, other, func() (Type, ConversionKind) {
		if t == DynamicType || other == DynamicType {
			// These should have been handled by unify.
			contract.Failf("unexpected type %v in OpaqueType.unify", t)
			return DynamicType, SafeConversion
		}

		for _, goal := range opaquePrecedence {
			if t == goal {
				return goal, goal.conversionFrom(other, true, nil)
			}
			if other == goal {
				return goal, goal.conversionFrom(t, true, nil)
			}
		}

		// There should be a total order on conversions to and from these types, so there should be a total order
		// on unifications with these types.
		return DynamicType, SafeConversion
	})
}

func (*OpaqueType) isType() {}
