use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;

use regex::Regex;

use bindgen::callbacks::{DiscoveredItem, DiscoveredItemId, ParseCallbacks};
use bindgen::Builder;

#[derive(Debug, Default)]
struct ItemDiscovery(Rc<RefCell<ItemCache>>);

pub type ItemCache = HashMap<DiscoveredItemId, DiscoveredItem>;

impl ParseCallbacks for ItemDiscovery {
    fn new_item_found(&self, _id: DiscoveredItemId, _item: DiscoveredItem) {
        self.0.borrow_mut().insert(_id, _item);
    }
}
#[test]
pub fn test_item_discovery_callback() {
    let discovery = ItemDiscovery::default();
    let info = Rc::clone(&discovery.0);

    Builder::default()
        .header(concat!(
        env!("CARGO_MANIFEST_DIR"),
        "/tests/parse_callbacks/item_discovery_callback/header_item_discovery.h"
        ))
        .parse_callbacks(Box::new(discovery))
        .generate()
        .expect("TODO: panic message");

    let expected = ItemCache::from([
        (
            DiscoveredItemId::new(10),
            DiscoveredItem::Struct {
                original_name: Some("NamedStruct".to_string()),
                final_name: "NamedStruct".to_string(),
            },
        ),
        (
            DiscoveredItemId::new(11),
            DiscoveredItem::Alias {
                alias_name: "AliasOfNamedStruct".to_string(),
                alias_for: DiscoveredItemId::new(10),
            },
        ),
        (
            DiscoveredItemId::new(20),
            DiscoveredItem::Union {
                original_name: Some("NamedUnion".to_string()),
                final_name: "NamedUnion".to_string(),
            },
        ),
        (
            DiscoveredItemId::new(21),
            DiscoveredItem::Alias {
                alias_name: "AliasOfNamedUnion".to_string(),
                alias_for: DiscoveredItemId::new(20),
            },
        ),
        (
            DiscoveredItemId::new(30),
            DiscoveredItem::Struct {
                original_name: None,
                final_name: "_bindgen_ty_*".to_string(),
            },
        ),
        (
            DiscoveredItemId::new(40),
            DiscoveredItem::Union {
                original_name: None,
                final_name: "_bindgen_ty_*".to_string(),
            },
        ),
    ]);

    compare_item_caches(info.borrow().clone(), expected);
}

pub fn compare_item_caches(generated: ItemCache, expected: ItemCache) {
    // We can't use a simple Eq::eq comparison because of two reasons:
    // - anonymous structs/unions will have a final name generated by bindgen which may change
    //   if the header file or the bindgen logic is altered
    // - aliases have a DiscoveredItemId that we can't directly compare for the same instability reasons
    for expected_item in expected.values() {
        let found = generated.iter().find(|(_generated_id, generated_item)| {
            compare_item_info(
                expected_item,
                generated_item,
                &expected,
                &generated,
            )
        });

        if found.is_none() {
            panic!(
                "Missing Expected Item: {:#?}\n in {:#?}",
                expected_item, generated
            );
        }
    }
}

fn compare_item_info(
    expected_item: &DiscoveredItem,
    generated_item: &DiscoveredItem,
    expected: &ItemCache,
    generated: &ItemCache,
) -> bool {
    if std::mem::discriminant(expected_item) !=
        std::mem::discriminant(generated_item)
    {
        return false;
    }

    match generated_item {
        DiscoveredItem::Struct { .. } => {
            compare_struct_info(expected_item, generated_item)
        }
        DiscoveredItem::Union { .. } => {
            compare_union_info(expected_item, generated_item)
        }
        DiscoveredItem::Alias { .. } => compare_alias_info(
            expected_item,
            generated_item,
            expected,
            generated,
        ),
    }
}

pub fn compare_names(expected_name: &str, generated_name: &str) -> bool {
    if let Ok(regex) = Regex::new(expected_name) {
        regex.is_match(generated_name)
    } else {
        false
    }
}

pub fn compare_struct_info(
    expected_item: &DiscoveredItem,
    generated_item: &DiscoveredItem,
) -> bool {
    let DiscoveredItem::Struct {
        original_name: expected_original_name,
        final_name: expected_final_name,
    } = expected_item
    else {
        unreachable!()
    };

    let DiscoveredItem::Struct {
        original_name: generated_original_name,
        final_name: generated_final_name,
    } = generated_item
    else {
        unreachable!()
    };

    if !compare_names(expected_final_name, generated_final_name) {
        return false;
    }

    match (expected_original_name, generated_original_name) {
        (None, None) => true,
        (Some(expected_original_name), Some(generated_original_name)) => {
            compare_names(expected_original_name, generated_original_name)
        }
        _ => false,
    }
}

pub fn compare_union_info(
    expected_item: &DiscoveredItem,
    generated_item: &DiscoveredItem,
) -> bool {
    let DiscoveredItem::Union {
        original_name: expected_original_name,
        final_name: expected_final_name,
    } = expected_item
    else {
        unreachable!()
    };

    let DiscoveredItem::Union {
        original_name: generated_original_name,
        final_name: generated_final_name,
    } = generated_item
    else {
        unreachable!()
    };

    if !compare_names(expected_final_name, generated_final_name) {
        return false;
    }

    match (expected_original_name, generated_original_name) {
        (None, None) => true,
        (Some(expected_original_name), Some(generated_original_name)) => {
            compare_names(expected_original_name, generated_original_name)
        }
        _ => false,
    }
}

pub fn compare_alias_info(
    expected_item: &DiscoveredItem,
    generated_item: &DiscoveredItem,
    expected: &ItemCache,
    generated: &ItemCache,
) -> bool {
    let DiscoveredItem::Alias {
        alias_name: expected_alias_name,
        alias_for: expected_alias_for,
    } = expected_item
    else {
        unreachable!()
    };

    let DiscoveredItem::Alias {
        alias_name: generated_alias_name,
        alias_for: generated_alias_for,
    } = generated_item
    else {
        unreachable!()
    };

    if !compare_names(expected_alias_name, generated_alias_name) {
        return false;
    }

    // Assumes correct test definition
    let expected_aliased = expected.get(expected_alias_for).unwrap();

    // We must have the aliased type in the cache
    let generated_aliased =
        if let Some(generated_aliased) = generated.get(generated_alias_for) {
            generated_aliased
        } else {
            return false;
        };

    compare_item_info(expected_aliased, generated_aliased, expected, generated)
}
