package multibase

import (
	"encoding/base64"
	"encoding/hex"
	"fmt"
	"unicode/utf8"

	b58 "github.com/mr-tron/base58"
	b32 "github.com/multiformats/go-base32"
	b36 "github.com/multiformats/go-base36"
)

// Encoding identifies the type of base-encoding that a multibase is carrying.
type Encoding int

// These are the encodings specified in the standard, not are all
// supported yet
const (
	Identity          = 0x00
	Base2             = '0'
	Base8             = '7'
	Base10            = '9'
	Base16            = 'f'
	Base16Upper       = 'F'
	Base32            = 'b'
	Base32Upper       = 'B'
	Base32pad         = 'c'
	Base32padUpper    = 'C'
	Base32hex         = 'v'
	Base32hexUpper    = 'V'
	Base32hexPad      = 't'
	Base32hexPadUpper = 'T'
	Base36            = 'k'
	Base36Upper       = 'K'
	Base45            = 'R'
	Base58BTC         = 'z'
	Base58Flickr      = 'Z'
	Base64            = 'm'
	Base64url         = 'u'
	Base64pad         = 'M'
	Base64urlPad      = 'U'
	Base256Emoji      = '🚀'
)

// EncodingToStr is a map of the supported encoding, unsupported encoding
// specified in standard are left out
var EncodingToStr = map[Encoding]string{
	0x00:         "identity",
	'0':          "base2",
	'f':          "base16",
	'F':          "base16upper",
	'b':          "base32",
	'B':          "base32upper",
	'c':          "base32pad",
	'C':          "base32padupper",
	'v':          "base32hex",
	'V':          "base32hexupper",
	't':          "base32hexpad",
	'T':          "base32hexpadupper",
	'k':          "base36",
	'K':          "base36upper",
	'z':          "base58btc",
	'Z':          "base58flickr",
	'm':          "base64",
	'u':          "base64url",
	'M':          "base64pad",
	'U':          "base64urlpad",
	Base256Emoji: "base256emoji",
}

var Encodings = map[string]Encoding{}

func init() {
	for e, n := range EncodingToStr {
		Encodings[n] = e
	}
}

// ErrUnsupportedEncoding is returned when the selected encoding is not known or
// implemented.
var ErrUnsupportedEncoding = fmt.Errorf("selected encoding not supported")

// Encode encodes a given byte slice with the selected encoding and returns a
// multibase string (<encoding><base-encoded-string>). It will return
// an error if the selected base is not known.
func Encode(base Encoding, data []byte) (string, error) {
	switch base {
	case Identity:
		// 0x00 inside a string is OK in golang and causes no problems with the length calculation.
		return string(rune(Identity)) + string(data), nil
	case Base2:
		return string(Base2) + binaryEncodeToString(data), nil
	case Base16:
		return string(Base16) + hex.EncodeToString(data), nil
	case Base16Upper:
		return string(Base16Upper) + hexEncodeToStringUpper(data), nil
	case Base32:
		return string(Base32) + base32StdLowerNoPad.EncodeToString(data), nil
	case Base32Upper:
		return string(Base32Upper) + base32StdUpperNoPad.EncodeToString(data), nil
	case Base32hex:
		return string(Base32hex) + base32HexLowerNoPad.EncodeToString(data), nil
	case Base32hexUpper:
		return string(Base32hexUpper) + base32HexUpperNoPad.EncodeToString(data), nil
	case Base32pad:
		return string(Base32pad) + base32StdLowerPad.EncodeToString(data), nil
	case Base32padUpper:
		return string(Base32padUpper) + base32StdUpperPad.EncodeToString(data), nil
	case Base32hexPad:
		return string(Base32hexPad) + base32HexLowerPad.EncodeToString(data), nil
	case Base32hexPadUpper:
		return string(Base32hexPadUpper) + base32HexUpperPad.EncodeToString(data), nil
	case Base36:
		return string(Base36) + b36.EncodeToStringLc(data), nil
	case Base36Upper:
		return string(Base36Upper) + b36.EncodeToStringUc(data), nil
	case Base58BTC:
		return string(Base58BTC) + b58.EncodeAlphabet(data, b58.BTCAlphabet), nil
	case Base58Flickr:
		return string(Base58Flickr) + b58.EncodeAlphabet(data, b58.FlickrAlphabet), nil
	case Base64pad:
		return string(Base64pad) + base64.StdEncoding.EncodeToString(data), nil
	case Base64urlPad:
		return string(Base64urlPad) + base64.URLEncoding.EncodeToString(data), nil
	case Base64url:
		return string(Base64url) + base64.RawURLEncoding.EncodeToString(data), nil
	case Base64:
		return string(Base64) + base64.RawStdEncoding.EncodeToString(data), nil
	case Base256Emoji:
		return string(Base256Emoji) + base256emojiEncode(data), nil
	default:
		return "", ErrUnsupportedEncoding
	}
}

// Decode takes a multibase string and decodes into a bytes buffer.
// It will return an error if the selected base is not known.
func Decode(data string) (Encoding, []byte, error) {
	if len(data) == 0 {
		return 0, nil, fmt.Errorf("cannot decode multibase for zero length string")
	}

	r, _ := utf8.DecodeRuneInString(data)
	enc := Encoding(r)

	switch enc {
	case Identity:
		return Identity, []byte(data[1:]), nil
	case Base2:
		bytes, err := decodeBinaryString(data[1:])
		return enc, bytes, err
	case Base16, Base16Upper:
		bytes, err := hex.DecodeString(data[1:])
		return enc, bytes, err
	case Base32, Base32Upper:
		bytes, err := b32.RawStdEncoding.DecodeString(data[1:])
		return enc, bytes, err
	case Base32hex, Base32hexUpper:
		bytes, err := b32.RawHexEncoding.DecodeString(data[1:])
		return enc, bytes, err
	case Base32pad, Base32padUpper:
		bytes, err := b32.StdEncoding.DecodeString(data[1:])
		return enc, bytes, err
	case Base32hexPad, Base32hexPadUpper:
		bytes, err := b32.HexEncoding.DecodeString(data[1:])
		return enc, bytes, err
	case Base36, Base36Upper:
		bytes, err := b36.DecodeString(data[1:])
		return enc, bytes, err
	case Base58BTC:
		bytes, err := b58.DecodeAlphabet(data[1:], b58.BTCAlphabet)
		return Base58BTC, bytes, err
	case Base58Flickr:
		bytes, err := b58.DecodeAlphabet(data[1:], b58.FlickrAlphabet)
		return Base58Flickr, bytes, err
	case Base64pad:
		bytes, err := base64.StdEncoding.DecodeString(data[1:])
		return Base64pad, bytes, err
	case Base64urlPad:
		bytes, err := base64.URLEncoding.DecodeString(data[1:])
		return Base64urlPad, bytes, err
	case Base64:
		bytes, err := base64.RawStdEncoding.DecodeString(data[1:])
		return Base64, bytes, err
	case Base64url:
		bytes, err := base64.RawURLEncoding.DecodeString(data[1:])
		return Base64url, bytes, err
	case Base256Emoji:
		bytes, err := base256emojiDecode(data[4:])
		return Base256Emoji, bytes, err
	default:
		return -1, nil, ErrUnsupportedEncoding
	}
}
