# Python SCALE Codec Library
#
# Copyright 2018-2020 openAware BV (NL).
# This file is part of Polkascan.
#
# Polkascan is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Polkascan is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Polkascan. If not, see <http://www.gnu.org/licenses/>.

from datetime import datetime
from hashlib import blake2b

from scalecodec.base import ScaleType, ScaleBytes
from scalecodec.exceptions import InvalidScaleTypeValueException


class Compact(ScaleType):

    def __init__(self, data=None, **kwargs):
        self.compact_length = 0
        self.compact_bytes = None
        super().__init__(data, **kwargs)

    def process_compact_bytes(self):
        compact_byte = self.get_next_bytes(1)
        try:
            byte_mod = compact_byte[0] % 4
        except IndexError:
            raise InvalidScaleTypeValueException("Invalid byte for Compact")

        if byte_mod == 0:
            self.compact_length = 1
        elif byte_mod == 1:
            self.compact_length = 2
        elif byte_mod == 2:
            self.compact_length = 4
        else:
            self.compact_length = int(5 + (compact_byte[0] - 3) / 4)

        if self.compact_length == 1:
            self.compact_bytes = compact_byte
        elif self.compact_length in [2, 4]:
            self.compact_bytes = compact_byte + self.get_next_bytes(self.compact_length - 1)
        else:
            self.compact_bytes = self.get_next_bytes(self.compact_length - 1)

        return self.compact_bytes

    def process(self):

        self.process_compact_bytes()

        if self.sub_type:

            byte_data = self.get_decoder_class(self.sub_type, ScaleBytes(self.compact_bytes)).process()

            if type(byte_data) is int and self.compact_length <= 4:
                return int(byte_data / 4)
            else:
                return byte_data
        else:
            return self.compact_bytes

    def process_encode(self, value):

        value = int(value)

        if value <= 0b00111111:
            return ScaleBytes(bytearray(int(value << 2).to_bytes(1, 'little')))

        elif value <= 0b0011111111111111:
            return ScaleBytes(bytearray(int((value << 2) | 0b01).to_bytes(2, 'little')))

        elif value <= 0b00111111111111111111111111111111:

            return ScaleBytes(bytearray(int((value << 2) | 0b10).to_bytes(4, 'little')))

        else:
            for bytes_length in range(4, 68):
                if 2 ** (8 * (bytes_length - 1)) <= value < 2 ** (8 * bytes_length):
                    return ScaleBytes(bytearray(
                        ((bytes_length - 4) << 2 | 0b11).to_bytes(1, 'little') + value.to_bytes(bytes_length,
                                                                                                'little')))
            else:
                raise ValueError('{} out of range'.format(value))


class CompactU32(Compact):
    """
    Specialized composite implementation for performance improvement
    """

    type_string = 'Compact<u32>'

    def process(self):
        self.process_compact_bytes()

        if self.compact_length <= 4:
            return int(int.from_bytes(self.compact_bytes, byteorder='little') / 4)
        else:
            return int.from_bytes(self.compact_bytes, byteorder='little')

    def process_encode(self, value):

        if value <= 0b00111111:
            return ScaleBytes(bytearray(int(value << 2).to_bytes(1, 'little')))

        elif value <= 0b0011111111111111:
            return ScaleBytes(bytearray(int((value << 2) | 0b01).to_bytes(2, 'little')))

        elif value <= 0b00111111111111111111111111111111:

            return ScaleBytes(bytearray(int((value << 2) | 0b10).to_bytes(4, 'little')))

        else:
            for bytes_length in range(4, 68):
                if 2 ** (8 * (bytes_length-1)) <= value < 2 ** (8 * bytes_length):
                    return ScaleBytes(bytearray(((bytes_length - 4) << 2 | 0b11).to_bytes(1, 'little') + value.to_bytes(bytes_length, 'little')))
            else:
                raise ValueError('{} out of range'.format(value))


class Option(ScaleType):
    def process(self):

        option_byte = self.get_next_bytes(1)

        if self.sub_type and option_byte != b'\x00':
            return self.process_type(self.sub_type).value

        return None

    def process_encode(self, value):

        if value is not None and self.sub_type:
            sub_type_obj = self.get_decoder_class(self.sub_type)
            return ScaleBytes('0x01') + sub_type_obj.encode(value)

        return ScaleBytes('0x00')


class Bytes(ScaleType):

    type_string = 'Vec<u8>'

    def process(self):

        length = self.process_type('Compact<u32>').value
        value = self.get_next_bytes(length)

        try:
            return value.decode()
        except UnicodeDecodeError:
            return '0x{}'.format(value.hex())

    def process_encode(self, value):
        string_length_compact = CompactU32()

        if value[0:2] == '0x':
            # TODO implicit HexBytes conversion can have unexpected result if string is actually starting with '0x'
            value = bytes.fromhex(value[2:])
            data = string_length_compact.encode(len(value))
            data += value
        else:
            data = string_length_compact.encode(len(value))
            data += value.encode()

        return data


class OptionBytes(ScaleType):

    type_string = 'Option<Vec<u8>>'

    def process(self):

        option_byte = self.get_next_bytes(1)

        if option_byte != b'\x00':
            return self.process_type('Bytes').value

        return None


# TODO replace in metadata
class String(ScaleType):

    def process(self):

        length = self.process_type('Compact<u32>').value
        value = self.get_next_bytes(length)

        return value.decode()

    def process_encode(self, value):
        string_length_compact = CompactU32()
        data = string_length_compact.encode(len(value))
        data += value.encode()
        return data


class HexBytes(ScaleType):

    def process(self):

        length = self.process_type('Compact<u32>').value

        return '0x{}'.format(self.get_next_bytes(length).hex())

    def process_encode(self, value):

        if value[0:2] != '0x':
            raise ValueError('HexBytes value should start with "0x"')

        value = bytes.fromhex(value[2:])

        string_length_compact = CompactU32()
        data = string_length_compact.encode(len(value))
        data += value
        return data


class CallBytes(ScaleType):

    def process(self):
        raise NotImplementedError()

    def process_encode(self, value):
        return bytes.fromhex(value[2:])


class U8(ScaleType):

    def process(self):
        return self.get_next_u8()

    def process_encode(self, value):

        if 0 <= int(value) <= 2**8 - 1:
            return ScaleBytes(bytearray(int(value).to_bytes(1, 'little')))
        else:
            raise ValueError('{} out of range for u8'.format(value))


class U16(ScaleType):

    def process(self):
        return int.from_bytes(self.get_next_bytes(2), byteorder='little')

    def process_encode(self, value):

        if 0 <= int(value) <= 2**16 - 1:
            return ScaleBytes(bytearray(int(value).to_bytes(2, 'little')))
        else:
            raise ValueError('{} out of range for u16'.format(value))


class U32(ScaleType):

    def process(self):
        return int.from_bytes(self.get_next_bytes(4), byteorder='little')

    def process_encode(self, value):

        if 0 <= int(value) <= 2**32 - 1:
            return ScaleBytes(bytearray(int(value).to_bytes(4, 'little')))
        else:
            raise ValueError('{} out of range for u32'.format(value))


class U64(ScaleType):

    def process(self):
        return int(int.from_bytes(self.get_next_bytes(8), byteorder='little'))

    def process_encode(self, value):

        if 0 <= int(value) <= 2**64 - 1:
            return ScaleBytes(bytearray(int(value).to_bytes(8, 'little')))
        else:
            raise ValueError('{} out of range for u64'.format(value))


class U128(ScaleType):

    def process(self):
        return int(int.from_bytes(self.get_next_bytes(16), byteorder='little'))

    def process_encode(self, value):

        if 0 <= int(value) <= 2**128 - 1:
            return ScaleBytes(bytearray(int(value).to_bytes(16, 'little')))
        else:
            raise ValueError('{} out of range for u128'.format(value))


class I8(ScaleType):

    def process(self):
        return int.from_bytes(self.get_next_bytes(1), byteorder='little', signed=True)

    def process_encode(self, value):

        if -128 <= int(value) <= 127:
            return ScaleBytes(bytearray(int(value).to_bytes(1, 'little', signed=True)))
        else:
            raise ValueError('{} out of range for i8'.format(value))


class I16(ScaleType):

    def process(self):
        return int.from_bytes(self.get_next_bytes(2), byteorder='little', signed=True)

    def process_encode(self, value):

        if -32768 <= int(value) <= 32767:
            return ScaleBytes(bytearray(int(value).to_bytes(2, 'little', signed=True)))
        else:
            raise ValueError('{} out of range for i16'.format(value))


class I32(ScaleType):

    def process(self):
        return int.from_bytes(self.get_next_bytes(4), byteorder='little', signed=True)

    def process_encode(self, value):

        if -2147483648 <= int(value) <= 2147483647:
            return ScaleBytes(bytearray(int(value).to_bytes(4, 'little', signed=True)))
        else:
            raise ValueError('{} out of range for i32'.format(value))


class I64(ScaleType):

    def process(self):
        return int.from_bytes(self.get_next_bytes(8), byteorder='little', signed=True)

    def process_encode(self, value):

        if -2**64 <= int(value) <= 2**64-1:
            return ScaleBytes(bytearray(int(value).to_bytes(8, 'little', signed=True)))
        else:
            raise ValueError('{} out of range for i64'.format(value))


class I128(ScaleType):

    def process(self):
        return int.from_bytes(self.get_next_bytes(16), byteorder='little', signed=True)

    def process_encode(self, value):

        if -2**128 <= int(value) <= 2**128-1:
            return ScaleBytes(bytearray(int(value).to_bytes(16, 'little', signed=True)))
        else:
            raise ValueError('{} out of range for i128'.format(value))


class I256(ScaleType):

    def process(self):
        return int.from_bytes(self.get_next_bytes(32), byteorder='little', signed=True)

    def process_encode(self, value):

        if -2**256 <= int(value) <= 2**256-1:
            return ScaleBytes(bytearray(int(value).to_bytes(32, 'little', signed=True)))
        else:
            raise ValueError('{} out of range for i256'.format(value))


class H160(ScaleType):

    def process(self):
        return '0x{}'.format(self.get_next_bytes(20).hex())

    def process_encode(self, value):
        if value[0:2] != '0x' or len(value) != 42:
            raise ValueError('Value should start with "0x" and should be 20 bytes long')
        return ScaleBytes(value)


class H256(ScaleType):

    def process(self):
        return '0x{}'.format(self.get_next_bytes(32).hex())

    def process_encode(self, value):
        if value[0:2] != '0x' or len(value) != 66:
            raise ValueError('Value should start with "0x" and should be 32 bytes long')
        return ScaleBytes(value)


class H512(ScaleType):

    def process(self):
        return '0x{}'.format(self.get_next_bytes(64).hex())

    def process_encode(self, value):
        if value[0:2] != '0x' or len(value) != 130:
            raise ValueError('Value should start with "0x" and should be 64 bytes long')
        return ScaleBytes(value)


class Struct(ScaleType):

    def __init__(self, data, type_mapping=None, **kwargs):

        if type_mapping:
            self.type_mapping = type_mapping

        super().__init__(data, **kwargs)

    def process(self):

        result = {}
        for key, data_type in self.type_mapping:
            result[key] = self.process_type(data_type, metadata=self.metadata).value

        return result

    def process_encode(self, value):
        data = ScaleBytes(bytearray())

        if type(value) is list:
            if len(value) != len(self.type_mapping):
                raise ValueError('Element count of value ({}) doesn\'t match type_mapping ({})'.format(len(value), len(self.type_mapping)))

            for idx, (key, data_type) in enumerate(self.type_mapping):

                element_obj = self.get_decoder_class(data_type, metadata=self.metadata)
                data += element_obj.encode(value[idx])

        else:
            for key, data_type in self.type_mapping:
                if key not in value:
                    raise ValueError('Element "{}" of struct is missing in given value'.format(key))

                element_obj = self.get_decoder_class(data_type, metadata=self.metadata)
                data += element_obj.encode(value[key])

        return data


class Set(ScaleType):
    value_list = []
    value_type = 'u64'

    def __init__(self, data, value_list=None, **kwargs):
        self.set_value = None

        if value_list:
            self.value_list = value_list

        super().__init__(data, **kwargs)

    def process(self):
        self.set_value = self.process_type(self.value_type).value
        result = []
        if self.set_value > 0:

            for value, set_mask in self.value_list.items():
                if self.set_value & set_mask > 0:
                    result.append(value)
        return result

    def process_encode(self, value):
        result = 0
        if type(value) is not list:
            raise ValueError('Value for encoding a set must be a list')

        for item, set_mask in self.value_list.items():
            if item in value:
                result += set_mask

        u64_obj = self.get_decoder_class(self.value_type)

        return u64_obj.encode(result)


class Era(ScaleType):

    def process(self):

        option_byte = self.get_next_bytes(1).hex()
        if option_byte == '00':
            return option_byte
        else:
            return option_byte + self.get_next_bytes(1).hex()

    def process_encode(self, value):
        if value == '00':
            return ScaleBytes('0x00')
        else:
            raise NotImplementedError('Mortal Era not implemented')


class EraIndex(U32):
    pass


class Bool(ScaleType):

    def process(self):
        return self.get_next_bool()

    def process_encode(self, value):
        if value is True:
            return ScaleBytes('0x01')
        elif value is False:
            return ScaleBytes('0x00')
        else:
            raise ValueError("Value must be boolean")


class Moment(U64):
    pass


class CompactMoment(CompactU32):
    type_string = 'Compact<Moment>'

    def process(self):
        int_value = super().process()

        if int_value > 10000000000:
            int_value = int_value / 1000

        return datetime.utcfromtimestamp(int_value)

    def serialize(self):
        return self.value.isoformat()


class BoxProposal(ScaleType):
    type_string = 'Box<Proposal>'

    def __init__(self, data, **kwargs):
        self.call_index = None
        self.call_function = None
        self.call_module = None
        self.call_args = []
        super().__init__(data, **kwargs)

    def process(self):

        self.call_index = self.get_next_bytes(2).hex()

        self.call_module, self.call_function = self.metadata.call_index[self.call_index]

        for arg in self.call_function.args:
            arg_type_obj = self.process_type(arg.type, metadata=self.metadata)

            self.call_args.append({
                'name': arg.name,
                'type': arg.type,
                'value': arg_type_obj.serialize(),
                'valueRaw': arg_type_obj.raw_value
            })

        return {
            'call_index': self.call_index,
            'call_function': self.call_function.name,
            'call_module': self.call_module.name,
            'call_args': self.call_args
        }

    def process_encode(self, value):
        # Check requirements
        if 'call_index' in value:
            self.call_index = value['call_index']

        elif 'call_module' in value and 'call_function' in value:
            # Look up call module from metadata
            for call_index, (call_module, call_function) in self.metadata.call_index.items():

                if call_module.name == value['call_module'] and call_function.name == value['call_function']:
                    self.call_index = call_index
                    self.call_module = call_module
                    self.call_function = call_function
                    break

            if not self.call_index:
                raise ValueError('Specified call module and function not found in metadata')

        elif not self.call_module or not self.call_function:
            raise ValueError('No call module and function specified')

        data = ScaleBytes(bytearray.fromhex(self.call_index))

        # Encode call params
        if len(self.call_function.args) > 0:
            for arg in self.call_function.args:
                if arg.name not in value['call_args']:
                    raise ValueError('Parameter \'{}\' not specified'.format(arg.name))
                else:
                    param_value = value['call_args'][arg.name]

                    arg_obj = self.get_decoder_class(arg.type, metadata=self.metadata)
                    data += arg_obj.encode(param_value)

        return data


class ProposalPreimage(Struct):
    type_string = '(Vec<u8>, AccountId, BalanceOf, BlockNumber)'

    type_mapping = (
        ("proposal", "HexBytes"),
        ("registredBy", "AccountId"),
        ("deposit", "BalanceOf"),
        ("blockNumber", "BlockNumber")
    )
    def process(self):

        result = {}
        for key, data_type in self.type_mapping:
            result[key] = self.process_type(data_type, metadata=self.metadata).value

        # Replace HexBytes with actual proposal
        result['proposal'] = Proposal(ScaleBytes(result['proposal']), metadata=self.metadata).decode()

        return result


class Proposal(BoxProposal):
    type_string = '<T as Trait<I>>::Proposal'


class ValidatorPrefs(Struct):
    type_string = '(Compact<Balance>)'

    type_mapping = (('commission', 'Compact<Balance>'),)


class ValidatorPrefsLegacy(Struct):
    type_string = '(Compact<u32>,Compact<Balance>)'

    type_mapping = (('unstakeThreshold', 'Compact<u32>'), ('validatorPayment', 'Compact<Balance>'))


class Linkage(Struct):
    type_string = 'Linkage<AccountId>'

    type_mapping = (
        ('previous', 'Option<AccountId>'),
        ('next', 'Option<AccountId>')
    )


class AccountId(H256):

    def __init__(self, data=None, sub_type=None, metadata=None):
        self.ss58_address = None
        super().__init__(data, sub_type, metadata)

    def process_encode(self, value):
        if value[0:2] != '0x' and len(value) == 47:
            from scalecodec.utils.ss58 import ss58_decode
            self.ss58_address = value
            value = '0x{}'.format(ss58_decode(value))
        return super().process_encode(value)


class AccountIndex(U32):
    pass


class ReferendumIndex(U32):
    pass


class PropIndex(U32):
    pass


class Vote(U8):
    pass


class SessionKey(H256):
    pass


class SessionIndex(U32):
    pass


class Balance(U128):
    pass


class ParaId(U32):
    pass


class Key(Bytes):
    pass


class KeyValue(Struct):
    type_string = '(Vec<u8>, Vec<u8>)'
    type_mapping = (('key', 'Vec<u8>'), ('value', 'Vec<u8>'))


class BalanceOf(Balance):
    pass


class NewAccountOutcome(CompactU32):
    type_string = 'NewAccountOutcome'


class Vec(ScaleType):

    def __init__(self, data=None, **kwargs):
        self.elements = []
        super().__init__(data, **kwargs)

    def process(self):
        element_count = self.process_type('Compact<u32>').value

        result = []
        for _ in range(0, element_count):
            element = self.process_type(self.sub_type, metadata=self.metadata)
            self.elements.append(element)
            result.append(element.value)

        return result

    def process_encode(self, value):

        if type(value) is not list:
            raise ValueError("Provided value is not a list")

        # encode element count to Compact<u32>
        element_count_compact = CompactU32()

        element_count_compact.encode(len(value))

        data = element_count_compact.data

        for element in value:

            element_obj = self.get_decoder_class(self.sub_type, metadata=self.metadata)
            data += element_obj.encode(element)

        return data


class VecNextAuthority(Vec):
    type_string = 'Vec<NextAuthority>'

    def process(self):
        element_count = self.process_type('Compact<u32>').value

        result = []
        for _ in range(0, element_count):
            element = self.process_type('NextAuthority')
            self.elements.append(element)
            result.append(element.value)

        return result


class Address(ScaleType):

    def __init__(self, data, **kwargs):
        self.account_length = None
        self.account_id = None
        self.account_index = None
        self.account_idx = None
        super().__init__(data, **kwargs)

    def process(self):
        self.account_length = self.get_next_bytes(1)

        if self.account_length == b'\xff':
            self.account_id = self.get_next_bytes(32).hex()
            self.account_length = self.account_length.hex()

            return self.account_id
        else:
            if self.account_length == b'\xfc':
                account_index = self.get_next_bytes(2)
            elif self.account_length == b'\xfd':
                account_index = self.get_next_bytes(4)
            elif self.account_length == b'\xfe':
                account_index = self.get_next_bytes(8)
            else:
                account_index = self.account_length

            self.account_index = account_index.hex()
            self.account_idx = int.from_bytes(account_index, byteorder='little')

            self.account_length = self.account_length.hex()

            return self.account_index

    def process_encode(self, value):

        if type(value) == str and value[0:2] != '0x':
            # Assume SS58 encoding address
            if len(value) >= 46:
                from scalecodec.utils.ss58 import ss58_decode
                value = '0x{}'.format(ss58_decode(value))
            else:
                from scalecodec.utils.ss58 import ss58_decode_account_index
                index_obj = AccountIndex()
                value = index_obj.encode(ss58_decode_account_index(value))

        if type(value) == str and value[0:2] == '0x' and len(value) == 66:
            # value is AccountId
            return ScaleBytes('0xff{}'.format(value[2:]))
        elif type(value) == int:
            # value is AccountIndex
            raise NotImplementedError('Encoding of AccountIndex Adresses not supported yet')
        else:
            raise ValueError('Value is in unsupported format, expected 32 bytes hex-string for AccountIds or int for AccountIndex')

    def serialize(self):
        if self.account_id:
            return '0x{}'.format(self.value)
        else:
            return self.value


class AccountIdAddress(Address):

    def process(self):
        self.account_id = self.process_type('AccountId').value.replace('0x', '')
        self.account_length = 'ff'
        return self.account_id

    def process_encode(self, value):
        if type(value) == str and value[0:2] != '0x':
            # Assume SS58 encoding address
            if len(value) >= 46:
                from scalecodec.utils.ss58 import ss58_decode
                value = '0x{}'.format(ss58_decode(value))
            else:
                from scalecodec.utils.ss58 import ss58_decode_account_index
                index_obj = AccountIndex()
                value = index_obj.encode(ss58_decode_account_index(value))

        if type(value) == str and value[0:2] == '0x' and len(value) == 66:
            # value is AccountId
            return ScaleBytes('0x{}'.format(value[2:]))
        elif type(value) == int:
            # value is AccountIndex
            raise NotImplementedError('Encoding of AccountIndex Adresses not supported yet')
        else:
            raise ValueError('Value is in unsupported format, expected 32 bytes hex-string for AccountIds or int for AccountIndex')


class RawAddress(Address):
    pass


class Enum(ScaleType):

    value_list = []
    type_mapping = None

    def __init__(self, data, value_list=None, type_mapping=None, **kwargs):

        self.index = None

        if type_mapping:
            self.type_mapping = type_mapping

        if value_list:
            self.value_list = value_list

        super().__init__(data, **kwargs)

    def process(self):
        self.index = int(self.get_next_bytes(1).hex(), 16)

        if self.type_mapping:
            try:
                enum_type_mapping = self.type_mapping[self.index]
                return self.process_type('Struct', type_mapping=[enum_type_mapping]).value

            except IndexError:
                raise ValueError("Index '{}' not present in Enum type mapping".format(self.index))
        else:
            try:
                return self.value_list[self.index]
            except IndexError:
                raise ValueError("Index '{}' not present in Enum value list".format(self.index))

    def process_encode(self, value):
        if self.type_mapping:

            if type(value) != dict:
                raise ValueError("Value must be a dict when type_mapping is set, not '{}'".format(value))

            if len(value) != 1:
                raise ValueError("Value for enum with type_mapping can only have one value")

            for enum_key, enum_value in value.items():
                for idx, (item_key, item_value) in enumerate(self.type_mapping):
                    if item_key == enum_key:
                        self.index = idx
                        struct_obj = self.get_decoder_class('Struct', type_mapping=[self.type_mapping[self.index]])
                        return ScaleBytes(bytearray([self.index])) + struct_obj.encode(value)

                raise ValueError("Value '{}' not present in type_mapping of this enum".format(enum_key))

        else:
            for idx, item in enumerate(self.value_list):
                if item == value:
                    self.index = idx
                    return ScaleBytes(bytearray([self.index]))

            raise ValueError("Value '{}' not present in value list of this enum".format(value))

    def get_enum_value(self):
        if self.value:

            if self.type_mapping:
                return list(self.value.values())[0]
            else:
                return self.value_list[self.index]


class Data(Enum):
    type_mapping = [
        ["None", "Null"],
        ["Raw", "Bytes"],
        ["BlakeTwo256", "H256"],
        ["Sha256", "H256"],
        ["Keccak256", "H256"],
        ["ShaThree256", "H256"]
      ]

    def process(self):

        self.index = int(self.get_next_bytes(1).hex(), 16)

        if self.index == 0:
            return {'None': None}

        elif self.index >= 1 and self.index <= 33:
            # Determine value of Raw type (length is processed in index byte)
            data = self.get_next_bytes(self.index - 1)

            try:
                value = data.decode()
            except UnicodeDecodeError:
                value = '0x{}'.format(data.hex())
            return {"Raw": value}

        elif self.index >= 34 and self.index <= 37:

            enum_value = self.type_mapping[self.index - 32][0]

            return {enum_value: self.process_type(self.type_mapping[self.index - 32][1]).value}

        raise ValueError("Unable to decode Data, invalid indicator byte '{}'".format(self.index))

    def process_encode(self, value):

        if type(value) != dict:
            raise ValueError("Value must be a dict when type_mapping is set, not '{}'".format(value))

        if len(value) != 1:
            raise ValueError("Value for enum with type_mapping can only have one value")

        for enum_key, enum_value in value.items():

            for idx, (item_key, item_value) in enumerate(self.type_mapping):
                if item_key == enum_key:
                    self.index = idx

                    if item_value == 'Null':
                        return ScaleBytes(bytearray([0]))

                    elif item_value == 'Bytes':

                        if enum_value[0:2] == '0x':

                            if len(enum_value) > 66:
                                raise ValueError("Raw type in Data cannot exceed 32 bytes")

                            enum_value = bytes.fromhex(enum_value[2:])
                            data = bytes([len(enum_value) + 1]) + enum_value
                            return ScaleBytes(bytearray(data))
                        else:

                            if len(enum_value) > 32:
                                raise ValueError("Raw type in Data cannot exceed 32 bytes")

                            data = bytes([len(enum_value) + 1]) + enum_value.encode()
                            return ScaleBytes(bytearray(data))
                    else:

                        struct_obj = self.get_decoder_class('Struct', type_mapping=[self.type_mapping[self.index]])
                        return ScaleBytes(bytearray([self.index])) + struct_obj.encode(value)

            raise ValueError("Value '{}' not present in type_mapping of this enum".format(enum_key))


class RewardDestination(Enum):

    value_list = ['Staked', 'Stash', 'Controller']


class StakingLedger(Struct):
    type_string = 'StakingLedger<AccountId, BalanceOf, BlockNumber>'
    type_mapping = (
        ('stash', 'AccountId'),
        ('total', 'Compact<Balance>'),
        ('active', 'Compact<Balance>'),
        ('unlocking', 'Vec<UnlockChunk<Balance>>'),
    )


class UnlockChunk(Struct):
    type_string = 'UnlockChunk<Balance>'
    type_mapping = (
        ('value', 'Compact<Balance>'),
        ('era', 'Compact<EraIndex>'),
    )


class Exposure(Struct):
    type_string = 'Exposure<AccountId, BalanceOf>'
    type_mapping = (
        ('total', 'Compact<Balance>'),
        ('own', 'Compact<Balance>'),
        ('others', 'Vec<IndividualExposure>'),
    )


class IndividualExposure(Struct):
    type_string = 'IndividualExposure<AccountId, Balance>'
    type_mapping = (
        ('who', 'AccountId'),
        ('value', 'Compact<Balance>'),
    )


class BabeAuthorityWeight(U64):
    pass


class Points(U32):
    pass


class EraPoints(Struct):
    type_mapping = (
        ('total', 'Points'),
        ('individual', 'Vec<Points>'),
    )


class VoteThreshold(Enum):

    value_list = ['SuperMajorityApprove', 'SuperMajorityAgainst', 'SimpleMajority']


class Null(ScaleType):

    def process(self):
        return None

    def process_encode(self, value):
        return ScaleBytes(bytearray())


class InherentOfflineReport(Null):
    pass


class LockPeriods(U8):
    pass


class Hash(H256):
    pass


class VoteIndex(U32):
    pass


class ProposalIndex(U32):
    pass


class Permill(U32):
    pass


class Perbill(U32):
    pass


class ApprovalFlag(U32):
    pass


class SetIndex(U32):
    pass


class AuthorityId(AccountId):
    pass


class ValidatorId(AccountId):
    pass


class AuthorityWeight(U64):
    pass


class StoredPendingChange(Struct):
    type_mapping = (
        ('scheduled_at', 'u32'),
        ('forced', 'u32'),
    )


class ReportIdOf(Hash):
    pass


class StorageHasher(Enum):

    value_list = ['Blake2_128', 'Blake2_256', 'Blake2_128Concat', 'Twox128', 'Twox256', 'Twox64Concat', 'Identity']

    def is_blake2_128(self):
        return self.index == 0

    def is_blake2_256(self):
        return self.index == 1

    def is_twoblake2_128_concat(self):
        return self.index == 2

    def is_twox128(self):
        return self.index == 3

    def is_twox256(self):
        return self.index == 4

    def is_twox64_concat(self):
        return self.index == 5

    def is_identity(self):
        return self.index == 6


class VoterInfo(Struct):
    type_string = 'VoterInfo<Balance>'

    type_mapping = (
        ('last_active', 'VoteIndex'),
        ('last_win', 'VoteIndex'),
        ('pot', 'Balance'),
        ('stake', 'Balance'),
    )


class Gas(U64):
    pass


class CodeHash(Hash):
    pass


class PrefabWasmModule(Struct):
    type_string = 'wasm::PrefabWasmModule'

    type_mapping = (
        ('scheduleVersion', 'Compact<u32>'),
        ('initial', 'Compact<u32>'),
        ('maximum', 'Compact<u32>'),
        ('_reserved', 'Option<Null>'),
        ('code', 'Bytes'),
    )


class OpaqueNetworkState(Struct):

    type_mapping = (
        ('peerId', 'OpaquePeerId'),
        ('externalAddresses', 'Vec<OpaqueMultiaddr>'),
    )


class OpaquePeerId(Bytes):
    pass


class OpaqueMultiaddr(Bytes):
    pass


class SessionKeysSubstrate(Struct):

    type_mapping = (
        ('grandpa', 'AccountId'),
        ('babe', 'AccountId'),
        ('im_online', 'AccountId'),
    )

class LegacyKeys(Struct):

    type_mapping = (
        ('grandpa', 'AccountId'),
        ('babe', 'AccountId'),
    )


class EdgewareKeys(Struct):
    type_mapping = (
        ('grandpa', 'AccountId'),
    )


class QueuedKeys(Struct):

    type_string = '(ValidatorId, Keys)'

    type_mapping = (
        ('validator', 'ValidatorId'),
        ('keys', 'Keys'),
    )


class LegacyQueuedKeys(Struct):

    type_string = '(ValidatorId, LegacyKeys)'

    type_mapping = (
        ('validator', 'ValidatorId'),
        ('keys', 'LegacyKeys'),
    )


class EdgewareQueuedKeys(Struct):

    type_string = '(ValidatorId, EdgewareKeys)'

    type_mapping = (
        ('validator', 'ValidatorId'),
        ('keys', 'EdgewareKeys'),
    )


class VecQueuedKeys(Vec):
    type_string = 'Vec<(ValidatorId, Keys)>'

    def process(self):
        element_count = self.process_type('Compact<u32>').value
        result = []
        for _ in range(0, element_count):
            element = self.process_type('QueuedKeys')
            self.elements.append(element)
            result.append(element.value)

        return result


class EthereumAddress(ScaleType):

    def process(self):
        value = self.get_next_bytes(20)
        return value.hex()

    def process_encode(self, value):
        if value[0:2] == '0x' and len(value) == 42:
            return ScaleBytes(value)
        else:
            raise ValueError('Value should start with "0x" and must be 20 bytes long')


class EcdsaSignature(ScaleType):

    def process(self):
        value = self.get_next_bytes(65)
        return value.hex()

    def process_encode(self, value):
        if value[0:2] == '0x' and len(value) == 132:
            return ScaleBytes(value)
        else:
            raise ValueError('Value should start with "0x" and must be 65 bytes long')


class BalanceLock(Struct):
    type_string = 'BalanceLock<Balance, BlockNumber>'

    type_mapping = (
        ('id', 'LockIdentifier'),
        ('amount', 'Balance'),
        ('until', 'U32'),
        ('reasons', 'WithdrawReasons'),
    )


class Bidder(Enum):
    type_string = 'Bidder<AccountId, ParaIdOf>'

    value_list = ['NewBidder', 'ParaId']


class BlockAttestations(Struct):

    type_mapping = (
        ('receipt', 'CandidateReceipt'),
        ('valid', 'Vec<AccountId>'),
        ('invalid', 'Vec<AccountId>'),
    )


class IncludedBlocks(Struct):

    type_mapping = (
        ('actualNumber', 'BlockNumber'),
        ('session', 'SessionIndex'),
        ('randomSeed', 'H256'),
        ('activeParachains', 'Vec<ParaId>'),
        ('paraBlocks', 'Vec<Hash>'),
    )

class HeadData(Bytes):
    pass


class Conviction(Enum):
    CONVICTION_MASK = 0b01111111
    DEFAULT_CONVICTION = 0b00000000

    value_list = ['None', 'Locked1x', 'Locked2x', 'Locked3x', 'Locked4x', 'Locked5x', 'Locked6x']


class EraRewards(Struct):

    type_mapping = (
        ('total', 'u32'),
        ('rewards', 'Vec<u32>'),
    )


class SlashJournalEntry(Struct):
    type_mapping = (
        ('who', 'AccountId'),
        ('amount', 'Balance'),
        ('ownSlash', 'Balance'),
    )


class UpwardMessage(Struct):
    type_mapping = (
        ('origin', 'ParachainDispatchOrigin'),
        ('data', 'Bytes'),
    )


class ParachainDispatchOrigin(Enum):
    value_list = ['Signed', 'Parachain']


class StoredState(Enum):
    value_list = ['Live', 'PendingPause', 'Paused', 'PendingResume']


class Votes(Struct):
    type_mapping = (
        ('index', 'ProposalIndex'),
        ('threshold', 'MemberCount'),
        ('ayes', 'Vec<AccountId>'),
        ('nays', 'Vec<AccountId>'),
    )

# Edgeware types
# TODO move to RuntimeConfiguration per network


class IdentityType(Bytes):
    pass


class VoteType(Enum):

    type_string = 'voting::VoteType'

    value_list = ['Binary', 'MultiOption']


class VoteOutcome(ScaleType):

    def process(self):
        return list(self.get_next_bytes(32))


class Identity(Bytes):
    pass


class ProposalTitle(Bytes):
    pass


class ProposalContents(Bytes):
    pass


class ProposalStage(Enum):
    value_list = ['PreVoting', 'Voting', 'Completed']


class ProposalCategory(Enum):
    value_list = ['Signaling']


class VoteStage(Enum):
    value_list = ['PreVoting', 'Commit', 'Voting', 'Completed']


class TallyType(Enum):

    type_string = 'voting::TallyType'

    value_list = ['OnePerson', 'OneCoin']


class Attestation(Bytes):
    pass


# Joystream types
# TODO move to RuntimeConfiguration per network

class ContentId(H256):
    pass


class MemberId(U64):
    pass


class PaidTermId(U64):
    pass


class SubscriptionId(U64):
    pass


class SchemaId(U64):
    pass


class DownloadSessionId(U64):
    pass


class UserInfo(Struct):

    type_mapping = (
        ('handle', 'Option<Vec<u8>>'),
        ('avatar_uri', 'Option<Vec<u8>>'),
        ('about', 'Option<Vec<u8>>')
    )


class Role(Enum):

    value_list = ['Storage']


class ContentVisibility(Enum):
    value_list = ['Draft', 'Public']


class ContentMetadata(Struct):
    type_mapping = (
        ('owner', 'AccountId'),
        ('added_at', 'BlockAndTime'),
        ('children_ids', 'Vec<ContentId>'),
        ('visibility', 'ContentVisibility'),
        ('schema', 'SchemaId'),
        ('json', 'Vec<u8>'),

    )


class ContentMetadataUpdate(Struct):
    type_mapping = (
        ('children_ids', 'Option<Vec<ContentId>>'),
        ('visibility', 'Option<ContentVisibility>'),
        ('schema', 'Option<SchemaId>'),
        ('json', 'Option<Vec<u8>>')
    )


class LiaisonJudgement(Enum):
    value_list = ['Pending', 'Accepted', 'Rejected']


class BlockAndTime(Struct):
    type_mapping = (
        ('block', 'BlockNumber'),
        ('time', 'Moment')
    )


class DataObjectTypeId(U64):
    type_string = "<T as DOTRTrait>::DataObjectTypeId"


class DataObject(Struct):
    type_mapping = (
        ('owner', 'AccountId'),
        ('added_at', 'BlockAndTime'),
        ('type_id', 'DataObjectTypeId'),
        ('size', 'u64'),
        ('liaison', 'AccountId'),
        ('liaison_judgement', 'LiaisonJudgement'),
        ('ipfs_content_id', 'Bytes'),
    )


class DataObjectStorageRelationshipId(U64):
    pass


class IPNSIdentity(Bytes):
    pass


class AccountInfo(Struct):
    type_string = 'AccountInfo<BlockNumber>'

    type_mapping = (
        ('identity', 'IPNSIdentity'),
        ('expires_at', 'BlockNumber'),
    )


class DownloadState(Enum):
    value_list = ['Started', 'Ended']


class DownloadSession(Struct):

    type_mapping = (
        ('content_id', 'ContentId'),
        ('consumer', 'AccountId'),
        ('distributor', 'AccountId'),
        ('initiated_at_block', 'BlockNumber'),
        ('initiated_at_time', 'BlockNumber'),
        ('state', 'DownloadState'),
        ('transmitted_bytes', 'u64'),
    )


class Url(Bytes):
    pass


class EntryMethod(Enum):
    value_list = ['Paid', 'Screening']


class Profile(Struct):
    type_mapping = (
        ('id', 'MemberId'),
        ('handle', 'Bytes'),
        ('avatar_uri', 'Bytes'),
        ('about', 'Bytes'),
        ('registered_at_block', 'BlockNumber'),
        ('registered_at_time', 'Moment'),
        ('entry', 'EntryMethod'),
        ('suspended', 'bool'),
        ('subscription', 'Option<SubscriptionId>'),
    )


class PaidMembershipTerms(Struct):
    type_mapping = (
        ('id', 'PaidTermId'),
        ('fee', 'BalanceOf'),
        ('text', 'Bytes'),
    )


class ThreadId(U64):
    pass


class InputValidationLengthConstraint(Struct):
    type_mapping = (
        ('min', 'u16'),
        ('max_min_diff', 'u16'),
    )


class BlockchainTimestamp(Struct):
    type_string = 'BlockchainTimestamp<BlockNumber, Moment>'

    type_mapping = (
        ('block', 'BlockNumber'),
        ('time', 'Moment'),
    )


class ModerationAction(Struct):
    type_mapping = (
        ('moderated_at', 'BlockchainTimestamp<BlockNumber, Moment>'),
        ('moderator_id', 'AccountId'),
        ('rationale', 'Vec<u8>'),
    )


class PostId(U64):
    pass


class PostTextChange(Struct):
    type_string = 'PostTextChange<BlockNumber, Moment>'

    type_mapping = (
        ('expired_at', 'BlockchainTimestamp<BlockNumber, Moment>'),
        ('text', 'Vec<u8>'),
    )


class Post(Struct):
    type_string = 'Post<BlockNumber, Moment, AccountId>'

    type_mapping = (
        ('id', 'PostId'),
        ('thread_id', 'ThreadId'),
        ('nr_in_thread', 'u32'),
        ('current_text', 'Vec<u8>'),
        ('moderation', 'Option<ModerationAction<BlockNumber, Moment, AccountId>>'),
        ('text_change_history', 'Vec<PostTextChange<BlockNumber, Moment>>'),
        ('created_at', 'BlockchainTimestamp<BlockNumber, Moment>'),
        ('author_id', 'AccountId'),

    )


class Thread(Struct):
    type_string = 'Thread<BlockNumber, Moment, AccountId>'

    type_mapping = (
        ('id', 'ThreadId'),
        ('title', 'Vec<u8>'),
        ('category_id', 'CategoryId'),
        ('nr_in_category', 'u32'),
        ('moderation', 'Option<ModerationAction<BlockNumber, Moment, AccountId>>'),
        ('num_unmoderated_posts', 'u32'),
        ('num_moderated_posts', 'u32'),
        ('author_id', 'AccountId'),
        ('created_at', 'BlockchainTimestamp<BlockNumber, Moment>'),
        ('author_id', 'AccountId'),
    )


class CategoryId(U64):
    pass


class ChildPositionInParentCategory(Struct):

    type_mapping = (
        ('parent_id', 'CategoryId'),
        ('child_nr_in_parent_category', 'u32'),
    )


class Category(Struct):
    type_string = 'Category<BlockNumber, Moment, AccountId>'

    type_mapping = (
        ('id', 'CategoryId'),
        ('title', 'Vec<u8>'),
        ('description', 'Vec<u8>'),
        ('created_at', 'BlockchainTimestamp<BlockNumber, Moment>'),
        ('deleted', 'bool'),
        ('archived', 'bool'),
        ('num_direct_subcategories', 'u32'),
        ('num_direct_unmoderated_threads', 'u32'),
        ('num_direct_moderated_threads', 'u32'),
        ('position_in_parent_category', 'Option<ChildPositionInParentCategory>'),
        ('moderator_id', 'AccountId'),
    )


class ProposalStatus(Enum):
    value_list = ['Active', 'Cancelled', 'Expired', 'Approved', 'Rejected', 'Slashed']


class VoteKind(Enum):
    value_list = ['Abstain', 'Approve', 'Reject', 'Slash']


class RuntimeUpgradeProposal(Struct):
    type_string = 'RuntimeUpgradeProposal<AccountId, Balance, BlockNumber, Hash>'

    type_mapping = (
        ('id', 'u32'),
        ('proposer', 'AccountId'),
        ('stake', 'Balance'),
        ('name', 'Vec<u8>'),
        ('description', 'Vec<u8>'),
        ('wasm_hash', 'Hash'),
        ('proposed_at', 'BlockNumber'),
        ('status', 'ProposalStatus'),
    )


class TallyResult(Struct):
    type_string = 'TallyResult<BlockNumber>'

    type_mapping = (
        ('proposal_id', 'u32'),
        ('abstentions', 'u32'),
        ('approvals', 'u32'),
        ('rejections', 'u32'),
        ('slashes', 'u32'),
        ('status', 'ProposalStatus'),
        ('finalized_at', 'BlockNumber'),
    )


class Call(ScaleType):

    type_string = "Box<Call>"

    def __init__(self, data, **kwargs):
        self.call_index = None
        self.call_function = None
        self.call_args = []
        self.call_module = None

        super().__init__(data, **kwargs)

    def process(self):

        self.call_index = self.get_next_bytes(2).hex()

        self.call_module, self.call_function = self.metadata.call_index[self.call_index]

        for arg in self.call_function.args:
            arg_type_obj = self.process_type(arg.type, metadata=self.metadata)

            self.call_args.append({
                'name': arg.name,
                'type': arg.type,
                'value': arg_type_obj.serialize(),
                'valueRaw': arg_type_obj.raw_value
            })

        return {
            'call_index': self.call_index,
            'call_function': self.call_function.name,
            'call_module': self.call_module.name,
            'call_args': self.call_args
        }

    def process_encode(self, value):
        # Check requirements
        if 'call_index' in value:
            self.call_index = value['call_index']

        elif 'call_module' in value and 'call_function' in value:
            # Look up call module from metadata
            for call_index, (call_module, call_function) in self.metadata.call_index.items():

                if call_module.name == value['call_module'] and call_function.name == value['call_function']:
                    self.call_index = call_index
                    self.call_module = call_module
                    self.call_function = call_function
                    break

            if not self.call_index:
                raise ValueError('Specified call module and function not found in metadata')

        elif not self.call_module or not self.call_function:
            raise ValueError('No call module and function specified')

        data = ScaleBytes(bytearray.fromhex(self.call_index))

        # Encode call params
        if len(self.call_function.args) > 0:
            for arg in self.call_function.args:
                if arg.name not in value['call_args']:
                    raise ValueError('Parameter \'{}\' not specified'.format(arg.name))
                else:
                    param_value = value['call_args'][arg.name]

                    arg_obj = self.get_decoder_class(arg.type, metadata=self.metadata)
                    data += arg_obj.encode(param_value)
        return data


class MultiAccountId(AccountId):

    @classmethod
    def create_from_account_list(cls, accounts, threshold):
        from scalecodec.utils.ss58 import ss58_decode

        account_ids = []
        for account in accounts:
            if account[0:2] != '0x':
                account = '0x{}'.format(ss58_decode(account))
            account_ids.append(account)

        account_list_cls = cls.get_decoder_class('Vec<AccountId>')
        account_list_data = account_list_cls.encode(sorted(account_ids))
        threshold_data = cls.get_decoder_class("u16").encode(threshold)

        multi_account_id = "0x{}".format(blake2b(
            b"modlpy/utilisuba" + bytes(account_list_data.data) + bytes(threshold_data.data), digest_size=32
        ).digest().hex())

        multi_account_obj = cls()
        multi_account_obj.encode(multi_account_id)

        return multi_account_obj