package currency

import (
	"encoding/json"
	"errors"
	"fmt"
	"strings"
	"unicode"
)

var (
	// ErrCurrencyCodeEmpty defines an error if the currency code is empty
	ErrCurrencyCodeEmpty = errors.New("currency code is empty")
	// ErrCurrencyNotFound returned when a currency is not found in a list
	ErrCurrencyNotFound = errors.New("currency code not found in list")
	// ErrCurrencyPairEmpty defines an error if the currency pair is empty
	ErrCurrencyPairEmpty = errors.New("currency pair is empty")
	// ErrCurrencyPairsEmpty returns when a currency.Pairs has len == 0
	ErrCurrencyPairsEmpty = errors.New("currency pairs is empty")
	// EMPTYCODE is an empty currency code
	EMPTYCODE = Code{}
	// EMPTYPAIR is an empty currency pair
	EMPTYPAIR = Pair{}

	errItemIsNil   = errors.New("item is nil")
	errItemIsEmpty = errors.New("item is empty")
	errRoleUnset   = errors.New("role unset")
)

// String implements the stringer interface and returns a string representation
// of the underlying role.
func (r Role) String() string {
	switch r {
	case Fiat:
		return FiatCurrencyString
	case Cryptocurrency:
		return CryptocurrencyString
	case Token:
		return TokenString
	case Contract:
		return ContractString
	case Stable:
		return StableString
	default:
		return UnsetRoleString
	}
}

// MarshalJSON conforms Role to the marshaller interface
func (r Role) MarshalJSON() ([]byte, error) {
	return json.Marshal(r.String())
}

// UnmarshalJSON conforms Role to the unmarshaller interface
func (r *Role) UnmarshalJSON(d []byte) error {
	var incoming string
	err := json.Unmarshal(d, &incoming)
	if err != nil {
		return err
	}

	switch incoming {
	case UnsetRoleString:
		*r = Unset
	case FiatCurrencyString:
		*r = Fiat
	case CryptocurrencyString:
		*r = Cryptocurrency
	case TokenString:
		*r = Token
	case ContractString:
		*r = Contract
	case StableString:
		*r = Stable
	default:
		return fmt.Errorf("unmarshal error role type %s unsupported for currency",
			incoming)
	}
	return nil
}

// HasData returns true if the type contains data
func (b *BaseCodes) HasData() bool {
	b.mtx.Lock()
	defer b.mtx.Unlock()
	return len(b.Items) != 0
}

// GetFullCurrencyData returns a type that is read to dump to file
func (b *BaseCodes) GetFullCurrencyData() (File, error) {
	var file File
	b.mtx.Lock()
	defer b.mtx.Unlock()
	for _, stored := range b.Items {
		for x := range stored {
			switch stored[x].Role {
			case Unset:
				file.UnsetCurrency = append(file.UnsetCurrency, stored[x])
			case Fiat:
				file.FiatCurrency = append(file.FiatCurrency, stored[x])
			case Cryptocurrency:
				file.Cryptocurrency = append(file.Cryptocurrency, stored[x])
			case Token:
				file.Token = append(file.Token, stored[x])
			case Contract:
				file.Contracts = append(file.Contracts, stored[x])
			case Stable:
				file.Stable = append(file.Stable, stored[x])
			default:
				return file, errors.New("role undefined")
			}
		}
	}
	file.LastMainUpdate = b.LastMainUpdate.Unix()
	return file, nil
}

// GetCurrencies gets the full currency list from the base code type available
// from the currency system
func (b *BaseCodes) GetCurrencies() Currencies {
	b.mtx.Lock()
	currencies := make(Currencies, len(b.Items))
	var target int
	for _, stored := range b.Items {
		if len(stored) == 0 {
			continue
		}
		currencies[target] = Code{Item: stored[0]}
		target++
	}
	b.mtx.Unlock()
	return currencies
}

// UpdateCurrency updates or registers a currency/contract
func (b *BaseCodes) UpdateCurrency(update *Item) error {
	if update == nil {
		return errItemIsNil
	}

	if update.Symbol == "" {
		return errSymbolEmpty
	}

	if update.Role == Unset {
		return fmt.Errorf("cannot update currency %w for %s", errRoleUnset, update.Symbol)
	}

	update.Symbol = strings.ToUpper(update.Symbol)
	update.Lower = strings.ToLower(update.Symbol)

	b.mtx.Lock()
	defer b.mtx.Unlock()

	stored, ok := b.Items[update.Symbol]
	if ok {
		for x := range stored {
			if stored[x].Role != Unset && stored[x].Role != update.Role {
				continue
			}

			stored[x].Role = update.Role // NOTE: Update role is checked above.

			if update.FullName != "" {
				stored[x].FullName = update.FullName
			}
			if update.AssocChain != "" {
				stored[x].AssocChain = update.AssocChain
			}
			if update.ID != 0 {
				stored[x].ID = update.ID
			}
			return nil
		}
	}
	b.Items[update.Symbol] = append(b.Items[update.Symbol], update)
	return nil
}

// Register registers a currency from a string and returns a currency code, this
// can optionally include a role when it is known.
func (b *BaseCodes) Register(c string, newRole Role) Code {
	if c == "" {
		return EMPTYCODE
	}

	var format bool
	// Digits fool upper and lower casing. So find first letter and check case.
	for x := range c {
		if !unicode.IsDigit(rune(c[x])) {
			format = unicode.IsUpper(rune(c[x]))
			break
		}
	}

	// Force upper string storage and matching
	c = strings.ToUpper(c)

	b.mtx.Lock()
	defer b.mtx.Unlock()

	if b.Items == nil {
		b.Items = make(map[string][]*Item)
	}

	stored, ok := b.Items[c]
	if ok {
		for x := range stored {
			if newRole != Unset {
				if stored[x].Role != Unset && stored[x].Role != newRole {
					// This will duplicate item with same name but different
					// role if not matched in stored list.
					// TODO: This will need a specific update to NewCode() or
					// new function to find the exact name and role.
					continue
				}
				stored[x].Role = newRole
			}
			return Code{Item: stored[x], UpperCase: format}
		}
	}

	newItem := &Item{Symbol: c, Lower: strings.ToLower(c), Role: newRole}
	b.Items[c] = append(b.Items[c], newItem)
	return Code{Item: newItem, UpperCase: format}
}

// LoadItem sets item data
func (b *BaseCodes) LoadItem(item *Item) error {
	if item == nil {
		return errItemIsNil
	}

	if *item == (Item{}) {
		return errItemIsEmpty
	}

	item.Symbol = strings.ToUpper(item.Symbol)
	item.Lower = strings.ToLower(item.Symbol)

	b.mtx.Lock()
	defer b.mtx.Unlock()

	stored, ok := b.Items[item.Symbol]
	if ok {
		for x := range stored {
			if stored[x].Role == item.Role {
				return nil
			}

			if stored[x].Role == Unset && item.Role != Unset {
				stored[x].Role = item.Role
				return nil
			}
		}
	}
	b.Items[item.Symbol] = append(b.Items[item.Symbol], item)
	return nil
}

// NewCode returns a new currency registered code
func NewCode(c string) Code {
	return storage.ValidateCode(c)
}

// String conforms to the stringer interface
func (i *Item) String() string {
	return i.Symbol
}

// String converts the code to string
func (c Code) String() string {
	if c.Item == nil {
		return ""
	}
	if c.UpperCase {
		return c.Item.Symbol
	}
	return c.Item.Lower
}

// Lower converts the code to lowercase formatting
func (c Code) Lower() Code {
	c.UpperCase = false
	return c
}

// Upper converts the code to uppercase formatting
func (c Code) Upper() Code {
	c.UpperCase = true
	return c
}

// UnmarshalJSON comforms type to the umarshaler interface
func (c *Code) UnmarshalJSON(d []byte) error {
	var newcode string
	err := json.Unmarshal(d, &newcode)
	if err != nil {
		return err
	}
	*c = NewCode(newcode)
	return nil
}

// MarshalJSON conforms type to the marshaler interface
func (c Code) MarshalJSON() ([]byte, error) {
	if c.Item == nil {
		return json.Marshal("")
	}
	return json.Marshal(c.String())
}

// IsEmpty returns true if the code is empty
func (c Code) IsEmpty() bool {
	return c.Item == nil || c.Item.Symbol == ""
}

// Equal returns if the code supplied is the same as the corresponding code
func (c Code) Equal(check Code) bool {
	return c.Item == check.Item
}

// IsFiatCurrency checks if the currency passed is an enabled fiat currency
func (c Code) IsFiatCurrency() bool {
	return c.Item != nil && c.Item.Role == Fiat
}

// IsCryptocurrency checks if the currency passed is an enabled CRYPTO currency.
// NOTE: All unset currencies will default to cryptocurrencies and stable coins
// are cryptocurrencies as well.
func (c Code) IsCryptocurrency() bool {
	return c.Item != nil && c.Item.Role&(Cryptocurrency|Stable) == c.Item.Role
}

// IsStableCurrency checks if the currency is a stable currency.
func (c Code) IsStableCurrency() bool {
	return c.Item != nil && c.Item.Role == Stable
}

// Currency allows an item to revert to a code
func (i *Item) Currency() Code {
	if i == nil {
		return EMPTYCODE
	}
	return NewCode(i.Symbol)
}

// UpperCurrency allows an item to revert to a code
// taking an upper
func (i *Item) UpperCurrency() Code {
	if i == nil {
		return EMPTYCODE.Upper()
	}
	return NewCode(i.Symbol).Upper()
}

// LowerCurrency allows an item to revert to a code
// returning in lower format
func (i *Item) LowerCurrency() Code {
	if i == nil {
		return EMPTYCODE.Lower()
	}
	return NewCode(i.Symbol).Lower()
}
