use super::*;

/// A resolved function call attached to the callee's scope.
///
/// Scope-read summaries need call edges in source order. In:
///
/// ```sh
/// use_flag() { printf '%s\n' "$flag"; }
/// flag=1
/// use_flag
/// ```
///
/// the call creates a future read of `flag` from the caller's scope, even
/// though the textual read is inside the function body.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) struct ResolvedCallSite {
    pub(super) offset: usize,
    pub(super) span: Span,
    pub(super) callee_scope: ScopeId,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum ScopeReadEventKind {
    Direct(NameId),
    Call(ScopeId),
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) struct ScopeReadEvent {
    pub(super) offset: usize,
    pub(super) block: Option<BlockId>,
    pub(super) kind: ScopeReadEventKind,
}

#[derive(Debug, Clone)]
pub(super) struct ScopeReadPlan {
    /// Names read directly inside the scope, ignoring calls to other scopes.
    pub(super) direct_reads: DenseBitSet,
    /// Calls made by this scope, kept in source order for future-read queries.
    pub(super) calls: Vec<ResolvedCallSite>,
    /// Interleaved direct reads and calls, also source ordered.
    pub(super) events: Vec<ScopeReadEvent>,
    /// Whether the scope is a function body rather than file or compound scope.
    pub(super) is_function: bool,
}

/// Compact id space covering only names that have bindings.
///
/// Scope-read bitsets are only ever queried for binding names, so the
/// matrices stay narrow by dropping every other referenced name.
pub(super) struct BoundNameSpace {
    compact_by_name: Vec<Option<u32>>,
    len: usize,
}

impl BoundNameSpace {
    pub(super) fn new(binding_name_ids: &[NameId], name_count: usize) -> Self {
        let mut compact_by_name = vec![None; name_count];
        let mut len = 0u32;
        for name in binding_name_ids {
            let compact = &mut compact_by_name[name.index()];
            if compact.is_none() {
                *compact = Some(len);
                len += 1;
            }
        }
        Self {
            compact_by_name,
            len: len as usize,
        }
    }

    pub(super) fn len(&self) -> usize {
        self.len
    }

    pub(super) fn get(&self, name: NameId) -> Option<usize> {
        self.compact_by_name[name.index()].map(|compact| compact as usize)
    }
}

impl ScopeReadPlan {
    pub(super) fn new(name_count: usize, is_function: bool) -> Self {
        Self {
            direct_reads: DenseBitSet::new(name_count),
            calls: Vec::new(),
            events: Vec::new(),
            is_function,
        }
    }
}

#[derive(Debug, Clone)]
pub(super) struct ScopeFutureReads {
    pub(super) suffix_reads: DenseBitMatrix,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) struct CallerReadSite {
    pub(super) caller_scope: ScopeId,
    pub(super) offset: usize,
}

#[derive(Debug, Clone)]
pub(super) struct CompatibilityReadSets {
    pub(super) escape_reads: DenseBitMatrix,
    pub(super) future_reads: Vec<ScopeFutureReads>,
}

pub(crate) fn summarize_scope_provided_bindings(
    context: &DataflowContext<'_>,
    exact: &ExactVariableDataflow,
    scope: ScopeId,
) -> Vec<ProvidedBinding> {
    let exit_blocks = exit_blocks_for_scope(context.cfg, exact.scope_components(context), scope);
    if exit_blocks.is_empty() {
        return Vec::new();
    }

    let eligible_names = context
        .bindings
        .iter()
        .filter(|binding| {
            binding.scope == scope
                && !binding.attributes.contains(BindingAttributes::LOCAL)
                && binding_initializes_name(binding).is_some()
        })
        .map(|binding| binding.name.clone())
        .collect::<FxHashSet<_>>();

    let initialized_states = exact.initialized_name_states(context);
    let mut maybe_counts = FxHashMap::<Name, usize>::default();
    let mut definite_counts = FxHashMap::<Name, usize>::default();

    for exit_block in &exit_blocks {
        for name in &eligible_names {
            let Some(name_id) = exact.names.get(name) else {
                continue;
            };
            if initialized_states
                .maybe_out
                .contains(exit_block.index(), name_id.index())
            {
                *maybe_counts.entry(name.clone()).or_default() += 1;
            }
            if initialized_states
                .definite_out
                .contains(exit_block.index(), name_id.index())
            {
                *definite_counts.entry(name.clone()).or_default() += 1;
            }
        }
    }

    let exit_count = exit_blocks.len();
    let mut provided = Vec::new();
    for (name, maybe_count) in maybe_counts {
        let definite_count = definite_counts.get(&name).copied().unwrap_or_default();
        let certainty = if definite_count == exit_count {
            ContractCertainty::Definite
        } else if maybe_count > 0 {
            ContractCertainty::Possible
        } else {
            continue;
        };
        provided.push(ProvidedBinding::new(
            name,
            ProvidedBindingKind::Variable,
            certainty,
        ));
    }

    provided.sort_by(|left, right| {
        left.name
            .as_str()
            .cmp(right.name.as_str())
            .then_with(|| (left.kind as u8).cmp(&(right.kind as u8)))
    });
    provided
}

pub(crate) fn summarize_scope_provided_functions(
    context: &DataflowContext<'_>,
    exact: &ExactVariableDataflow,
    scope: ScopeId,
) -> Vec<ProvidedBinding> {
    let reaching_definitions = exact.reaching_definitions(context);
    let exit_blocks = exit_blocks_for_scope(context.cfg, exact.scope_components(context), scope);
    if exit_blocks.is_empty() {
        return Vec::new();
    }

    let eligible_names = context
        .bindings
        .iter()
        .filter(|binding| binding.scope == scope && function_binding_certainty(binding).is_some())
        .map(|binding| binding.name.clone())
        .collect::<FxHashSet<_>>();

    let mut maybe_counts = FxHashMap::<Name, usize>::default();
    let mut definite_counts = FxHashMap::<Name, usize>::default();

    for exit_block in &exit_blocks {
        let reaching_row = exit_block.index();

        for name in &eligible_names {
            let Some(name_id) = exact.names.get(name) else {
                continue;
            };
            let mut maybe_present = false;
            let mut definite_present = false;
            for binding_index in exact.binding_data.bindings_for_name[name_id.index()].iter_ones() {
                if !reaching_definitions
                    .reaching_out
                    .contains(reaching_row, binding_index)
                {
                    continue;
                }
                let binding = &context.bindings[binding_index];
                if binding.scope != scope {
                    continue;
                }
                let Some(certainty) = function_binding_certainty(binding) else {
                    continue;
                };
                maybe_present = true;
                definite_present |= certainty == ContractCertainty::Definite;
            }
            if maybe_present {
                *maybe_counts.entry(name.clone()).or_default() += 1;
            }
            if definite_present {
                *definite_counts.entry(name.clone()).or_default() += 1;
            }
        }
    }

    let exit_count = exit_blocks.len();
    let mut provided = Vec::new();
    for (name, maybe_count) in maybe_counts {
        let definite_count = definite_counts.get(&name).copied().unwrap_or_default();
        let certainty = if definite_count == exit_count {
            ContractCertainty::Definite
        } else if maybe_count > 0 {
            ContractCertainty::Possible
        } else {
            continue;
        };
        provided.push(ProvidedBinding::new(
            name,
            ProvidedBindingKind::Function,
            certainty,
        ));
    }

    provided.sort_by(|left, right| {
        left.name
            .as_str()
            .cmp(right.name.as_str())
            .then_with(|| (left.kind as u8).cmp(&(right.kind as u8)))
    });
    provided
}

fn exit_blocks_for_scope(
    cfg: &ControlFlowGraph,
    scope_components: &[ExactScopeComponent],
    scope: ScopeId,
) -> Vec<BlockId> {
    let component = &scope_components[scope.index()];
    if let Some(scope_exits) = cfg.scope_exits(scope) {
        return scope_exits.to_vec();
    }

    component
        .blocks
        .iter_ones()
        .filter_map(|block_index| {
            let block_id = BlockId(block_index as u32);
            block_exits_component(cfg, &component.blocks, block_id).then_some(block_id)
        })
        .collect()
}

pub(super) fn reachable_blocks_dense(
    cfg: &ControlFlowGraph,
    entry: BlockId,
    block_count: usize,
) -> DenseBitSet {
    let mut visited = DenseBitSet::new(block_count);
    let mut stack = vec![entry];
    while let Some(block_id) = stack.pop() {
        if visited.contains(block_id.index()) {
            continue;
        }
        visited.insert(block_id.index());
        stack.extend(
            cfg.successors(block_id)
                .iter()
                .map(|(successor, _)| *successor),
        );
    }
    visited
}

#[allow(clippy::too_many_arguments)]
pub(super) fn build_scope_read_plans(
    cfg: &ControlFlowGraph,
    scopes: &[Scope],
    references: &[Reference],
    synthetic_reads: &[SyntheticRead],
    reference_blocks: &[Option<BlockId>],
    reference_name_ids: &[NameId],
    synthetic_read_name_ids: &[NameId],
    call_sites: &FxHashMap<Name, SmallVec<[CallSite; 2]>>,
    visible_function_call_bindings: &FxHashMap<SpanKey, BindingId>,
    function_body_scopes: &FxHashMap<BindingId, ScopeId>,
    bound_names: &BoundNameSpace,
) -> (Vec<ScopeReadPlan>, Vec<Vec<CallerReadSite>>) {
    let calls_by_scope = resolved_calls_by_scope(
        call_sites,
        visible_function_call_bindings,
        function_body_scopes,
    );
    let mut plans = scopes
        .iter()
        .map(|scope| {
            ScopeReadPlan::new(
                bound_names.len(),
                matches!(scope.kind, ScopeKind::Function(_)),
            )
        })
        .collect::<Vec<_>>();
    let mut callers_by_callee = vec![Vec::new(); scopes.len()];

    for (reference_index, reference) in references.iter().enumerate() {
        let plan = &mut plans[reference.scope.index()];
        let name_id = reference_name_ids[reference_index];
        if let Some(compact) = bound_names.get(name_id) {
            plan.direct_reads.insert(compact);
        }
        plan.events.push(ScopeReadEvent {
            offset: reference.span.start.offset(),
            block: reference_blocks[reference_index],
            kind: ScopeReadEventKind::Direct(name_id),
        });
    }

    for (read_index, synthetic_read) in synthetic_reads.iter().enumerate() {
        let plan = &mut plans[synthetic_read.scope.index()];
        let name_id = synthetic_read_name_ids[read_index];
        if let Some(compact) = bound_names.get(name_id) {
            plan.direct_reads.insert(compact);
        }
        plan.events.push(ScopeReadEvent {
            offset: synthetic_read.span.start.offset(),
            block: command_block_for_span(cfg, synthetic_read.span),
            kind: ScopeReadEventKind::Direct(name_id),
        });
    }

    for (scope_id, calls) in calls_by_scope {
        let plan = &mut plans[scope_id.index()];
        for call in &calls {
            callers_by_callee[call.callee_scope.index()].push(CallerReadSite {
                caller_scope: scope_id,
                offset: call.offset,
            });
            plan.events.push(ScopeReadEvent {
                offset: call.offset,
                block: command_block_for_span(cfg, call.span),
                kind: ScopeReadEventKind::Call(call.callee_scope),
            });
        }
        plan.calls = calls;
    }

    for plan in &mut plans {
        plan.events.sort_by_key(|event| event.offset);
    }

    (plans, callers_by_callee)
}

pub(super) fn compute_transitive_read_sets(
    read_plans: &[ScopeReadPlan],
    scopes: &[Scope],
    name_count: usize,
) -> DenseBitMatrix {
    let nested_child_scopes = nested_non_function_child_scopes(scopes);
    let mut transitive_reads = DenseBitMatrix::zeros(read_plans.len(), name_count);
    let mut reads = DenseBitSet::new(name_count);
    loop {
        let mut changed = false;
        for (scope_index, plan) in read_plans.iter().enumerate() {
            reads.copy_from(&plan.direct_reads);
            for &child_scope in &nested_child_scopes[scope_index] {
                reads.union_with_words(transitive_reads.row(child_scope.index()));
            }
            for call in &plan.calls {
                reads.union_with_words(transitive_reads.row(call.callee_scope.index()));
            }
            if transitive_reads.replace_row_if_changed(scope_index, reads.as_words()) {
                changed = true;
            }
        }
        if !changed {
            break;
        }
    }

    transitive_reads
}

pub(super) fn compute_compatibility_read_sets(
    read_plans: &[ScopeReadPlan],
    callers_by_callee: &[Vec<CallerReadSite>],
    transitive_reads: &DenseBitMatrix,
    bound_names: &BoundNameSpace,
) -> CompatibilityReadSets {
    let future_reads = build_future_read_summaries(read_plans, transitive_reads, bound_names);
    let mut escape_reads = DenseBitMatrix::zeros(read_plans.len(), bound_names.len());
    let mut reads = DenseBitSet::new(bound_names.len());
    loop {
        let mut changed = false;
        for (scope_index, plan) in read_plans.iter().enumerate() {
            if !plan.is_function {
                continue;
            }

            reads.clear();
            for caller in &callers_by_callee[scope_index] {
                future_reads_union_after(
                    &mut reads,
                    caller.caller_scope,
                    caller.offset,
                    read_plans,
                    &future_reads,
                    &escape_reads,
                );
            }
            if escape_reads.replace_row_if_changed(scope_index, reads.as_words()) {
                changed = true;
            }
        }
        if !changed {
            break;
        }
    }

    CompatibilityReadSets {
        escape_reads,
        future_reads,
    }
}

fn nested_non_function_child_scopes(scopes: &[Scope]) -> Vec<Vec<ScopeId>> {
    let mut children = vec![Vec::new(); scopes.len()];
    for scope in scopes {
        let Some(parent) = scope.parent else {
            continue;
        };
        if matches!(scope.kind, ScopeKind::Function(_)) {
            continue;
        }
        children[parent.index()].push(scope.id);
    }
    children
}

fn build_future_read_summaries(
    read_plans: &[ScopeReadPlan],
    transitive_reads: &DenseBitMatrix,
    bound_names: &BoundNameSpace,
) -> Vec<ScopeFutureReads> {
    read_plans
        .iter()
        .map(|plan| {
            let mut suffix_reads = DenseBitMatrix::zeros(plan.events.len() + 1, bound_names.len());
            for event_index in (0..plan.events.len()).rev() {
                suffix_reads.copy_row_from_row(event_index, event_index + 1);
                match plan.events[event_index].kind {
                    ScopeReadEventKind::Direct(name_id) => {
                        if let Some(compact) = bound_names.get(name_id) {
                            suffix_reads.insert(event_index, compact);
                        }
                    }
                    ScopeReadEventKind::Call(callee_scope) => {
                        suffix_reads.union_row_with_words(
                            event_index,
                            transitive_reads.row(callee_scope.index()),
                        );
                    }
                }
            }
            ScopeFutureReads { suffix_reads }
        })
        .collect()
}

fn future_reads_union_after(
    destination: &mut DenseBitSet,
    scope: ScopeId,
    offset: usize,
    read_plans: &[ScopeReadPlan],
    future_reads: &[ScopeFutureReads],
    escape_reads: &DenseBitMatrix,
) {
    let plan = &read_plans[scope.index()];
    let index = plan.events.partition_point(|event| event.offset <= offset);
    destination.union_with_words(future_reads[scope.index()].suffix_reads.row(index));
    if plan.is_function {
        destination.union_with_words(escape_reads.row(scope.index()));
    }
}

fn future_reads_contain_after(
    scope: ScopeId,
    offset: usize,
    compact_name: usize,
    read_plans: &[ScopeReadPlan],
    future_reads: &[ScopeFutureReads],
    escape_reads: &DenseBitMatrix,
) -> bool {
    let plan = &read_plans[scope.index()];
    let index = plan.events.partition_point(|event| event.offset <= offset);
    future_reads[scope.index()]
        .suffix_reads
        .contains(index, compact_name)
        || (plan.is_function && escape_reads.contains(scope.index(), compact_name))
}

#[allow(clippy::too_many_arguments)]
pub(super) fn binding_has_future_reads_before_local_shadow(
    binding: &Binding,
    name_id: NameId,
    bound_names: &BoundNameSpace,
    bindings: &[Binding],
    next_local_shadows: &[Option<BindingId>],
    cfg: &ControlFlowGraph,
    binding_blocks: &[Option<BlockId>],
    read_plans: &[ScopeReadPlan],
    transitive_reads: &DenseBitMatrix,
    future_reads: &[ScopeFutureReads],
    escape_reads: &DenseBitMatrix,
) -> bool {
    let Some(compact_name) = bound_names.get(name_id) else {
        return false;
    };
    let shadow = next_local_shadows[binding.id.index()].map(|shadow| &bindings[shadow.index()]);
    let escape_reads_visible = read_plans[binding.scope.index()].is_function
        && escape_reads.contains(binding.scope.index(), compact_name);

    if let Some(shadow) = shadow {
        if let (Some(binding_block), Some(shadow_block)) = (
            binding_blocks[binding.id.index()],
            binding_blocks[shadow.id.index()],
        ) {
            escape_reads_visible
                || future_reads_contain_after_without_shadow(
                    binding.scope,
                    binding.span.start.offset(),
                    shadow.span.start.offset(),
                    binding_block,
                    shadow_block,
                    name_id,
                    compact_name,
                    read_plans,
                    transitive_reads,
                    cfg,
                )
        } else {
            escape_reads_visible
                || future_reads_contain_after_until(
                    binding.scope,
                    binding.span.start.offset(),
                    shadow.span.start.offset(),
                    name_id,
                    compact_name,
                    read_plans,
                    transitive_reads,
                )
        }
    } else {
        future_reads_contain_after(
            binding.scope,
            binding.span.start.offset(),
            compact_name,
            read_plans,
            future_reads,
            escape_reads,
        )
    }
}

pub(super) fn next_shadowing_local_declarations(
    bindings: &[Binding],
    binding_name_ids: &[NameId],
) -> Vec<Option<BindingId>> {
    let mut next_shadows = vec![None; bindings.len()];
    let mut next_by_scope_and_name: FxHashMap<(ScopeId, NameId), BindingId> = FxHashMap::default();

    for binding in bindings.iter().rev() {
        let key = (binding.scope, binding_name_ids[binding.id.index()]);
        next_shadows[binding.id.index()] = next_by_scope_and_name.get(&key).copied();

        if matches!(binding.kind, BindingKind::Declaration(_))
            && binding.attributes.contains(BindingAttributes::LOCAL)
        {
            next_by_scope_and_name.insert(key, binding.id);
        }
    }

    next_shadows
}

pub(super) fn future_reads_contain_after_until(
    scope: ScopeId,
    after_offset: usize,
    before_offset: usize,
    name_id: NameId,
    compact_name: usize,
    read_plans: &[ScopeReadPlan],
    transitive_reads: &DenseBitMatrix,
) -> bool {
    if before_offset <= after_offset {
        return false;
    }

    let plan = &read_plans[scope.index()];
    let start = plan
        .events
        .partition_point(|event| event.offset <= after_offset);
    let end = plan
        .events
        .partition_point(|event| event.offset < before_offset);

    plan.events[start..end]
        .iter()
        .any(|event| match event.kind {
            ScopeReadEventKind::Direct(candidate) => candidate == name_id,
            ScopeReadEventKind::Call(callee_scope) => {
                transitive_reads.contains(callee_scope.index(), compact_name)
            }
        })
}

#[allow(clippy::too_many_arguments)]
fn future_reads_contain_after_without_shadow(
    scope: ScopeId,
    after_offset: usize,
    shadow_offset: usize,
    binding_block: BlockId,
    shadow_block: BlockId,
    name_id: NameId,
    compact_name: usize,
    read_plans: &[ScopeReadPlan],
    transitive_reads: &DenseBitMatrix,
    cfg: &ControlFlowGraph,
) -> bool {
    let plan = &read_plans[scope.index()];
    let start = plan
        .events
        .partition_point(|event| event.offset <= after_offset);

    plan.events[start..].iter().any(|event| {
        let uses_name = match event.kind {
            ScopeReadEventKind::Direct(candidate) => candidate == name_id,
            ScopeReadEventKind::Call(callee_scope) => {
                transitive_reads.contains(callee_scope.index(), compact_name)
            }
        };
        if !uses_name {
            return false;
        }
        if event.offset < shadow_offset {
            return true;
        }

        let Some(event_block) = event.block else {
            return true;
        };
        if event_block == shadow_block {
            return false;
        }

        block_reaches_without(cfg, binding_block, event_block, shadow_block)
    })
}

pub(crate) fn binding_initializes_name(binding: &Binding) -> Option<ContractCertainty> {
    match binding.kind {
        BindingKind::Declaration(_) | BindingKind::Nameref => binding
            .attributes
            .contains(BindingAttributes::DECLARATION_INITIALIZED)
            .then_some(ContractCertainty::Definite),
        BindingKind::FunctionDefinition => None,
        BindingKind::Imported => {
            if binding
                .attributes
                .contains(BindingAttributes::IMPORTED_FUNCTION)
            {
                None
            } else if binding
                .attributes
                .contains(BindingAttributes::IMPORTED_POSSIBLE)
            {
                Some(ContractCertainty::Possible)
            } else {
                Some(ContractCertainty::Definite)
            }
        }
        BindingKind::Assignment
        | BindingKind::ParameterDefaultAssignment
        | BindingKind::AppendAssignment
        | BindingKind::ArrayAssignment
        | BindingKind::LoopVariable
        | BindingKind::ReadTarget
        | BindingKind::MapfileTarget
        | BindingKind::PrintfTarget
        | BindingKind::GetoptsTarget
        | BindingKind::ZparseoptsTarget
        | BindingKind::ArithmeticAssignment => Some(ContractCertainty::Definite),
    }
}

pub(crate) fn function_binding_certainty(binding: &Binding) -> Option<ContractCertainty> {
    match binding.kind {
        BindingKind::FunctionDefinition => Some(ContractCertainty::Definite),
        BindingKind::Imported
            if binding
                .attributes
                .contains(BindingAttributes::IMPORTED_FUNCTION) =>
        {
            if binding
                .attributes
                .contains(BindingAttributes::IMPORTED_POSSIBLE)
            {
                Some(ContractCertainty::Possible)
            } else {
                Some(ContractCertainty::Definite)
            }
        }
        _ => None,
    }
}

pub(super) fn resolved_calls_by_scope(
    call_sites: &FxHashMap<Name, SmallVec<[CallSite; 2]>>,
    visible_function_call_bindings: &FxHashMap<SpanKey, BindingId>,
    function_scopes: &FxHashMap<BindingId, ScopeId>,
) -> FxHashMap<ScopeId, Vec<ResolvedCallSite>> {
    let mut calls_by_scope: FxHashMap<ScopeId, Vec<ResolvedCallSite>> = FxHashMap::default();
    for call in resolved_function_calls_with_callee_scope(
        call_sites,
        visible_function_call_bindings,
        function_scopes,
    ) {
        calls_by_scope
            .entry(call.site.scope)
            .or_default()
            .push(ResolvedCallSite {
                offset: call.site.span.start.offset(),
                span: call.site.span,
                callee_scope: call.callee_scope,
            });
    }
    for calls in calls_by_scope.values_mut() {
        calls.sort_by_key(|call| call.offset);
    }
    calls_by_scope
}

pub(super) fn is_function_escape_candidate(binding: &Binding, scopes: &[Scope]) -> bool {
    matches!(scopes[binding.scope.index()].kind, ScopeKind::Function(_))
        && !binding.attributes.contains(BindingAttributes::LOCAL)
        && !matches!(
            binding.kind,
            BindingKind::FunctionDefinition | BindingKind::Imported | BindingKind::Nameref
        )
}
