use crate::odra_vm_contract_env::OdraVmContractEnv;
use crate::OdraVm;
use odra_core::casper_types::{bytesrepr::Bytes, PublicKey, RuntimeArgs, U512};
use odra_core::entry_point_callback::EntryPointsCaller;
use odra_core::prelude::*;
use odra_core::{
    host::{HostContext, HostEnv},
    CallDef, ContractContext, ContractEnv
};
use odra_core::{EventError, GasReport, VmError};

/// HostContext utilizing the Odra in-memory virtual machine.
pub struct OdraVmHost {
    vm: Rc<OdraVm>,
    contract_env: Rc<ContractEnv>
}

impl HostContext for OdraVmHost {
    fn set_caller(&self, caller: Address) {
        self.vm.set_caller(caller)
    }

    fn set_gas(&self, gas: u64) {
        // Set gas does nothing in this context
    }

    fn caller(&self) -> Address {
        *self.vm.callstack_tip().address()
    }

    fn get_account(&self, index: usize) -> Address {
        self.vm.get_account(index)
    }

    fn get_validator(&self, index: usize) -> PublicKey {
        self.vm.get_validator(index)
    }

    fn remove_validator(&self, index: usize) {
        self.vm.remove_validator(index);
    }

    fn balance_of(&self, address: &Address) -> U512 {
        self.vm.balance_of(address)
    }

    fn advance_block_time(&self, time_diff: u64) {
        self.vm.advance_block_time_by(time_diff)
    }

    fn advance_with_auctions(&self, time_diff: u64) {
        self.vm.advance_with_auctions(time_diff)
    }

    fn auction_delay(&self) -> u64 {
        self.vm.auction_delay()
    }

    fn unbonding_delay(&self) -> u64 {
        self.vm.unbonding_delay()
    }

    fn delegated_amount(&self, delegator: Address, validator: PublicKey) -> U512 {
        self.vm.delegated_amount(delegator, validator)
    }

    fn block_time(&self) -> u64 {
        self.vm.get_block_time()
    }

    fn get_event(&self, contract_address: &Address, index: u32) -> Result<Bytes, EventError> {
        self.vm.get_event(contract_address, index)
    }

    fn get_native_event(
        &self,
        contract_address: &Address,
        index: u32
    ) -> Result<Bytes, EventError> {
        self.vm.get_native_event(contract_address, index)
    }

    fn get_events_count(&self, contract_address: &Address) -> Result<u32, EventError> {
        self.vm.get_events_count(contract_address)
    }

    fn get_native_events_count(&self, contract_address: &Address) -> Result<u32, EventError> {
        self.vm.get_native_events_count(contract_address)
    }

    fn call_contract(
        &self,
        address: &Address,
        call_def: CallDef,
        _use_proxy: bool
    ) -> OdraResult<Bytes> {
        crate::panic_hook::set_odra_panic_hook();

        let mut opt_result: Option<Bytes> = None;
        let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
            opt_result = Some(self.vm.call_contract(*address, call_def));
        }));

        match opt_result {
            Some(result) => Ok(result),
            None => {
                eprintln!("↳ Stack trace:\n{}", self.vm.read_stack_record());
                let error = self.vm.error();
                Err(error.unwrap_or(OdraError::VmError(VmError::Panic)))
            }
        }
    }

    fn new_contract(
        &self,
        name: &str,
        init_args: RuntimeArgs,
        entry_points_caller: EntryPointsCaller
    ) -> OdraResult<Address> {
        let address = self
            .vm
            .new_contract(name, init_args.clone(), entry_points_caller.clone());

        if entry_points_caller
            .entry_points()
            .iter()
            .any(|ep| ep.name == "init")
        {
            self.call_contract(
                &address,
                CallDef::new(String::from("init"), true, init_args),
                false
            )?;
            self.vm.post_install(address);
        }

        Ok(address)
    }

    fn upgrade_contract(
        &self,
        name: &str,
        contract_to_upgrade: Address,
        upgrade_args: RuntimeArgs,
        entry_points_caller: EntryPointsCaller
    ) -> OdraResult<Address> {
        let address = self.vm.upgrade_contract(
            name,
            contract_to_upgrade,
            upgrade_args.clone(),
            entry_points_caller.clone()
        );

        if entry_points_caller
            .entry_points()
            .iter()
            .any(|ep| ep.name == "upgrade")
        {
            self.call_contract(
                &address,
                CallDef::new(String::from("upgrade"), true, upgrade_args),
                false
            )?;
            self.vm.post_install(address);
        }

        Ok(address)
    }

    fn register_contract(
        &self,
        address: Address,
        contract_name: String,
        entry_points_caller: EntryPointsCaller
    ) {
        panic!("register_contract is not supported for OdraVM");
    }

    fn contract_env(&self) -> ContractEnv {
        (*self.contract_env).clone()
    }

    fn gas_report(&self) -> GasReport {
        // For OdraVM there is no gas, so nothing to report.c
        GasReport::default()
    }

    fn last_call_gas_cost(&self) -> u64 {
        // For OdraVM there is no gas, so nothing to return.
        0
    }

    fn sign_message(&self, message: &Bytes, address: &Address) -> Bytes {
        self.vm.sign_message(message, address)
    }

    fn public_key(&self, address: &Address) -> PublicKey {
        self.vm.public_key(address)
    }

    fn transfer(&self, to: Address, amount: U512) -> OdraResult<()> {
        let caller = self.caller();
        self.vm.checked_transfer_tokens(&caller, &to, &amount)
    }
}

impl OdraVmHost {
    /// Creates a new `OdraVmHost` instance.
    pub fn new(vm: Rc<OdraVm>) -> Rc<Self> {
        let contract_env = Rc::new(ContractEnv::new(OdraVmContractEnv::new(vm.clone())));
        Rc::new(Self { vm, contract_env })
    }
}
