use crate::{
    api::{CallTypeApi, ErrorApiImpl, StorageMapperApi},
    contract_base::BlockchainWrapper,
    storage::StorageKey,
    storage_get, storage_get_len, storage_set,
    types::{
        system_proxy::DCDTSystemSCProxy, CallbackClosure, DCDTSystemSCAddress, DcdtLocalRole,
        DcdtTokenPayment, ManagedAddress, ManagedRef, ManagedVec, TokenIdentifier, Tx,
    },
};

use super::TokenMapperState;

pub(crate) const TOKEN_ID_ALREADY_SET_ERR_MSG: &[u8] = b"Token ID already set";
pub(crate) const PENDING_ERR_MSG: &[u8] = b"Issue was already called";
pub(crate) const MUST_SET_TOKEN_ID_ERR_MSG: &[u8] = b"Must issue or set token ID first";
pub(crate) const INVALID_TOKEN_ID_ERR_MSG: &[u8] = b"Invalid token ID";
pub(crate) const INVALID_PAYMENT_TOKEN_ERR_MSG: &[u8] = b"Invalid payment token";

pub trait StorageTokenWrapper<SA>
where
    SA: StorageMapperApi + CallTypeApi,
{
    fn get_storage_key(&self) -> ManagedRef<SA, StorageKey<SA>>;

    fn is_empty(&self) -> bool {
        storage_get_len(self.get_storage_key()) == 0
    }

    fn get_token_state(&self) -> TokenMapperState<SA>;

    fn get_token_id(&self) -> TokenIdentifier<SA>;

    fn get_token_id_ref(&self) -> &TokenIdentifier<SA>;

    fn set_token_id(&mut self, token_id: TokenIdentifier<SA>);

    fn set_if_empty(&mut self, token_id: TokenIdentifier<SA>) {
        if self.is_empty() {
            self.set_token_id(token_id);
        }
    }

    fn require_issued_or_set(&self) {
        if self.is_empty() {
            SA::error_api_impl().signal_error(MUST_SET_TOKEN_ID_ERR_MSG);
        }
    }

    fn require_same_token(&self, expected_token_id: &TokenIdentifier<SA>) {
        let actual_token_id = self.get_token_id_ref();
        if actual_token_id != expected_token_id {
            SA::error_api_impl().signal_error(INVALID_PAYMENT_TOKEN_ERR_MSG);
        }
    }

    fn require_all_same_token(&self, payments: &ManagedVec<SA, DcdtTokenPayment<SA>>) {
        let actual_token_id = self.get_token_id_ref();
        for p in payments {
            if actual_token_id != &p.token_identifier {
                SA::error_api_impl().signal_error(INVALID_PAYMENT_TOKEN_ERR_MSG);
            }
        }
    }

    fn set_local_roles(
        &self,
        roles: &[DcdtLocalRole],
        opt_callback: Option<CallbackClosure<SA>>,
    ) -> ! {
        let own_sc_address = Self::get_sc_address();
        self.set_local_roles_for_address(&own_sc_address, roles, opt_callback);
    }

    fn set_local_roles_for_address(
        &self,
        address: &ManagedAddress<SA>,
        roles: &[DcdtLocalRole],
        opt_callback: Option<CallbackClosure<SA>>,
    ) -> ! {
        self.require_issued_or_set();

        let token_id = self.get_token_id_ref();
        Tx::new_tx_from_sc()
            .to(DCDTSystemSCAddress)
            .typed(DCDTSystemSCProxy)
            .set_special_roles(address, token_id, roles[..].iter().cloned())
            .callback(opt_callback)
            .async_call_and_exit()
    }

    fn get_sc_address() -> ManagedAddress<SA> {
        let b_wrapper = BlockchainWrapper::new();
        b_wrapper.get_sc_address()
    }
}

pub(crate) fn store_token_id<
    SA: StorageMapperApi + CallTypeApi,
    Mapper: StorageTokenWrapper<SA>,
>(
    mapper: &Mapper,
    token_id: &TokenIdentifier<SA>,
) {
    if mapper.get_token_state().is_set() {
        SA::error_api_impl().signal_error(TOKEN_ID_ALREADY_SET_ERR_MSG);
    }
    if !token_id.is_valid_dcdt_identifier() {
        SA::error_api_impl().signal_error(INVALID_TOKEN_ID_ERR_MSG);
    }
    storage_set(
        mapper.get_storage_key(),
        &TokenMapperState::Token(token_id.clone()),
    );
}
pub(crate) fn check_not_set<SA: StorageMapperApi + CallTypeApi, Mapper: StorageTokenWrapper<SA>>(
    mapper: &Mapper,
) {
    let storage_value: TokenMapperState<SA> = storage_get(mapper.get_storage_key());
    match storage_value {
        TokenMapperState::NotSet => {},
        TokenMapperState::Pending => {
            SA::error_api_impl().signal_error(PENDING_ERR_MSG);
        },
        TokenMapperState::Token(_) => {
            SA::error_api_impl().signal_error(TOKEN_ID_ALREADY_SET_ERR_MSG);
        },
    }
}
