from typing import Any, List, Union

from boa3.builtin import NeoMetadata, metadata, public
from boa3.builtin.contract import Nep17TransferEvent
from boa3.builtin.interop.blockchain import get_contract
from boa3.builtin.interop.contract import GAS, NEO, call_contract
from boa3.builtin.interop.runtime import calling_script_hash, check_witness
from boa3.builtin.interop.storage import delete, get, put
from boa3.builtin.type import UInt160


# -------------------------------------------
# METADATA
# -------------------------------------------


@metadata
def manifest_metadata() -> NeoMetadata:
    """
    Defines this smart contract's metadata information
    """
    meta = NeoMetadata()
    meta.author = "Mirella Medeiros, Ricardo Prado and Lucas Uezu. COZ in partnership with Simpli"
    meta.description = "ICO Example"
    meta.email = "contact@coz.io"
    return meta


# -------------------------------------------
# Storage Key Prefixes
# -------------------------------------------


KYC_WHITELIST_PREFIX = b'KYCWhitelistApproved'
TOKEN_TOTAL_SUPPLY_PREFIX = b'TokenTotalSupply'
TRANSFER_ALLOWANCE_PREFIX = b'TransferAllowancePrefix_'


# -------------------------------------------
# TOKEN SETTINGS
# -------------------------------------------


# Script hash of the contract owner
TOKEN_OWNER = UInt160()

# Symbol of the Token
TOKEN_SYMBOL = 'ICO'

# Number of decimal places
TOKEN_DECIMALS = 8

# Initial Supply of tokens in the system
TOKEN_INITIAL_SUPPLY = 10_000_000 * 100_000_000  # 10m total supply * 10^8 (decimals)


# -------------------------------------------
# Events
# -------------------------------------------


on_transfer = Nep17TransferEvent


# -------------------------------------------
# Methods
# -------------------------------------------


@public
def verify() -> bool:
    """
    When this contract address is included in the transaction signature,
    this method will be triggered as a VerificationTrigger to verify that the signature is correct.
    For example, this method needs to be called when withdrawing token from the contract.

    :return: whether the transaction signature is correct
    """
    return is_administrator()


def is_administrator() -> bool:
    """
    Validates if the invoker has administrative rights

    :return: whether the contract's invoker is an administrator
    """
    return check_witness(TOKEN_OWNER)


def is_valid_address(address: UInt160) -> bool:
    """
    Validates if the address passed through the kyc.

    :return: whether the given address is validated by kyc
    """
    return get(KYC_WHITELIST_PREFIX + address).to_int() > 0


@public
def deploy() -> bool:
    """
    Initializes the storage when the smart contract is deployed.

    :return: whether the deploy was successful. This method must return True only during the smart contract's deploy.
    """
    if not check_witness(TOKEN_OWNER):
        return False

    if get(TOKEN_TOTAL_SUPPLY_PREFIX).to_int() > 0:
        return False

    put(TOKEN_TOTAL_SUPPLY_PREFIX, TOKEN_INITIAL_SUPPLY)
    put(TOKEN_OWNER, TOKEN_INITIAL_SUPPLY)

    on_transfer(None, TOKEN_OWNER, TOKEN_INITIAL_SUPPLY)
    return True


@public
def mint(amount: int) -> bool:
    """
    Mints new tokens

    :param amount: the amount of gas to be refunded
    :type amount: int
    :return: whether the refund was successful
    """
    assert amount >= 0
    if not is_administrator():
        return False

    if amount > 0:
        current_total_supply = totalSupply()
        owner_balance = balanceOf(TOKEN_OWNER)

        put(TOKEN_TOTAL_SUPPLY_PREFIX, current_total_supply + amount)
        put(TOKEN_OWNER, owner_balance + amount)

    on_transfer(None, TOKEN_OWNER, amount)
    post_transfer(None, TOKEN_OWNER, amount, None)
    return True


@public
def refund(address: UInt160, neo_amount: int, gas_amount: int) -> bool:
    """
    Refunds an address with given Neo and Gas

    :param address: the address that have the tokens
    :type address: UInt160
    :param neo_amount: the amount of neo to be refunded
    :type neo_amount: int
    :param gas_amount: the amount of gas to be refunded
    :type gas_amount: int
    :return: whether the refund was successful
    """
    assert len(address) == 20
    assert neo_amount > 0 or gas_amount > 0

    if not is_administrator():
        return False

    if neo_amount > 0:
        result = call_contract(NEO, 'transfer', [calling_script_hash, address, neo_amount, None])
        if result != True:
            # due to a current limitation in the neo3-boa, changing the condition to `not result`
            # will result in a compiler error
            return False

    if gas_amount > 0:
        result = call_contract(GAS, 'transfer', [calling_script_hash, address, gas_amount, None])
        if result != True:
            # due to a current limitation in the neo3-boa, changing the condition to `not result`
            # will result in a compiler error
            return False

    return True


# -------------------------------------------
# Public methods from NEP5.1
# -------------------------------------------


@public
def symbol() -> str:
    """
    Gets the symbols of the token.

    This symbol should be short (3-8 characters is recommended), with no whitespace characters or new-lines and should
    be limited to the uppercase latin alphabet (i.e. the 26 letters used in English).
    This method must always return the same value every time it is invoked.

    :return: a short string symbol of the token managed in this contract.
    """
    return TOKEN_SYMBOL


@public
def decimals() -> int:
    """
    Gets the amount of decimals used by the token.

    E.g. 8, means to divide the token amount by 100,000,000 (10 ^ 8) to get its user representation.
    This method must always return the same value every time it is invoked.

    :return: the number of decimals used by the token.
    """
    return TOKEN_DECIMALS


@public
def totalSupply() -> int:
    """
    Gets the total token supply deployed in the system.

    This number mustn't be in its user representation. E.g. if the total supply is 10,000,000 tokens, this method
    must return 10,000,000 * 10 ^ decimals.

    :return: the total token supply deployed in the system.
    """
    return get(TOKEN_TOTAL_SUPPLY_PREFIX).to_int()


@public
def balanceOf(account: UInt160) -> int:
    """
    Get the current balance of an address

    The parameter account should be a 20-byte address.

    :param account: the account address to retrieve the balance for
    :type account: UInt160

    :return: the token balance of the `account`
    :raise AssertionError: raised if `account` length is not 20.
    """
    assert len(account) == 20
    return get(account).to_int()


@public
def transfer(from_address: UInt160, to_address: UInt160, amount: int, data: Any) -> bool:
    """
    Transfers a specified amount of NEP17 tokens from one account to another

    If the method succeeds, it must fire the `transfer` event and must return true, even if the amount is 0,
    or from and to are the same address.

    :param from_address: the address to transfer from
    :type from_address: UInt160
    :param to_address: the address to transfer to
    :type to_address: UInt160
    :param amount: the amount of NEP17 tokens to transfer
    :type amount: int
    :param data: whatever data is pertinent to the onPayment method
    :type data: Any

    :return: whether the transfer was successful
    :raise AssertionError: raised if `from_address` or `to_address` length is not 20 or if `amount` if less than zero.
    """
    # the parameters from and to should be 20-byte addresses. If not, this method should throw an exception.
    assert len(from_address) == 20 and len(to_address) == 20
    # the parameter amount must be greater than or equal to 0. If not, this method should throw an exception.
    assert amount >= 0

    # The function MUST return false if the from account balance does not have enough tokens to spend.
    from_balance = get(from_address).to_int()
    if from_balance < amount:
        return False

    # The function should check whether the from address equals the caller contract hash.
    # If so, the transfer should be processed;
    # If not, the function should use the check_witness to verify the transfer.
    if from_address != calling_script_hash:
        if not check_witness(from_address):
            return False

    # skip balance changes if transferring to yourself or transferring 0 cryptocurrency
    if from_address != to_address and amount != 0:
        if from_balance == amount:
            delete(from_address)
        else:
            put(from_address, from_balance - amount)

        to_balance = get(to_address).to_int()
        put(to_address, to_balance + amount)

    # if the method succeeds, it must fire the transfer event
    on_transfer(from_address, to_address, amount)
    # if the to_address is a smart contract, it must call the contracts onPayment
    post_transfer(from_address, to_address, amount, data)
    # and then it must return true
    return True


def post_transfer(from_address: Union[UInt160, None], to_address: Union[UInt160, None], amount: int, data: Any):
    """
    Checks if the one receiving NEP17 tokens is a smart contract and if it's one the onPayment method will be called

    :param from_address: the address of the sender
    :type from_address: UInt160
    :param to_address: the address of the receiver
    :type to_address: UInt160
    :param amount: the amount of cryptocurrency that is being sent
    :type amount: int
    :param data: any pertinent data that might validate the transaction
    :type data: Any
    """
    if not isinstance(to_address, None):    # TODO: change to 'is not None' when `is` semantic is implemented
        contract = get_contract(to_address)
        if not isinstance(contract, None):      # TODO: change to 'is not None' when `is` semantic is implemented
            call_contract(to_address, 'onPayment', [from_address, amount, data])


@public
def allowance(from_address: UInt160, to_address: UInt160) -> int:
    """
    Returns the amount of tokens that the to account can transfer from the from account.

    :param from_address: the address that have the tokens
    :type from_address: UInt160
    :param to_address: the address that is authorized to use the tokens
    :type to_address: UInt160

    :return: the amount of tokens that the `to` account can transfer from the `from` account
    :raise AssertionError: raised if `from_address` or `to_address` length is not 20.
    """
    # the parameters from and to should be 20-byte addresses. If not, this method should throw an exception.
    assert len(from_address) == 20 and len(to_address) == 20
    return get(TRANSFER_ALLOWANCE_PREFIX + from_address + to_address).to_int()


@public
def transferFrom(originator: UInt160, from_address: UInt160, to_address: UInt160, amount: int, data: Any) -> bool:
    """
    Transfers an amount from the `from` account to the `to` account if the `originator` has been approved to transfer
    the requested amount.

    :param originator: the address where the actual token is
    :type originator: UInt160
    :param from_address: the address to transfer from with originator's approval
    :type from_address: UInt160
    :param to_address: the address to transfer to
    :type to_address: UInt160
    :param amount: the amount of NEP17 tokens to transfer
    :type amount: int
    :param data: any pertinent data that might validate the transaction
    :type data: Any

    :return: whether the transfer was successful
    :raise AssertionError: raised if `from_address` or `to_address` length is not 20 or if `amount` if less than zero.
    """
    # the parameters from and to should be 20-byte addresses. If not, this method should throw an exception.
    assert len(originator) == 20 and len(from_address) == 20 and len(to_address) == 20
    # the parameter amount must be greater than or equal to 0. If not, this method should throw an exception.
    assert amount >= 0

    # The function should check whether the from address equals the caller contract hash.
    # If so, the transfer should be processed;
    # If not, the function should use the check_witness to verify the transfer.
    if from_address != calling_script_hash:
        if not check_witness(from_address):
            return False

    approved_transfer_amount = allowance(originator, from_address)
    if approved_transfer_amount < amount:
        return False

    originator_balance = balanceOf(originator)
    if originator_balance < amount:
        return False

    # update allowance between originator and from
    if approved_transfer_amount == amount:
        delete(TRANSFER_ALLOWANCE_PREFIX + originator + from_address)
    else:
        put(TRANSFER_ALLOWANCE_PREFIX + originator + from_address, approved_transfer_amount - amount)

    # skip balance changes if transferring to yourself or transferring 0 cryptocurrency
    if amount != 0 and from_address != to_address:
        # update originator's balance
        if originator_balance == amount:
            delete(originator)
        else:
            put(originator, originator_balance - amount)

        # updates to's balance
        to_balance = get(to_address).to_int()
        put(to_address, to_balance + amount)

    # if the method succeeds, it must fire the transfer event
    on_transfer(from_address, to_address, amount)
    # if the to_address is a smart contract, it must call the contracts onPayment
    post_transfer(from_address, to_address, amount, data)
    # and then it must return true
    return True


@public
def approve(originator: UInt160, to_address: UInt160, amount: int) -> bool:
    """
    Approves the to account to transfer amount tokens from the originator account.

    :param originator: the address that have the tokens
    :type originator: UInt160
    :param to_address: the address that is authorized to use the tokens
    :type to_address: UInt160
    :param amount: the amount of NEP17 tokens to transfer
    :type amount: int

    :return: whether the approval was successful
    :raise AssertionError: raised if `originator` or `to_address` length is not 20 or if `amount` if less than zero.
    """
    assert len(originator) == 20 and len(to_address) == 20
    assert amount >= 0

    if not check_witness(originator):
        return False

    if originator == to_address:
        return False

    if not is_valid_address(originator) or not is_valid_address(to_address):
        # one of the address doesn't passed the kyc yet
        return False

    if balanceOf(originator) < amount:
        return False

    put(TRANSFER_ALLOWANCE_PREFIX + originator + to_address, amount)
    return True


# -------------------------------------------
# Public methods from KYC
# -------------------------------------------


@public
def kyc_register(addresses: List[UInt160]) -> int:
    """
    Includes the given addresses to the kyc whitelist

    :param addresses: a list with the addresses to be included
    :return: the number of included addresses
    """
    included_addresses = 0
    if is_administrator():
        for address in addresses:
            if len(address) == 20:
                kyc_key = KYC_WHITELIST_PREFIX + address
                put(kyc_key, True)
                included_addresses += 1

    return included_addresses


@public
def kyc_remove(addresses: List[UInt160]) -> int:
    """
    Removes the given addresses from the kyc whitelist

    :param addresses: a list with the addresses to be removed
    :return: the number of removed addresses
    """
    removed_addresses = 0
    if is_administrator():
        for address in addresses:
            if len(address) == 20:
                kyc_key = KYC_WHITELIST_PREFIX + address
                delete(kyc_key)
                removed_addresses += 1

    return removed_addresses

[((45, 14, 45, 23), 'boa3.builtin.type.UInt160', 'UInt160', ({}, {}), '()', False, 'from boa3.builtin.type import UInt160\n'), ((22, 11, 22, 24), 'boa3.builtin.NeoMetadata', 'NeoMetadata', ({}, {}), '()', False, 'from boa3.builtin import NeoMetadata, metadata, public\n'), ((88, 11, 88, 37), 'boa3.builtin.interop.runtime.check_witness', 'check_witness', ({(88, 25, 88, 36): 'TOKEN_OWNER'}, {}), '(TOKEN_OWNER)', False, 'from boa3.builtin.interop.runtime import calling_script_hash, check_witness\n'), ((113, 4, 113, 56), 'boa3.builtin.interop.storage.put', 'put', ({(113, 8, 113, 33): 'TOKEN_TOTAL_SUPPLY_PREFIX', (113, 35, 113, 55): 'TOKEN_INITIAL_SUPPLY'}, {}), '(TOKEN_TOTAL_SUPPLY_PREFIX, TOKEN_INITIAL_SUPPLY)', False, 'from boa3.builtin.interop.storage import delete, get, put\n'), ((114, 4, 114, 42), 'boa3.builtin.interop.storage.put', 'put', ({(114, 8, 114, 19): 'TOKEN_OWNER', (114, 21, 114, 41): 'TOKEN_INITIAL_SUPPLY'}, {}), '(TOKEN_OWNER, TOKEN_INITIAL_SUPPLY)', False, 'from boa3.builtin.interop.storage import delete, get, put\n'), ((432, 4, 432, 68), 'boa3.builtin.interop.storage.put', 'put', ({(432, 8, 432, 59): '(TRANSFER_ALLOWANCE_PREFIX + originator + to_address)', (432, 61, 432, 67): 'amount'}, {}), '(TRANSFER_ALLOWANCE_PREFIX + originator + to_address, amount)', False, 'from boa3.builtin.interop.storage import delete, get, put\n'), ((107, 11, 107, 37), 'boa3.builtin.interop.runtime.check_witness', 'check_witness', ({(107, 25, 107, 36): 'TOKEN_OWNER'}, {}), '(TOKEN_OWNER)', False, 'from boa3.builtin.interop.runtime import calling_script_hash, check_witness\n'), ((137, 8, 137, 69), 'boa3.builtin.interop.storage.put', 'put', ({(137, 12, 137, 37): 'TOKEN_TOTAL_SUPPLY_PREFIX', (137, 39, 137, 68): '(current_total_supply + amount)'}, {}), '(TOKEN_TOTAL_SUPPLY_PREFIX, current_total_supply + amount)', False, 'from boa3.builtin.interop.storage import delete, get, put\n'), ((138, 8, 138, 48), 'boa3.builtin.interop.storage.put', 'put', ({(138, 12, 138, 23): 'TOKEN_OWNER', (138, 25, 138, 47): '(owner_balance + amount)'}, {}), '(TOKEN_OWNER, owner_balance + amount)', False, 'from boa3.builtin.interop.storage import delete, get, put\n'), ((165, 17, 165, 97), 'boa3.builtin.interop.contract.call_contract', 'call_contract', ({(165, 31, 165, 34): 'NEO', (165, 36, 165, 46): '"""transfer"""', (165, 48, 165, 96): '[calling_script_hash, address, neo_amount, None]'}, {}), "(NEO, 'transfer', [calling_script_hash, address, neo_amount, None]\n    )", False, 'from boa3.builtin.interop.contract import GAS, NEO, call_contract\n'), ((172, 17, 172, 97), 'boa3.builtin.interop.contract.call_contract', 'call_contract', ({(172, 31, 172, 34): 'GAS', (172, 36, 172, 46): '"""transfer"""', (172, 48, 172, 96): '[calling_script_hash, address, gas_amount, None]'}, {}), "(GAS, 'transfer', [calling_script_hash, address, gas_amount, None]\n    )", False, 'from boa3.builtin.interop.contract import GAS, NEO, call_contract\n'), ((288, 8, 288, 44), 'boa3.builtin.interop.storage.put', 'put', ({(288, 12, 288, 22): 'to_address', (288, 24, 288, 43): '(to_balance + amount)'}, {}), '(to_address, to_balance + amount)', False, 'from boa3.builtin.interop.storage import delete, get, put\n'), ((312, 19, 312, 43), 'boa3.builtin.interop.blockchain.get_contract', 'get_contract', ({(312, 32, 312, 42): 'to_address'}, {}), '(to_address)', False, 'from boa3.builtin.interop.blockchain import get_contract\n'), ((377, 8, 377, 69), 'boa3.builtin.interop.storage.delete', 'delete', ({(377, 15, 377, 68): '(TRANSFER_ALLOWANCE_PREFIX + originator + from_address)'}, {}), '(TRANSFER_ALLOWANCE_PREFIX + originator + from_address)', False, 'from boa3.builtin.interop.storage import delete, get, put\n'), ((379, 8, 379, 101), 'boa3.builtin.interop.storage.put', 'put', ({(379, 12, 379, 65): '(TRANSFER_ALLOWANCE_PREFIX + originator + from_address)', (379, 67, 379, 100): '(approved_transfer_amount - amount)'}, {}), '(TRANSFER_ALLOWANCE_PREFIX + originator + from_address, \n    approved_transfer_amount - amount)', False, 'from boa3.builtin.interop.storage import delete, get, put\n'), ((391, 8, 391, 44), 'boa3.builtin.interop.storage.put', 'put', ({(391, 12, 391, 22): 'to_address', (391, 24, 391, 43): '(to_balance + amount)'}, {}), '(to_address, to_balance + amount)', False, 'from boa3.builtin.interop.storage import delete, get, put\n'), ((419, 11, 419, 36), 'boa3.builtin.interop.runtime.check_witness', 'check_witness', ({(419, 25, 419, 35): 'originator'}, {}), '(originator)', False, 'from boa3.builtin.interop.runtime import calling_script_hash, check_witness\n'), ((223, 11, 223, 41), 'boa3.builtin.interop.storage.get', 'get', ({(223, 15, 223, 40): 'TOKEN_TOTAL_SUPPLY_PREFIX'}, {}), '(TOKEN_TOTAL_SUPPLY_PREFIX)', False, 'from boa3.builtin.interop.storage import delete, get, put\n'), ((240, 11, 240, 23), 'boa3.builtin.interop.storage.get', 'get', ({(240, 15, 240, 22): 'account'}, {}), '(account)', False, 'from boa3.builtin.interop.storage import delete, get, put\n'), ((269, 19, 269, 36), 'boa3.builtin.interop.storage.get', 'get', ({(269, 23, 269, 35): 'from_address'}, {}), '(from_address)', False, 'from boa3.builtin.interop.storage import delete, get, put\n'), ((277, 15, 277, 42), 'boa3.builtin.interop.runtime.check_witness', 'check_witness', ({(277, 29, 277, 41): 'from_address'}, {}), '(from_address)', False, 'from boa3.builtin.interop.runtime import calling_script_hash, check_witness\n'), ((283, 12, 283, 32), 'boa3.builtin.interop.storage.delete', 'delete', ({(283, 19, 283, 31): 'from_address'}, {}), '(from_address)', False, 'from boa3.builtin.interop.storage import delete, get, put\n'), ((285, 12, 285, 52), 'boa3.builtin.interop.storage.put', 'put', ({(285, 16, 285, 28): 'from_address', (285, 30, 285, 51): '(from_balance - amount)'}, {}), '(from_address, from_balance - amount)', False, 'from boa3.builtin.interop.storage import delete, get, put\n'), ((314, 12, 314, 80), 'boa3.builtin.interop.contract.call_contract', 'call_contract', ({(314, 26, 314, 36): 'to_address', (314, 38, 314, 49): '"""onPayment"""', (314, 51, 314, 79): '[from_address, amount, data]'}, {}), "(to_address, 'onPayment', [from_address, amount, data])", False, 'from boa3.builtin.interop.contract import GAS, NEO, call_contract\n'), ((332, 11, 332, 69), 'boa3.builtin.interop.storage.get', 'get', ({(332, 15, 332, 68): '(TRANSFER_ALLOWANCE_PREFIX + from_address + to_address)'}, {}), '(TRANSFER_ALLOWANCE_PREFIX + from_address + to_address)', False, 'from boa3.builtin.interop.storage import delete, get, put\n'), ((364, 15, 364, 42), 'boa3.builtin.interop.runtime.check_witness', 'check_witness', ({(364, 29, 364, 41): 'from_address'}, {}), '(from_address)', False, 'from boa3.builtin.interop.runtime import calling_script_hash, check_witness\n'), ((385, 12, 385, 30), 'boa3.builtin.interop.storage.delete', 'delete', ({(385, 19, 385, 29): 'originator'}, {}), '(originator)', False, 'from boa3.builtin.interop.storage import delete, get, put\n'), ((387, 12, 387, 56), 'boa3.builtin.interop.storage.put', 'put', ({(387, 16, 387, 26): 'originator', (387, 28, 387, 55): '(originator_balance - amount)'}, {}), '(originator, originator_balance - amount)', False, 'from boa3.builtin.interop.storage import delete, get, put\n'), ((97, 11, 97, 46), 'boa3.builtin.interop.storage.get', 'get', ({(97, 15, 97, 45): '(KYC_WHITELIST_PREFIX + address)'}, {}), '(KYC_WHITELIST_PREFIX + address)', False, 'from boa3.builtin.interop.storage import delete, get, put\n'), ((110, 7, 110, 37), 'boa3.builtin.interop.storage.get', 'get', ({(110, 11, 110, 36): 'TOKEN_TOTAL_SUPPLY_PREFIX'}, {}), '(TOKEN_TOTAL_SUPPLY_PREFIX)', False, 'from boa3.builtin.interop.storage import delete, get, put\n'), ((287, 21, 287, 36), 'boa3.builtin.interop.storage.get', 'get', ({(287, 25, 287, 35): 'to_address'}, {}), '(to_address)', False, 'from boa3.builtin.interop.storage import delete, get, put\n'), ((390, 21, 390, 36), 'boa3.builtin.interop.storage.get', 'get', ({(390, 25, 390, 35): 'to_address'}, {}), '(to_address)', False, 'from boa3.builtin.interop.storage import delete, get, put\n'), ((454, 16, 454, 34), 'boa3.builtin.interop.storage.put', 'put', ({(454, 20, 454, 27): 'kyc_key', (454, 29, 454, 33): '(True)'}, {}), '(kyc_key, True)', False, 'from boa3.builtin.interop.storage import delete, get, put\n'), ((473, 16, 473, 31), 'boa3.builtin.interop.storage.delete', 'delete', ({(473, 23, 473, 30): 'kyc_key'}, {}), '(kyc_key)', False, 'from boa3.builtin.interop.storage import delete, get, put\n')]