Documentation
use std::{hash::Hash, sync::Arc};

use hyphae::{CellImmutable, CellMap, CellMutable, MapDiff, traits::CellValue};

use super::AnyItem;
use crate::common::with_id::WithTypedId;

#[must_use]
pub fn downcast_any_item_map_diff<T: Clone + 'static>(
    diff: &MapDiff<Arc<str>, Arc<dyn AnyItem>>,
    context: &'static str,
) -> Option<MapDiff<Arc<str>, T>> {
    match diff {
        MapDiff::Initial { entries } => Some(MapDiff::Initial {
            entries: entries
                .iter()
                .filter_map(|(k, v)| {
                    v.as_any().downcast_ref::<T>().map_or_else(
                        || {
                            tracing::error!("{context} type mismatch in Initial");
                            None
                        },
                        |typed| Some((k.clone(), typed.clone())),
                    )
                })
                .collect(),
        }),
        MapDiff::Insert { key, value } => {
            let Some(typed) = value.as_any().downcast_ref::<T>() else {
                tracing::error!("{context} type mismatch in Insert");
                return None;
            };
            Some(MapDiff::Insert {
                key: key.clone(),
                value: typed.clone(),
            })
        }
        MapDiff::Remove { key, old_value } => {
            let Some(typed) = old_value.as_any().downcast_ref::<T>() else {
                tracing::error!("{context} type mismatch in Remove");
                return None;
            };
            Some(MapDiff::Remove {
                key: key.clone(),
                old_value: typed.clone(),
            })
        }
        MapDiff::Update {
            key,
            old_value,
            new_value,
        } => {
            let Some(old_typed) = old_value.as_any().downcast_ref::<T>() else {
                tracing::error!("{context} type mismatch in Update old_value");
                return None;
            };
            let Some(new_typed) = new_value.as_any().downcast_ref::<T>() else {
                tracing::error!("{context} type mismatch in Update new_value");
                return None;
            };
            Some(MapDiff::Update {
                key: key.clone(),
                old_value: old_typed.clone(),
                new_value: new_typed.clone(),
            })
        }
        MapDiff::Batch { changes } => Some(MapDiff::Batch {
            changes: changes
                .iter()
                .filter_map(|change| downcast_any_item_map_diff::<T>(change, context))
                .collect(),
        }),
    }
}

pub fn apply_map_diff<K, V>(output: &CellMap<K, V, CellMutable>, diff: &MapDiff<K, V>)
where
    K: Hash + Eq + CellValue,
    V: CellValue,
{
    output.apply_diff_owned(diff.clone());
}

#[must_use]
pub fn typed_map_from_any_item<T: CellValue + 'static>(
    source: CellMap<Arc<str>, Arc<dyn AnyItem>, CellImmutable>,
    context: &'static str,
) -> CellMap<Arc<str>, T, CellImmutable> {
    let typed = CellMap::<Arc<str>, T>::new();
    let weak = typed.downgrade();
    let guard = source.subscribe_diffs(move |diff| {
        let Some(typed) = weak.upgrade() else { return };
        if let Some(typed_diff) = downcast_any_item_map_diff::<T>(diff, context) {
            apply_map_diff(&typed, &typed_diff);
        }
    });
    typed.own_guard(guard);
    drop(source);
    typed.lock()
}

#[must_use]
pub fn typed_map_arc_from_any_item<T: std::fmt::Debug + PartialEq + Send + Sync + 'static>(
    source: CellMap<Arc<str>, Arc<dyn AnyItem>, CellImmutable>,
    context: &'static str,
) -> CellMap<Arc<str>, Arc<T>, CellImmutable> {
    let typed = CellMap::<Arc<str>, Arc<T>>::new();
    let weak = typed.downgrade();
    let guard = source.subscribe_diffs(move |diff| {
        let Some(typed) = weak.upgrade() else { return };
        if let Some(typed_diff) = downcast_any_item_map_diff_arc::<T>(diff, context) {
            apply_map_diff(&typed, &typed_diff);
        }
    });
    typed.own_guard(guard);
    drop(source);
    typed.lock()
}

#[must_use]
pub fn typed_map_from_any_item_with_typed_id<T>(
    source: CellMap<Arc<str>, Arc<dyn AnyItem>, CellImmutable>,
    context: &'static str,
) -> CellMap<<T as WithTypedId>::Id, Arc<T>, CellImmutable>
where
    T: CellValue + WithTypedId + Send + Sync + 'static,
{
    let typed = CellMap::<<T as WithTypedId>::Id, Arc<T>>::new();
    let weak = typed.downgrade();
    let guard = source.subscribe_diffs(move |diff| {
        let Some(typed) = weak.upgrade() else { return };
        let Some(typed_diff) = downcast_any_item_map_diff_arc::<T>(diff, context) else {
            return;
        };
        let mut changes: Vec<MapDiff<<T as WithTypedId>::Id, Arc<T>>> = Vec::new();
        remap_diff_to_typed_id(&typed_diff, &mut changes);
        if changes.len() == 1 {
            if let Some(change) = changes.pop() {
                typed.apply_diff_owned(change);
            }
        } else {
            typed.apply_batch(changes);
        }
    });
    typed.own_guard(guard);
    drop(source);
    typed.lock()
}

pub fn downcast_any_item_arc<T: Send + Sync + 'static>(
    value: &Arc<dyn AnyItem>,
    context: &'static str,
) -> Option<Arc<T>> {
    let any_arc: Arc<dyn std::any::Any + Send + Sync> = value.clone();
    any_arc.downcast::<T>().map_or_else(
        |_| {
            tracing::error!("{context} type mismatch while downcasting Arc<dyn AnyItem>");
            None
        },
        Some,
    )
}

pub fn downcast_any_item_map_diff_arc<T: Send + Sync + 'static>(
    diff: &MapDiff<Arc<str>, Arc<dyn AnyItem>>,
    context: &'static str,
) -> Option<MapDiff<Arc<str>, Arc<T>>> {
    match diff {
        MapDiff::Initial { entries } => Some(MapDiff::Initial {
            entries: entries
                .iter()
                .filter_map(|(k, v)| {
                    downcast_any_item_arc::<T>(v, context).map(|typed| (k.clone(), typed))
                })
                .collect(),
        }),
        MapDiff::Insert { key, value } => Some(MapDiff::Insert {
            key: key.clone(),
            value: downcast_any_item_arc::<T>(value, context)?,
        }),
        MapDiff::Remove { key, old_value } => Some(MapDiff::Remove {
            key: key.clone(),
            old_value: downcast_any_item_arc::<T>(old_value, context)?,
        }),
        MapDiff::Update {
            key,
            old_value,
            new_value,
        } => Some(MapDiff::Update {
            key: key.clone(),
            old_value: downcast_any_item_arc::<T>(old_value, context)?,
            new_value: downcast_any_item_arc::<T>(new_value, context)?,
        }),
        MapDiff::Batch { changes } => Some(MapDiff::Batch {
            changes: changes
                .iter()
                .filter_map(|change| downcast_any_item_map_diff_arc::<T>(change, context))
                .collect(),
        }),
    }
}

fn remap_diff_to_typed_id<T>(
    diff: &MapDiff<Arc<str>, Arc<T>>,
    out: &mut Vec<MapDiff<<T as WithTypedId>::Id, Arc<T>>>,
) where
    T: CellValue + WithTypedId + 'static,
{
    match diff {
        MapDiff::Initial { entries } => {
            out.push(MapDiff::Initial {
                entries: entries
                    .iter()
                    .map(|(_, value)| (value.as_ref().typed_id(), value.clone()))
                    .collect(),
            });
        }
        MapDiff::Insert { value, .. } => {
            out.push(MapDiff::Insert {
                key: value.as_ref().typed_id(),
                value: value.clone(),
            });
        }
        MapDiff::Remove { old_value, .. } => {
            out.push(MapDiff::Remove {
                key: old_value.as_ref().typed_id(),
                old_value: old_value.clone(),
            });
        }
        MapDiff::Update {
            old_value,
            new_value,
            ..
        } => {
            let old_key = old_value.as_ref().typed_id();
            let new_key = new_value.as_ref().typed_id();
            if old_key == new_key {
                out.push(MapDiff::Update {
                    key: new_key,
                    old_value: old_value.clone(),
                    new_value: new_value.clone(),
                });
            } else {
                out.push(MapDiff::Remove {
                    key: old_key,
                    old_value: old_value.clone(),
                });
                out.push(MapDiff::Insert {
                    key: new_key,
                    value: new_value.clone(),
                });
            }
        }
        MapDiff::Batch { changes } => {
            for change in changes {
                remap_diff_to_typed_id(change, out);
            }
        }
    }
}