use std::collections::VecDeque;

use rustc_middle::bug;
use rustc_middle::mir::coverage::{
    BlockMarkerId, ConditionId, ConditionInfo, MCDCBranchSpan, MCDCDecisionSpan,
};
use rustc_middle::mir::{BasicBlock, SourceInfo};
use rustc_middle::thir::LogicalOp;
use rustc_middle::ty::TyCtxt;
use rustc_span::Span;

use crate::builder::Builder;
use crate::errors::MCDCExceedsConditionLimit;

/// LLVM uses `i16` to represent condition id. Hence `i16::MAX` is the hard limit for number of
/// conditions in a decision.
const MAX_CONDITIONS_IN_DECISION: usize = i16::MAX as usize;

#[derive(Default)]
struct MCDCDecisionCtx {
    /// To construct condition evaluation tree.
    decision_stack: VecDeque<ConditionInfo>,
    processing_decision: Option<MCDCDecisionSpan>,
    conditions: Vec<MCDCBranchSpan>,
}

struct MCDCState {
    decision_ctx_stack: Vec<MCDCDecisionCtx>,
}

impl MCDCState {
    fn new() -> Self {
        Self { decision_ctx_stack: vec![MCDCDecisionCtx::default()] }
    }

    /// Decision depth is given as a u16 to reduce the size of the `CoverageKind`,
    /// as it is very unlikely that the depth ever reaches 2^16.
    #[inline]
    fn decision_depth(&self) -> u16 {
        match u16::try_from(self.decision_ctx_stack.len())
            .expect(
                "decision depth did not fit in u16, this is likely to be an instrumentation error",
            )
            .checked_sub(1)
        {
            Some(d) => d,
            None => bug!("Unexpected empty decision stack"),
        }
    }

    // At first we assign ConditionIds for each sub expression.
    // If the sub expression is composite, re-assign its ConditionId to its LHS and generate a new ConditionId for its RHS.
    //
    // Example: "x = (A && B) || (C && D) || (D && F)"
    //
    //      Visit Depth1:
    //              (A && B) || (C && D) || (D && F)
    //              ^-------LHS--------^    ^-RHS--^
    //                      ID=1              ID=2
    //
    //      Visit LHS-Depth2:
    //              (A && B) || (C && D)
    //              ^-LHS--^    ^-RHS--^
    //                ID=1        ID=3
    //
    //      Visit LHS-Depth3:
    //               (A && B)
    //               LHS   RHS
    //               ID=1  ID=4
    //
    //      Visit RHS-Depth3:
    //                         (C && D)
    //                         LHS   RHS
    //                         ID=3  ID=5
    //
    //      Visit RHS-Depth2:              (D && F)
    //                                     LHS   RHS
    //                                     ID=2  ID=6
    //
    //      Visit Depth1:
    //              (A && B)  || (C && D)  || (D && F)
    //              ID=1  ID=4   ID=3  ID=5   ID=2  ID=6
    //
    // A node ID of '0' always means MC/DC isn't being tracked.
    //
    // If a "next" node ID is '0', it means it's the end of the test vector.
    //
    // As the compiler tracks expression in pre-order, we can ensure that condition info of parents are always properly assigned when their children are visited.
    // - If the op is AND, the "false_next" of LHS and RHS should be the parent's "false_next". While "true_next" of the LHS is the RHS, the "true next" of RHS is the parent's "true_next".
    // - If the op is OR, the "true_next" of LHS and RHS should be the parent's "true_next". While "false_next" of the LHS is the RHS, the "false next" of RHS is the parent's "false_next".
    fn record_conditions(&mut self, op: LogicalOp, span: Span) {
        let decision_depth = self.decision_depth();
        let Some(decision_ctx) = self.decision_ctx_stack.last_mut() else {
            bug!("Unexpected empty decision_ctx_stack")
        };
        let decision = match decision_ctx.processing_decision.as_mut() {
            Some(decision) => {
                decision.span = decision.span.to(span);
                decision
            }
            None => decision_ctx.processing_decision.insert(MCDCDecisionSpan {
                span,
                num_conditions: 0,
                end_markers: vec![],
                decision_depth,
            }),
        };

        let parent_condition = decision_ctx.decision_stack.pop_back().unwrap_or_else(|| {
            assert_eq!(
                decision.num_conditions, 0,
                "decision stack must be empty only for empty decision"
            );
            decision.num_conditions += 1;
            ConditionInfo {
                condition_id: ConditionId::START,
                true_next_id: None,
                false_next_id: None,
            }
        });
        let lhs_id = parent_condition.condition_id;

        let rhs_condition_id = ConditionId::from(decision.num_conditions);
        decision.num_conditions += 1;
        let (lhs, rhs) = match op {
            LogicalOp::And => {
                let lhs = ConditionInfo {
                    condition_id: lhs_id,
                    true_next_id: Some(rhs_condition_id),
                    false_next_id: parent_condition.false_next_id,
                };
                let rhs = ConditionInfo {
                    condition_id: rhs_condition_id,
                    true_next_id: parent_condition.true_next_id,
                    false_next_id: parent_condition.false_next_id,
                };
                (lhs, rhs)
            }
            LogicalOp::Or => {
                let lhs = ConditionInfo {
                    condition_id: lhs_id,
                    true_next_id: parent_condition.true_next_id,
                    false_next_id: Some(rhs_condition_id),
                };
                let rhs = ConditionInfo {
                    condition_id: rhs_condition_id,
                    true_next_id: parent_condition.true_next_id,
                    false_next_id: parent_condition.false_next_id,
                };
                (lhs, rhs)
            }
        };
        // We visit expressions tree in pre-order, so place the left-hand side on the top.
        decision_ctx.decision_stack.push_back(rhs);
        decision_ctx.decision_stack.push_back(lhs);
    }

    fn try_finish_decision(
        &mut self,
        span: Span,
        true_marker: BlockMarkerId,
        false_marker: BlockMarkerId,
        degraded_branches: &mut Vec<MCDCBranchSpan>,
    ) -> Option<(MCDCDecisionSpan, Vec<MCDCBranchSpan>)> {
        let Some(decision_ctx) = self.decision_ctx_stack.last_mut() else {
            bug!("Unexpected empty decision_ctx_stack")
        };
        let Some(condition_info) = decision_ctx.decision_stack.pop_back() else {
            let branch = MCDCBranchSpan {
                span,
                condition_info: ConditionInfo {
                    condition_id: ConditionId::START,
                    true_next_id: None,
                    false_next_id: None,
                },
                true_marker,
                false_marker,
            };
            degraded_branches.push(branch);
            return None;
        };
        let Some(decision) = decision_ctx.processing_decision.as_mut() else {
            bug!("Processing decision should have been created before any conditions are taken");
        };
        if condition_info.true_next_id.is_none() {
            decision.end_markers.push(true_marker);
        }
        if condition_info.false_next_id.is_none() {
            decision.end_markers.push(false_marker);
        }
        decision_ctx.conditions.push(MCDCBranchSpan {
            span,
            condition_info,
            true_marker,
            false_marker,
        });

        if decision_ctx.decision_stack.is_empty() {
            let conditions = std::mem::take(&mut decision_ctx.conditions);
            decision_ctx.processing_decision.take().map(|decision| (decision, conditions))
        } else {
            None
        }
    }
}

pub(crate) struct MCDCInfoBuilder {
    degraded_spans: Vec<MCDCBranchSpan>,
    mcdc_spans: Vec<(MCDCDecisionSpan, Vec<MCDCBranchSpan>)>,
    state: MCDCState,
}

impl MCDCInfoBuilder {
    pub(crate) fn new() -> Self {
        Self { degraded_spans: vec![], mcdc_spans: vec![], state: MCDCState::new() }
    }

    pub(crate) fn visit_evaluated_condition(
        &mut self,
        tcx: TyCtxt<'_>,
        source_info: SourceInfo,
        true_block: BasicBlock,
        false_block: BasicBlock,
        mut inject_block_marker: impl FnMut(SourceInfo, BasicBlock) -> BlockMarkerId,
    ) {
        let true_marker = inject_block_marker(source_info, true_block);
        let false_marker = inject_block_marker(source_info, false_block);

        // take_condition() returns Some for decision_result when the decision stack
        // is empty, i.e. when all the conditions of the decision were instrumented,
        // and the decision is "complete".
        if let Some((decision, conditions)) = self.state.try_finish_decision(
            source_info.span,
            true_marker,
            false_marker,
            &mut self.degraded_spans,
        ) {
            let num_conditions = conditions.len();
            assert_eq!(
                num_conditions, decision.num_conditions,
                "final number of conditions is not correct"
            );
            match num_conditions {
                0 => {
                    unreachable!("Decision with no condition is not expected");
                }
                1..=MAX_CONDITIONS_IN_DECISION => {
                    self.mcdc_spans.push((decision, conditions));
                }
                _ => {
                    self.degraded_spans.extend(conditions);

                    tcx.dcx().emit_warn(MCDCExceedsConditionLimit {
                        span: decision.span,
                        num_conditions,
                        max_conditions: MAX_CONDITIONS_IN_DECISION,
                    });
                }
            }
        }
    }

    pub(crate) fn into_done(
        self,
    ) -> (Vec<(MCDCDecisionSpan, Vec<MCDCBranchSpan>)>, Vec<MCDCBranchSpan>) {
        (self.mcdc_spans, self.degraded_spans)
    }
}

impl Builder<'_, '_> {
    pub(crate) fn visit_coverage_branch_operation(&mut self, logical_op: LogicalOp, span: Span) {
        if let Some(coverage_info) = self.coverage_info.as_mut()
            && let Some(mcdc_info) = coverage_info.mcdc_info.as_mut()
        {
            mcdc_info.state.record_conditions(logical_op, span);
        }
    }

    pub(crate) fn mcdc_increment_depth_if_enabled(&mut self) {
        if let Some(coverage_info) = self.coverage_info.as_mut()
            && let Some(mcdc_info) = coverage_info.mcdc_info.as_mut()
        {
            mcdc_info.state.decision_ctx_stack.push(MCDCDecisionCtx::default());
        };
    }

    pub(crate) fn mcdc_decrement_depth_if_enabled(&mut self) {
        if let Some(coverage_info) = self.coverage_info.as_mut()
            && let Some(mcdc_info) = coverage_info.mcdc_info.as_mut()
            && mcdc_info.state.decision_ctx_stack.pop().is_none()
        {
            bug!("Unexpected empty decision stack");
        };
    }
}
