use super::*;
use console::program::{Future, Register};
use synthesizer_program::{Await, FinalizeRegistersState, Operand};
impl<N: Network> Process<N> {
    #[inline]
    pub fn finalize_deployment<P: FinalizeStorage<N>>(
        &self,
        state: FinalizeGlobalState,
        store: &FinalizeStore<N, P>,
        deployment: &Deployment<N>,
        fee: &Fee<N>,
    ) -> Result<(Stack<N>, Vec<FinalizeOperation<N>>)> {
        let timer = timer!("Process::finalize_deployment");
        let stack = Stack::new(self, deployment.program())?;
        lap!(timer, "Compute the stack");
        for (function_name, (verifying_key, _)) in deployment.verifying_keys() {
            stack.insert_verifying_key(function_name, verifying_key.clone())?;
        }
        lap!(timer, "Insert the verifying keys");
        atomic_batch_scope!(store, {
            let mut finalize_operations = Vec::with_capacity(deployment.program().mappings().len());
            let fee_stack = self.get_stack(fee.program_id())?;
            finalize_operations.extend(finalize_fee_transition(state, store, fee_stack, fee)?);
            lap!(timer, "Finalize transition for '{}/{}'", fee.program_id(), fee.function_name());
            let program_id = deployment.program_id();
            for mapping in deployment.program().mappings().values() {
                finalize_operations.push(store.initialize_mapping(*program_id, *mapping.name())?);
            }
            finish!(timer, "Initialize the program mappings");
            Ok((stack, finalize_operations))
        })
    }
    #[inline]
    pub fn finalize_execution<P: FinalizeStorage<N>>(
        &self,
        state: FinalizeGlobalState,
        store: &FinalizeStore<N, P>,
        execution: &Execution<N>,
        fee: Option<&Fee<N>>,
    ) -> Result<Vec<FinalizeOperation<N>>> {
        let timer = timer!("Program::finalize_execution");
        ensure!(!execution.is_empty(), "There are no transitions in the execution");
        let transition = execution.peek()?;
        let stack = self.get_stack(transition.program_id())?;
        let number_of_calls = stack.get_number_of_calls(transition.function_name())?;
        ensure!(
            number_of_calls == execution.len(),
            "The number of transitions in the execution is incorrect. Expected {number_of_calls}, but found {}",
            execution.len()
        );
        lap!(timer, "Verify the number of transitions");
        let call_graph = self.construct_call_graph(execution)?;
        atomic_batch_scope!(store, {
            let mut finalize_operations = finalize_transition(state, store, stack, transition, call_graph)?;
            if let Some(fee) = fee {
                let fee_stack = self.get_stack(fee.program_id())?;
                finalize_operations.extend(finalize_fee_transition(state, store, fee_stack, fee)?);
                lap!(timer, "Finalize transition for '{}/{}'", fee.program_id(), fee.function_name());
            }
            finish!(timer);
            Ok(finalize_operations)
        })
    }
    #[inline]
    pub fn finalize_fee<P: FinalizeStorage<N>>(
        &self,
        state: FinalizeGlobalState,
        store: &FinalizeStore<N, P>,
        fee: &Fee<N>,
    ) -> Result<Vec<FinalizeOperation<N>>> {
        let timer = timer!("Program::finalize_fee");
        atomic_batch_scope!(store, {
            let stack = self.get_stack(fee.program_id())?;
            let result = finalize_fee_transition(state, store, stack, fee);
            finish!(timer, "Finalize transition for '{}/{}'", fee.program_id(), fee.function_name());
            result
        })
    }
}
fn finalize_fee_transition<N: Network, P: FinalizeStorage<N>>(
    state: FinalizeGlobalState,
    store: &FinalizeStore<N, P>,
    stack: &Stack<N>,
    fee: &Fee<N>,
) -> Result<Vec<FinalizeOperation<N>>> {
    let mut call_graph = HashMap::new();
    call_graph.insert(*fee.transition_id(), Vec::new());
    match finalize_transition(state, store, stack, fee, call_graph) {
        Ok(finalize_operations) => Ok(finalize_operations),
        Err(error) => bail!("'finalize' failed on '{}/{}' - {error}", fee.program_id(), fee.function_name()),
    }
}
fn finalize_transition<N: Network, P: FinalizeStorage<N>>(
    state: FinalizeGlobalState,
    store: &FinalizeStore<N, P>,
    stack: &Stack<N>,
    transition: &Transition<N>,
    call_graph: HashMap<N::TransitionID, Vec<N::TransitionID>>,
) -> Result<Vec<FinalizeOperation<N>>> {
    let program_id = transition.program_id();
    let function_name = transition.function_name();
    #[cfg(debug_assertions)]
    println!("Finalizing transition for {}/{function_name}...", transition.program_id());
    debug_assert_eq!(stack.program_id(), transition.program_id());
    let future = match transition.outputs().last().and_then(|output| output.future()) {
        Some(future) => future,
        _ => return Ok(Vec::new()),
    };
    ensure!(
        future.program_id() == program_id && future.function_name() == function_name,
        "The program ID and function name of the future do not match the transition"
    );
    let mut finalize_operations = Vec::new();
    let mut states = Vec::new();
    states.push(initialize_finalize_state(state, future, stack, *transition.id())?);
    while let Some(FinalizeState {
        mut counter,
        finalize,
        mut registers,
        stack,
        mut call_counter,
        mut recent_call_locator,
    }) = states.pop()
    {
        while counter < finalize.commands().len() {
            let command = &finalize.commands()[counter];
            match &command {
                Command::BranchEq(branch_eq) => {
                    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
                        branch_to(counter, branch_eq, finalize, stack, ®isters)
                    }));
                    match result {
                        Ok(Ok(new_counter)) => {
                            counter = new_counter;
                        }
                        Ok(Err(error)) => bail!("'finalize' failed to evaluate command ({command}): {error}"),
                        Err(_) => bail!("'finalize' failed to evaluate command ({command})"),
                    }
                }
                Command::BranchNeq(branch_neq) => {
                    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
                        branch_to(counter, branch_neq, finalize, stack, ®isters)
                    }));
                    match result {
                        Ok(Ok(new_counter)) => {
                            counter = new_counter;
                        }
                        Ok(Err(error)) => bail!("'finalize' failed to evaluate command ({command}): {error}"),
                        Err(_) => bail!("'finalize' failed to evaluate command ({command})"),
                    }
                }
                Command::Await(await_) => {
                    let locator = *match await_.register() {
                        Register::Locator(locator) => locator,
                        Register::Access(..) => bail!("The 'await' register must be a locator"),
                    };
                    if let Some(recent_call_locator) = recent_call_locator {
                        ensure!(
                            locator > recent_call_locator,
                            "Await register's locator '{locator}' must be greater than the last seen call locator '{recent_call_locator}'",
                        )
                    }
                    let transition_id = registers.transition_id();
                    let child_transition_id = match call_graph.get(transition_id) {
                        Some(transitions) => match transitions.get(call_counter) {
                            Some(transition_id) => *transition_id,
                            None => bail!("Child transition ID not found."),
                        },
                        None => bail!("Transition ID '{transition_id}' not found in call graph"),
                    };
                    let callee_state = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
                        setup_await(state, await_, stack, ®isters, child_transition_id)
                    })) {
                        Ok(Ok(callee_state)) => callee_state,
                        Ok(Err(error)) => bail!("'finalize' failed to evaluate command ({command}): {error}"),
                        Err(_) => bail!("'finalize' failed to evaluate command ({command})"),
                    };
                    recent_call_locator = Some(locator);
                    call_counter += 1;
                    counter += 1;
                    let caller_state =
                        FinalizeState { counter, finalize, registers, stack, call_counter, recent_call_locator };
                    states.push(caller_state);
                    states.push(callee_state);
                    break;
                }
                _ => {
                    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
                        command.finalize(stack, store, &mut registers)
                    }));
                    match result {
                        Ok(Ok(Some(finalize_operation))) => finalize_operations.push(finalize_operation),
                        Ok(Ok(None)) => {}
                        Ok(Err(error)) => bail!("'finalize' failed to evaluate command ({command}): {error}"),
                        Err(_) => bail!("'finalize' failed to evaluate command ({command})"),
                    }
                    counter += 1;
                }
            };
        }
    }
    Ok(finalize_operations)
}
struct FinalizeState<'a, N: Network> {
    counter: usize,
    finalize: &'a Finalize<N>,
    registers: FinalizeRegisters<N>,
    stack: &'a Stack<N>,
    call_counter: usize,
    recent_call_locator: Option<u64>,
}
fn initialize_finalize_state<'a, N: Network>(
    state: FinalizeGlobalState,
    future: &Future<N>,
    stack: &'a Stack<N>,
    transition_id: N::TransitionID,
) -> Result<FinalizeState<'a, N>> {
    let (finalize, stack) = match stack.program_id() == future.program_id() {
        true => (stack.get_function_ref(future.function_name())?.finalize_logic(), stack),
        false => {
            let stack = stack.get_external_stack(future.program_id())?;
            (stack.get_function_ref(future.function_name())?.finalize_logic(), stack)
        }
    };
    let finalize = match finalize {
        Some(finalize) => finalize,
        None => bail!(
            "The function '{}/{}' does not have an associated finalize block",
            future.program_id(),
            future.function_name()
        ),
    };
    let mut registers = FinalizeRegisters::new(
        state,
        transition_id,
        *future.function_name(),
        stack.get_finalize_types(future.function_name())?.clone(),
    );
    finalize.inputs().iter().map(|i| i.register()).zip_eq(future.arguments().iter()).try_for_each(
        |(register, input)| {
            registers.store(stack, register, Value::from(input))
        },
    )?;
    Ok(FinalizeState { counter: 0, finalize, registers, stack, call_counter: 0, recent_call_locator: None })
}
#[inline]
fn setup_await<'a, N: Network>(
    state: FinalizeGlobalState,
    await_: &Await<N>,
    stack: &'a Stack<N>,
    registers: &FinalizeRegisters<N>,
    transition_id: N::TransitionID,
) -> Result<FinalizeState<'a, N>> {
    let future = match registers.load(stack, &Operand::Register(await_.register().clone()))? {
        Value::Future(future) => future,
        _ => bail!("The input to 'await' is not a future"),
    };
    initialize_finalize_state(state, &future, stack, transition_id)
}
#[inline]
fn branch_to<N: Network, const VARIANT: u8>(
    counter: usize,
    branch: &Branch<N, VARIANT>,
    finalize: &Finalize<N>,
    stack: &Stack<N>,
    registers: &FinalizeRegisters<N>,
) -> Result<usize> {
    let first = registers.load(stack, branch.first())?;
    let second = registers.load(stack, branch.second())?;
    let get_position_index = |position: &Identifier<N>| match finalize.positions().get(position) {
        Some(index) if *index > counter => Ok(*index),
        Some(_) => bail!("Cannot branch to an earlier position '{position}' in the program"),
        None => bail!("The position '{position}' does not exist."),
    };
    match VARIANT {
        0 if first == second => get_position_index(branch.position()),
        0 if first != second => Ok(counter + 1),
        1 if first == second => Ok(counter + 1),
        1 if first != second => get_position_index(branch.position()),
        _ => bail!("Invalid 'branch' variant: {VARIANT}"),
    }
}
#[cfg(test)]
mod tests {
    use super::*;
    use crate::tests::test_execute::{sample_fee, sample_finalize_state};
    use console::prelude::TestRng;
    use ledger_store::{
        helpers::memory::{BlockMemory, FinalizeMemory},
        BlockStore,
    };
    type CurrentNetwork = console::network::Testnet3;
    type CurrentAleo = circuit::network::AleoV0;
    #[test]
    fn test_finalize_deployment() {
        let rng = &mut TestRng::default();
        let program = Program::<CurrentNetwork>::from_str(
            r"
program testing.aleo;
struct message:
    amount as u128;
mapping account:
    key as address.public;
    value as u64.public;
record token:
    owner as address.private;
    amount as u64.private;
function initialize:
    input r0 as address.private;
    input r1 as u64.private;
    cast r0 r1 into r2 as token.record;
    output r2 as token.record;
function compute:
    input r0 as message.private;
    input r1 as message.public;
    input r2 as message.private;
    input r3 as token.record;
    add r0.amount r1.amount into r4;
    cast r3.owner r3.amount into r5 as token.record;
    output r4 as u128.public;
    output r5 as token.record;",
        )
        .unwrap();
        let mut process = Process::load().unwrap();
        let deployment = process.deploy::<CurrentAleo, _>(&program, rng).unwrap();
        let block_store = BlockStore::<CurrentNetwork, BlockMemory<_>>::open(None).unwrap();
        let finalize_store = FinalizeStore::<_, FinalizeMemory<_>>::open(None).unwrap();
        assert!(!process.contains_program(program.id()));
        let fee = sample_fee::<_, CurrentAleo, _, _>(&process, &block_store, &finalize_store, rng);
        let (stack, _) =
            process.finalize_deployment(sample_finalize_state(1), &finalize_store, &deployment, &fee).unwrap();
        process.add_stack(stack);
        assert!(process.contains_program(program.id()));
    }
}