package compiler

import (
	"encoding/hex"
	"fmt"
	"strconv"
	"strings"

	chainjson "github.com/vapor/encoding/json"
)

// Contract is a compiled Equity contract.
type Contract struct {
	// Name is the contract name.
	Name string `json:"name"`

	// Params is the list of contract parameters.
	Params []*Param `json:"params,omitempty"`

	// Clauses is the list of contract clauses.
	Clauses []*Clause `json:"clauses"`

	// Value is the name of the value locked by the contract.
	Value ValueInfo `json:"value"`

	// Body is the optimized bytecode of the contract body. This is not
	// a complete program!  Use instantiate to turn this (plus some
	// arguments) into a program.
	Body chainjson.HexBytes `json:"body_bytecode"`

	// Opcodes is the human-readable string of opcodes corresponding to
	// Body.
	Opcodes string `json:"body_opcodes,omitempty"`

	// Recursive tells whether this contract calls itself.  (This is
	// used to select between two possible instantiation options.)
	Recursive bool `json:"recursive"`

	// Pre-optimized list of instruction steps, with stack snapshots.
	Steps []Step `json:"-"`
}

// Param is a contract or clause parameter.
type Param struct {
	// Name is the parameter name.
	Name string `json:"name"`

	// Type is the declared parameter type.
	Type typeDesc `json:"type"`

	// InferredType, if available, is a more-specific type than Type,
	// inferred from the logic of the contract.
	InferredType typeDesc `json:"inferred_type,omitempty"`
}

// Clause is a compiled contract clause.
type Clause struct {
	// Name is the clause name.
	Name string `json:"name"`

	// Params is the list of clause parameters.
	Params []*Param `json:"params,omitempty"`

	statements []statement

	// BlockHeight is the list of expressions passed to greater()/less() in this
	// clause.
	BlockHeight []string `json:"blockheight,omitempty"`

	// HashCalls is the list of hash functions and their arguments used
	// in this clause.
	HashCalls []HashCall `json:"hash_calls,omitempty"`

	// Values is the list of values unlocked or relocked in this clause.
	Values []ValueInfo `json:"values"`

	// Contracts is the list of contracts called by this clause.
	Contracts []string `json:"contracts,omitempty"`
}

// HashCall describes a call to a hash function.
type HashCall struct {
	// HashType is "sha3" or "sha256".
	HashType string `json:"hash_type"`

	// Arg is the expression passed to the hash function.
	Arg string `json:"arg"`

	// ArgType is the type of Arg.
	ArgType string `json:"arg_type"`
}

// IfBody describes a if ... else ... struct
type IfStatmentBody struct {
	// if statements body
	trueBody []statement

	// else statements body
	falseBody []statement
}

type statement interface {
	countVarRefs(map[string]int)
}

type defineStatement struct {
	variable *Param
	expr     expression
}

func (s defineStatement) countVarRefs(counts map[string]int) {
	s.expr.countVarRefs(counts)
}

type assignStatement struct {
	variable *Param
	expr     expression
}

func (s assignStatement) countVarRefs(counts map[string]int) {
	s.expr.countVarRefs(counts)
}

type ifStatement struct {
	condition expression
	body      *IfStatmentBody
}

func (s ifStatement) countVarRefs(counts map[string]int) {
	s.condition.countVarRefs(counts)
}

type verifyStatement struct {
	expr expression
}

func (s verifyStatement) countVarRefs(counts map[string]int) {
	s.expr.countVarRefs(counts)
}

type lockStatement struct {
	lockedAmount expression
	lockedAsset  expression
	program      expression

	// Added as a decoration, used by CHECKOUTPUT
	index int64
}

func (s lockStatement) countVarRefs(counts map[string]int) {
	s.lockedAmount.countVarRefs(counts)
	s.lockedAsset.countVarRefs(counts)
	s.program.countVarRefs(counts)
}

type unlockStatement struct {
	unlockedAmount expression
	unlockedAsset  expression
}

func (s unlockStatement) countVarRefs(counts map[string]int) {
	s.unlockedAmount.countVarRefs(counts)
	s.unlockedAsset.countVarRefs(counts)
}

type expression interface {
	String() string
	typ(*environ) typeDesc
	countVarRefs(map[string]int)
}

type binaryExpr struct {
	left, right expression
	op          *binaryOp
}

func (e binaryExpr) String() string {
	return fmt.Sprintf("(%s %s %s)", e.left, e.op.op, e.right)
}

func (e binaryExpr) typ(*environ) typeDesc {
	return e.op.result
}

func (e binaryExpr) countVarRefs(counts map[string]int) {
	e.left.countVarRefs(counts)
	e.right.countVarRefs(counts)
}

type unaryExpr struct {
	op   *unaryOp
	expr expression
}

func (e unaryExpr) String() string {
	return fmt.Sprintf("%s%s", e.op.op, e.expr)
}

func (e unaryExpr) typ(*environ) typeDesc {
	return e.op.result
}

func (e unaryExpr) countVarRefs(counts map[string]int) {
	e.expr.countVarRefs(counts)
}

type callExpr struct {
	fn   expression
	args []expression
}

func (e callExpr) String() string {
	var argStrs []string
	for _, a := range e.args {
		argStrs = append(argStrs, a.String())
	}
	return fmt.Sprintf("%s(%s)", e.fn, strings.Join(argStrs, ", "))
}

func (e callExpr) typ(env *environ) typeDesc {
	if b := referencedBuiltin(e.fn); b != nil {
		switch b.name {
		case "sha3":
			if len(e.args) == 1 {
				switch e.args[0].typ(env) {
				case strType:
					return sha3StrType
				case pubkeyType:
					return sha3PubkeyType
				}
			}

		case "sha256":
			if len(e.args) == 1 {
				switch e.args[0].typ(env) {
				case strType:
					return sha256StrType
				case pubkeyType:
					return sha256PubkeyType
				}
			}
		}

		return b.result
	}
	if e.fn.typ(env) == predType {
		return boolType
	}
	if e.fn.typ(env) == contractType {
		return progType
	}
	return nilType
}

func (e callExpr) countVarRefs(counts map[string]int) {
	e.fn.countVarRefs(counts)
	for _, a := range e.args {
		a.countVarRefs(counts)
	}
}

type varRef string

func (v varRef) String() string {
	return string(v)
}

func (e varRef) typ(env *environ) typeDesc {
	if entry := env.lookup(string(e)); entry != nil {
		return entry.t
	}
	return nilType
}

func (e varRef) countVarRefs(counts map[string]int) {
	counts[string(e)]++
}

type bytesLiteral []byte

func (e bytesLiteral) String() string {
	return "0x" + hex.EncodeToString([]byte(e))
}

func (bytesLiteral) typ(*environ) typeDesc {
	return "String"
}

func (bytesLiteral) countVarRefs(map[string]int) {}

type integerLiteral int64

func (e integerLiteral) String() string {
	return strconv.FormatInt(int64(e), 10)
}

func (integerLiteral) typ(*environ) typeDesc {
	return "Integer"
}

func (integerLiteral) countVarRefs(map[string]int) {}

type booleanLiteral bool

func (e booleanLiteral) String() string {
	if e {
		return "true"
	}
	return "false"
}

func (booleanLiteral) typ(*environ) typeDesc {
	return "Boolean"
}

func (booleanLiteral) countVarRefs(map[string]int) {}

type listExpr []expression

func (e listExpr) String() string {
	var elts []string
	for _, elt := range e {
		elts = append(elts, elt.String())
	}
	return fmt.Sprintf("[%s]", strings.Join(elts, ", "))
}

func (listExpr) typ(*environ) typeDesc {
	return "List"
}

func (e listExpr) countVarRefs(counts map[string]int) {
	for _, elt := range e {
		elt.countVarRefs(counts)
	}
}
