package tokensale

import (
	"github.com/nspcc-dev/neo-go/pkg/interop/runtime"
	"github.com/nspcc-dev/neo-go/pkg/interop/storage"
	"github.com/nspcc-dev/neo-go/pkg/interop/util"
)

const (
	decimals   = 8
	multiplier = decimals * 10
)

var (
	owner   = util.FromAddress("NULwe3UAHckN2fzNdcVg31tDiaYtMDwANt")
	trigger byte
	token   TokenConfig
	ctx     storage.Context
)

// TokenConfig holds information about the token we want to use for the sale.
type TokenConfig struct {
	// Name of the token.
	Name string
	// 3 letter abbreviation of the token.
	Symbol string
	// How decimals this token will have.
	Decimals int
	// Address of the token owner. This is the Uint160 hash.
	Owner []byte
	// The total amount of tokens created. Notice that we need to multiply the
	// amount by 100000000. (10^8)
	TotalSupply int
	// Initial amount is number of tokens that are available for the token sale.
	InitialAmount int
	// How many NEO will be worth 1 token. For example:
	// Lets say 1 euro per token, where 1 NEO is 60 euro. This means buyers
	// will get (60 * 10^8) tokens for 1 NEO.
	AmountPerNEO int
	// How many Gas will be worth 1 token. This is the same calculation as
	// for the AmountPerNEO, except Gas price will have a different value.
	AmountPerGas int
	// The maximum amount you can mint in the limited round. For example:
	// 500 NEO/buyer * 60 tokens/NEO * 10^8
	MaxExchangeLimitRound int
	// When to start the token sale.
	SaleStart int
	// When to end the initial limited round if there is one. For example:
	// SaleStart + 10000
	LimitRoundEnd int
	// The prefix used to store how many tokens there are in circulation.
	CirculationKey []byte
	// The prefix used to store how many tokens there are in the limited round.
	LimitRoundKey []byte
	// The prefix used to store the addresses that are registered with KYC.
	KYCKey []byte
}

// newTokenConfig returns the initialized TokenConfig.
func newTokenConfig() TokenConfig {
	return TokenConfig{
		Name:                  "My awesome token",
		Symbol:                "MAT",
		Decimals:              decimals,
		Owner:                 owner,
		TotalSupply:           10000000 * multiplier,
		InitialAmount:         5000000 * multiplier,
		AmountPerNEO:          60 * multiplier,
		AmountPerGas:          40 * multiplier,
		MaxExchangeLimitRound: 500 * 60 * multiplier,
		SaleStart:             75500,
		LimitRoundEnd:         75500 + 10000,
		CirculationKey:        []byte("in_circulation"),
		LimitRoundKey:         []byte("r1"),
		KYCKey:                []byte("kyc_ok"),
	}
}

// getIntFromDB is a helper that checks for nil result of storage.Get and returns
// zero as the default value.
func getIntFromDB(ctx storage.Context, key []byte) int {
	var res int
	val := storage.Get(ctx, key)
	if val != nil {
		res = val.(int)
	}
	return res
}

// InCirculation returns the amount of total tokens that are in circulation.
func InCirculation() int {
	return getIntFromDB(ctx, token.CirculationKey)
}

// addToCirculation sets the given amount as "in circulation" in the storage.
func addToCirculation(amount int) bool {
	if amount < 0 {
		return false
	}
	supply := getIntFromDB(ctx, token.CirculationKey)
	supply += amount
	if supply > token.TotalSupply {
		return false
	}
	storage.Put(ctx, token.CirculationKey, supply)
	return true
}

// AvailableAmount returns the total amount of available tokens left
// to be distributed.
func AvailableAmount() int {
	inCirc := getIntFromDB(ctx, token.CirculationKey)
	return token.TotalSupply - inCirc
}

// init initializes runtime trigger, TokenConfig and storage context before any
// other contract method is called
func init() {
	trigger = runtime.GetTrigger()
	token = newTokenConfig()
	ctx = storage.GetContext()
}

// checkOwnerWitness is a helper function which checks whether the invoker is the
// owner of the contract.
func checkOwnerWitness() bool {
	// This is used to verify if a transfer of system assets (NEO and Gas)
	// involving this contract's address can proceed.
	if trigger == runtime.Application {
		// Check if the invoker is the owner of the contract.
		return runtime.CheckWitness(token.Owner)
	}
	return false
}

// Name returns the token name
func Name() interface{} {
	if trigger != runtime.Application {
		return false
	}
	return token.Name
}

// Decimals returns the token decimals
func Decimals() interface{} {
	if trigger != runtime.Application {
		return false
	}
	return token.Decimals
}

// Symbol returns the token symbol
func Symbol() interface{} {
	if trigger != runtime.Application {
		return false
	}
	return token.Symbol
}

// TotalSupply returns the token total supply value
func TotalSupply() interface{} {
	if trigger != runtime.Application {
		return false
	}
	return getIntFromDB(ctx, token.CirculationKey)
}

// BalanceOf returns the amount of token on the specified address
func BalanceOf(holder []byte) interface{} {
	if trigger != runtime.Application {
		return false
	}
	return getIntFromDB(ctx, holder)
}

// Transfer transfers specified amount of token from one user to another
func Transfer(from, to []byte, amount int) bool {
	if trigger != runtime.Application {
		return false
	}
	if amount <= 0 || len(to) != 20 || !runtime.CheckWitness(from) {
		return false
	}
	amountFrom := getIntFromDB(ctx, from)
	if amountFrom < amount {
		return false
	}
	if amountFrom == amount {
		storage.Delete(ctx, from)
	} else {
		diff := amountFrom - amount
		storage.Put(ctx, from, diff)
	}
	amountTo := getIntFromDB(ctx, to)
	totalAmountTo := amountTo + amount
	storage.Put(ctx, to, totalAmountTo)
	return true
}

// TransferFrom transfers specified amount of token from one user to another.
// It differs from Transfer in that it use allowance value to store the amount
// of token available to transfer.
func TransferFrom(from, to []byte, amount int) bool {
	if trigger != runtime.Application {
		return false
	}
	if amount <= 0 {
		return false
	}
	availableKey := append(from, to...)
	if len(availableKey) != 40 {
		return false
	}
	availableTo := getIntFromDB(ctx, availableKey)
	if availableTo < amount {
		return false
	}
	fromBalance := getIntFromDB(ctx, from)
	if fromBalance < amount {
		return false
	}
	toBalance := getIntFromDB(ctx, to)
	newFromBalance := fromBalance - amount
	newToBalance := toBalance + amount
	storage.Put(ctx, to, newToBalance)
	storage.Put(ctx, from, newFromBalance)

	newAllowance := availableTo - amount
	if newAllowance == 0 {
		storage.Delete(ctx, availableKey)
	} else {
		storage.Put(ctx, availableKey, newAllowance)
	}
	return true
}

// Approve stores token transfer data if the owner has enough token to send.
func Approve(owner, spender []byte, amount int) bool {
	if !checkOwnerWitness() || amount < 0 {
		return false
	}
	if len(spender) != 20 {
		return false
	}
	toSpend := getIntFromDB(ctx, owner)
	if toSpend < amount {
		return false
	}
	approvalKey := append(owner, spender...)
	if amount == 0 {
		storage.Delete(ctx, approvalKey)
	} else {
		storage.Put(ctx, approvalKey, amount)
	}
	return true
}

// Allowance returns allowance value for specified sender and receiver.
func Allowance(from, to []byte) interface{} {
	if trigger != runtime.Application {
		return false
	}
	key := append(from, to...)
	return getIntFromDB(ctx, key)
}

// Mint initial supply of tokens
func Mint(to []byte) bool {
	if trigger != runtime.Application {
		return false
	}
	if !checkOwnerWitness() {
		return false
	}
	minted := storage.Get(ctx, []byte("minted"))
	if minted != nil && minted.(bool) == true {
		return false
	}

	storage.Put(ctx, to, token.TotalSupply)
	storage.Put(ctx, []byte("minted"), true)
	addToCirculation(token.TotalSupply)
	return true
}