use crate::{
    config::TracingInspectorConfig,
    fourbyte::FourByteInspector,
    tracing_inspector::TracingInspector,
    types::{LogCallOrder, TxExecContext},
    utils::{to_alloy_address, to_alloy_h256, to_alloy_u256},
};
use alloy_primitives::{Address, Bytes, LogData};
use alloy_rpc_types_trace::geth::{
    CallConfig, GethDebugBuiltInTracerType, GethDebugBuiltInTracerType::*,
    GethDebugTracerType, GethDebugTracingOptions, GethTrace, NoopFrame,
    PreStateConfig,
};
use cfx_executor::{
    machine::Machine,
    observer::{
        CallTracer, CheckpointTracer, DrainTrace, InternalTransferTracer,
        OpcodeTracer, SetAuthTracer, StorageTracer,
    },
    stack::{FrameResult, FrameReturn},
};
use cfx_types::{Space, H160};
use cfx_vm_types::{ActionParams, CallType, Error, InterpreterInfo};
use revm::db::InMemoryDB;
use revm_interpreter::{Gas, InstructionResult, InterpreterResult};
use revm_primitives::State;
use std::sync::Arc;
pub struct GethTracer {
    inner: TracingInspector,
    fourbyte_inspector: FourByteInspector,
    tx_gas_limit: u64, gas_left: u64, depth: usize,
    opts: GethDebugTracingOptions,
    pub gas_stack: Vec<u64>,
}
impl GethTracer {
    pub fn new(
        tx_exec_context: TxExecContext, machine: Arc<Machine>,
        opts: GethDebugTracingOptions,
    ) -> Self {
        let TxExecContext { tx_gas_limit, .. } = tx_exec_context;
        let config = match opts.tracer {
            Some(GethDebugTracerType::BuiltInTracer(builtin_tracer)) => {
                match builtin_tracer {
                    FourByteTracer | NoopTracer | MuxTracer => {
                        TracingInspectorConfig::none()
                    }
                    CallTracer => {
                        let c = opts
                            .tracer_config
                            .clone()
                            .into_call_config()
                            .expect("should success");
                        TracingInspectorConfig::from_geth_call_config(&c)
                    }
                    PreStateTracer => {
                        let c = opts
                            .tracer_config
                            .clone()
                            .into_pre_state_config()
                            .expect("should success");
                        TracingInspectorConfig::from_geth_prestate_config(&c)
                    }
                }
            }
            Some(GethDebugTracerType::JsTracer(_)) => {
                TracingInspectorConfig::none()
            }
            None => TracingInspectorConfig::from_geth_config(&opts.config),
        };
        Self {
            inner: TracingInspector::new(config, machine, tx_exec_context),
            fourbyte_inspector: FourByteInspector::new(),
            tx_gas_limit,
            depth: 0,
            gas_left: tx_gas_limit,
            opts,
            gas_stack: Vec::new(),
        }
    }
    fn tracer_type(&self) -> Option<GethDebugBuiltInTracerType> {
        match self.opts.tracer.clone() {
            Some(t) => match t {
                GethDebugTracerType::BuiltInTracer(builtin_tracer) => {
                    Some(builtin_tracer)
                }
                GethDebugTracerType::JsTracer(_) => {
                    Some(NoopTracer)
                }
            },
            None => None,
        }
    }
    fn call_config(&self) -> Option<CallConfig> {
        self.opts.tracer_config.clone().into_call_config().ok()
    }
    fn prestate_config(&self) -> Option<PreStateConfig> {
        self.opts.tracer_config.clone().into_pre_state_config().ok()
    }
    pub fn is_fourbyte_tracer(&self) -> bool {
        self.tracer_type() == Some(FourByteTracer)
    }
    pub fn gas_used(&self) -> u64 { self.tx_gas_limit - self.gas_left }
    pub fn drain(self) -> GethTrace {
        let trace = match self.tracer_type() {
            Some(t) => match t {
                FourByteTracer => self.fourbyte_inspector.drain(),
                CallTracer => {
                    let gas_used = self.gas_used();
                    let opts = self.call_config().expect("should have config");
                    let frame = self
                        .inner
                        .into_geth_builder()
                        .geth_call_traces(opts, gas_used);
                    GethTrace::CallTracer(frame)
                }
                PreStateTracer => {
                    let opts =
                        self.prestate_config().expect("should have config");
                    let state = State::default();
                    let db = InMemoryDB::default();
                    let frame = self
                        .inner
                        .into_geth_builder()
                        .geth_prestate_traces(state, opts, db)
                        .unwrap();
                    GethTrace::PreStateTracer(frame)
                }
                NoopTracer | MuxTracer => {
                    GethTrace::NoopTracer(NoopFrame::default())
                }
            },
            None => {
                let gas_used = self.gas_used();
                let return_value = self
                    .inner
                    .last_call_return_data
                    .clone()
                    .unwrap_or_default();
                let opts = self.opts.config;
                let frame = self.inner.into_geth_builder().geth_traces(
                    gas_used,
                    return_value,
                    opts,
                );
                GethTrace::Default(frame)
            }
        };
        trace
    }
}
impl DrainTrace for GethTracer {
    fn drain_trace(self, map: &mut typemap::ShareDebugMap) {
        map.insert::<GethTraceKey>(self.drain());
    }
}
pub struct GethTraceKey;
impl typemap::Key for GethTraceKey {
    type Value = GethTrace;
}
impl CheckpointTracer for GethTracer {}
impl InternalTransferTracer for GethTracer {}
impl StorageTracer for GethTracer {}
impl SetAuthTracer for GethTracer {}
impl CallTracer for GethTracer {
    fn record_call(&mut self, params: &ActionParams) {
        if self.is_fourbyte_tracer() {
            self.fourbyte_inspector.record_call(params);
            return;
        }
        let gas_limit = params.gas.as_u64();
        self.gas_stack.push(gas_limit);
        let (from, to) = match params.call_type {
            CallType::DelegateCall | CallType::CallCode => {
                (params.address, params.code_address)
            }
            _ => (params.sender, params.address),
        };
        let value = if matches!(params.call_type, CallType::DelegateCall)
            && self.inner.active_trace().is_some()
        {
            let parent = self.inner.active_trace().unwrap();
            parent.trace.value
        } else {
            to_alloy_u256(params.value.value())
        };
        let maybe_precompile =
            self.inner.config.exclude_precompile_calls.then(|| {
                self.inner.is_precompile_call(&to, value, params.space)
            });
        let to = to_alloy_address(to);
        let from = to_alloy_address(from);
        self.inner.start_trace_on_call(
            to,
            params.data.clone().unwrap_or_default().into(),
            value,
            params.call_type.into(),
            from,
            params.gas.as_u64(),
            maybe_precompile,
            self.tx_gas_limit,
            self.depth,
        );
        self.depth += 1;
    }
    fn record_call_result(&mut self, result: &FrameResult) {
        if self.is_fourbyte_tracer() {
            return;
        }
        self.depth -= 1;
        let mut gas_spent = self.gas_stack.pop().expect("should have value");
        if let Ok(r) = result {
            gas_spent = gas_spent - r.gas_left.as_u64();
            self.gas_left = r.gas_left.as_u64();
        }
        let instruction_result = to_instruction_result(result);
        if instruction_result.is_error() {
            self.inner.gas_inspector.set_gas_remainning(0);
        }
        let output = result
            .as_ref()
            .map(|f| Bytes::from(f.return_data.to_vec()))
            .unwrap_or_default();
        let outcome = InterpreterResult {
            result: instruction_result,
            output,
            gas: Gas::default(),
        };
        self.inner.fill_trace_on_call_end(outcome, None, gas_spent);
    }
    fn record_create(&mut self, params: &ActionParams) {
        if self.is_fourbyte_tracer() {
            return;
        }
        let gas_limit = params.gas.as_u64();
        self.gas_stack.push(gas_limit);
        let value = if matches!(params.call_type, CallType::DelegateCall) {
            if let Some(parent) = self.inner.active_trace() {
                parent.trace.value
            } else {
                to_alloy_u256(params.value.value())
            }
        } else {
            to_alloy_u256(params.value.value())
        };
        self.inner.start_trace_on_call(
            Address::default(), params.data.clone().unwrap_or_default().into(),
            value,
            params.call_type.into(),
            to_alloy_address(params.sender),
            params.gas.as_u64(),
            Some(false),
            params.gas.as_u64(),
            self.depth,
        );
        self.depth += 1;
    }
    fn record_create_result(&mut self, result: &FrameResult) {
        if self.is_fourbyte_tracer() {
            return;
        }
        self.depth -= 1;
        let mut gas_spent = self.gas_stack.pop().expect("should have value");
        if let Ok(r) = result {
            gas_spent = gas_spent - r.gas_left.as_u64();
            self.gas_left = r.gas_left.as_u64();
        }
        let instruction_result = to_instruction_result(result);
        if instruction_result.is_error() {
            self.inner.gas_inspector.set_gas_remainning(0);
        }
        let output = result
            .as_ref()
            .map(|f| Bytes::from(f.return_data.to_vec()))
            .unwrap_or_default();
        let outcome = InterpreterResult {
            result: instruction_result,
            output,
            gas: Gas::default(),
        };
        let create_address =
            if let Ok(FrameReturn { create_address, .. }) = result {
                create_address.as_ref().map(|h| to_alloy_address(*h))
            } else {
                None
            };
        self.inner
            .fill_trace_on_call_end(outcome, create_address, gas_spent);
    }
}
impl OpcodeTracer for GethTracer {
    fn do_trace_opcode(&self, enabled: &mut bool) {
        if self.inner.config.record_steps {
            *enabled |= true;
        }
    }
    fn initialize_interp(&mut self, gas_limit: cfx_types::U256) {
        self.inner
            .gas_inspector
            .set_gas_remainning(gas_limit.as_u64());
    }
    fn step(&mut self, interp: &dyn InterpreterInfo) {
        self.inner
            .gas_inspector
            .set_gas_remainning(interp.gas_remainning().as_u64());
        if self.inner.config.record_steps {
            self.inner.start_step(interp, self.depth as u64);
        }
    }
    fn step_end(&mut self, interp: &dyn InterpreterInfo) {
        let remainning = interp.gas_remainning().as_u64();
        let last_gas_cost = self
            .inner
            .gas_inspector
            .gas_remaining()
            .saturating_sub(remainning);
        self.inner.gas_inspector.set_gas_remainning(remainning);
        self.inner.gas_inspector.set_last_gas_cost(last_gas_cost);
        if self.inner.config.record_steps {
            self.inner.fill_step_on_step_end(interp);
        }
    }
    fn log(
        &mut self, _address: &cfx_types::Address,
        topics: &Vec<cfx_types::H256>, data: &[u8],
    ) {
        if self.inner.config.record_logs {
            let trace_idx = self.inner.last_trace_idx();
            let trace = &mut self.inner.traces.arena[trace_idx];
            trace.ordering.push(LogCallOrder::Log(trace.logs.len()));
            trace.logs.push(LogData::new_unchecked(
                topics.iter().map(|f| to_alloy_h256(*f)).collect(),
                Bytes::from(data.to_vec()),
            ));
        }
    }
    fn selfdestruct(
        &mut self, space: Space, _contract: &cfx_types::Address,
        target: &cfx_types::Address, _value: cfx_types::U256,
    ) {
        if self.is_fourbyte_tracer() {
            return;
        }
        let trace_idx = self.inner.last_trace_idx();
        let trace = &mut self.inner.traces.arena[trace_idx].trace;
        trace.selfdestruct_refund_target =
            Some(to_alloy_address(*target as H160))
    }
}
pub fn to_instruction_result(frame_result: &FrameResult) -> InstructionResult {
    let result = match frame_result {
        Ok(r) => match r.apply_state {
            true => InstructionResult::Return,
            false => InstructionResult::Revert,
        },
        Err(err) => match err {
            Error::OutOfGas => InstructionResult::OutOfGas,
            Error::BadJumpDestination { .. } => InstructionResult::InvalidJump,
            Error::BadInstruction { .. } => InstructionResult::OpcodeNotFound,
            Error::StackUnderflow { .. } => InstructionResult::StackUnderflow,
            Error::OutOfStack { .. } => InstructionResult::StackOverflow,
            Error::SubStackUnderflow { .. } => {
                InstructionResult::StackUnderflow
            }
            Error::OutOfSubStack { .. } => InstructionResult::StackOverflow,
            Error::InvalidSubEntry => InstructionResult::NotActivated, Error::NotEnoughBalanceForStorage { .. } => {
                InstructionResult::OutOfFunds
            }
            Error::ExceedStorageLimit => InstructionResult::OutOfGas, Error::BuiltIn(_) => InstructionResult::PrecompileError,
            Error::InternalContract(_) => InstructionResult::PrecompileError, Error::MutableCallInStaticContext => {
                InstructionResult::StateChangeDuringStaticCall
            }
            Error::StateDbError(_) => InstructionResult::FatalExternalError,
            Error::Wasm(_) => InstructionResult::NotActivated,
            Error::OutOfBounds => InstructionResult::OutOfOffset,
            Error::Reverted => InstructionResult::Revert,
            Error::InvalidAddress(_) => InstructionResult::Revert, Error::ConflictAddress(_) => InstructionResult::CreateCollision,
            Error::CreateContractStartingWithEF => {
                InstructionResult::CreateContractStartingWithEF
            }
            Error::CreateInitCodeSizeLimit => {
                InstructionResult::CreateInitCodeSizeLimit
            }
            Error::NonceOverflow(_) => InstructionResult::NonceOverflow,
        },
    };
    result
}