package moneroutil

import (
	"encoding/binary"
)

const BASE58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"

const REVERSEBASE58 =
//               0   1   2   3   4   5   6   7   8   9   a   b   c   d   e   f */
/* 00 */ "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" +
	/* 10 */ "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" +
	/* 20 */ "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" +
	/* 30 */ "\xff\x00\x01\x02\x03\x04\x05\x06\x07\x08\xff\xff\xff\xff\xff\xff" +
	/* 40 */ "\xff\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\xff\x11\x12\x13\x14\x15\xff" +
	/* 50 */ "\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20\xff\xff\xff\xff\xff" +
	/* 60 */ "\xff\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\xff\x2c\x2d\x2e" +
	/* 70 */ "\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\xff\xff\xff\xff\xff" +
	/* 80 */ "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" +
	/* 90 */ "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" +
	/* a0 */ "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" +
	/* b0 */ "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" +
	/* c0 */ "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" +
	/* d0 */ "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" +
	/* e0 */ "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" +
	/* f0 */ "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff"

func encodeChunk(raw []byte, buf []byte) []byte {
	intToDecode := binary.BigEndian.Uint64(raw[:])

	for intToDecode > 0 {
		buf = append(buf, BASE58[intToDecode%58])
		intToDecode /= 58
	}
	for len(buf) < 11 {
		buf = append(buf, '1')
	}
	for i, j := 0, len(buf)-1; i < j; i, j = i+1, j-1 {
		buf[i], buf[j] = buf[j], buf[i]
	}
	return buf
}
func encodeChunkTail(raw []byte, buf []byte) []byte {
	var data [8]byte
	copy(data[8-len(raw):], raw)

	intToDecode := binary.BigEndian.Uint64(data[:])

	for intToDecode > 0 {
		buf = append(buf, BASE58[intToDecode%58])
		intToDecode /= 58
	}
	for len(buf) < 7 {
		buf = append(buf, '1')
	}
	for i, j := 0, len(buf)-1; i < j; i, j = i+1, j-1 {
		buf[i], buf[j] = buf[j], buf[i]
	}
	return buf
}

func decodeChunk(buf []byte, encoded []byte) []byte {
	var intResult uint64
	currentMultiplier := uint64(1)
	for i := len(encoded) - 1; i >= 0; i-- {
		intResult += currentMultiplier * uint64(REVERSEBASE58[encoded[i]])
		// this can overflow, but only on the last iteration when i == 0 when all data is valid
		currentMultiplier *= 58
	}

	var result [8]byte
	binary.BigEndian.PutUint64(result[:], intResult)
	switch len(encoded) {
	case 0:
		return append(buf, result[8:]...)
	case 2:
		return append(buf, result[7:]...)
	case 3:
		return append(buf, result[6:]...)
	case 5:
		return append(buf, result[5:]...)
	case 6:
		return append(buf, result[4:]...)
	case 7:
		return append(buf, result[3:]...)
	case 9:
		return append(buf, result[2:]...)
	case 10:
		return append(buf, result[1:]...)
	case 11:
		return append(buf, result[:]...)
	default:
	}
	return nil
}

func EncodeMoneroBase58(data ...[]byte) []byte {
	return EncodeMoneroBase58PreAllocated(make([]byte, 0, func() (result int) {
		for _, v := range data {
			result += len(v)
		}
		return
	}()), data...)
}

func EncodeMoneroBase58PreAllocated(buf []byte, data ...[]byte) []byte {
	//preallocate common case
	combined := make([]byte, 0, 96)
	for _, item := range data {
		combined = append(combined, item...)
	}

	result := buf
	tmpBuf := make([]byte, 0, len(combined)*2)
	length := len(combined)
	rounds := length / 8
	for i := 0; i < rounds; i++ {
		result = append(result, encodeChunk(combined[i*8:(i+1)*8], tmpBuf[:0])...)
	}
	if length%8 > 0 {
		result = append(result, encodeChunkTail(combined[rounds*8:], tmpBuf[:0])...)
	}
	return result
}

func DecodeMoneroBase58(data []byte) (result []byte) {
	//common case
	return DecodeMoneroBase58PreAllocated(make([]byte, 0, 69), data)
}

func DecodeMoneroBase58PreAllocated(buf []byte, data []byte) (result []byte) {
	result = buf
	length := len(data)
	rounds := length / 11
	for i := 0; i < rounds; i++ {
		result = decodeChunk(result, data[i*11:i*11+11])
	}
	if length%11 > 0 {
		result = decodeChunk(result, data[rounds*11:])
	}
	return
}
