use std::any::{self, Any};
use std::hash::{Hash, Hasher};
use std::ops::Deref;
use std::sync::Arc;

use cairo_lang_diagnostics::Severity;
use cairo_lang_filesystem::cfg::CfgSet;
use cairo_lang_filesystem::db::Edition;
use cairo_lang_filesystem::ids::{CodeMapping, SmolStrId};
use cairo_lang_filesystem::span::TextWidth;
use cairo_lang_syntax::node::ids::SyntaxStablePtrId;
use cairo_lang_syntax::node::{SyntaxNode, ast};
use cairo_lang_utils::ordered_hash_set::OrderedHashSet;
use salsa::Database;
use serde::{Deserialize, Serialize};

/// A trait for arbitrary data that a macro generates along with the generated file.
#[typetag::serde]
pub trait GeneratedFileAuxData: std::fmt::Debug + Sync + Send {
    fn as_any(&self) -> &dyn Any;
    fn eq(&self, other: &dyn GeneratedFileAuxData) -> bool;
    /// Returns a hash value for this aux data. This is used to make the containing structs
    /// hashable. Implementations should ensure that equal objects have equal hash values.
    fn hash_value(&self) -> u64;
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DynGeneratedFileAuxData(pub Arc<dyn GeneratedFileAuxData>);
unsafe impl salsa::Update for DynGeneratedFileAuxData {
    unsafe fn maybe_update(old_pointer: *mut Self, new_value: Self) -> bool {
        let old_aux_data: &mut Self = unsafe { &mut *old_pointer };

        // Fast path: same allocation => unchanged.
        if Arc::ptr_eq(&old_aux_data.0, &new_value.0) {
            return false;
        }
        // Content-equal => unchanged.
        if GeneratedFileAuxData::eq(&*old_aux_data.0, &*new_value.0) {
            return false;
        }
        // Otherwise, replace the Arc.
        *old_aux_data = new_value;
        true
    }
}
impl DynGeneratedFileAuxData {
    pub fn new<T: GeneratedFileAuxData + 'static>(aux_data: T) -> Self {
        DynGeneratedFileAuxData(Arc::new(aux_data))
    }
}
impl Deref for DynGeneratedFileAuxData {
    type Target = Arc<dyn GeneratedFileAuxData>;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}
impl PartialEq for DynGeneratedFileAuxData {
    fn eq(&self, that: &DynGeneratedFileAuxData) -> bool {
        self.0.eq(&*that.0)
    }
}
impl Eq for DynGeneratedFileAuxData {}
impl Hash for DynGeneratedFileAuxData {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.0.hash_value().hash(state);
    }
}

/// Virtual code file generated by a plugin.
pub struct PluginGeneratedFile {
    /// Name for the virtual file. Will appear in diagnostics.
    pub name: String,
    /// Code content for the file.
    pub content: String,
    /// A code mapper, to allow more readable diagnostics that originate in plugin generated
    /// virtual files.
    pub code_mappings: Vec<CodeMapping>,
    /// Arbitrary data that the plugin generates along with the file.
    pub aux_data: Option<DynGeneratedFileAuxData>,
    /// Diagnostic note for the plugin generated file.
    /// This will be used as [`cairo_lang_diagnostics::DiagnosticNote`] on diagnostics originating
    /// from this file.
    pub diagnostics_note: Option<String>,
    /// This needs to be set to true if the plugin is unhygienic, i.e. it does not preserve
    /// variable hygiene.
    pub is_unhygienic: bool,
}

/// Result of plugin code generation.
#[derive(Default)]
pub struct PluginResult<'db> {
    /// Filename, content.
    pub code: Option<PluginGeneratedFile>,
    /// Diagnostics.
    pub diagnostics: Vec<PluginDiagnostic<'db>>,
    /// If true - the original item should be removed, if false - it should remain as is.
    pub remove_original_item: bool,
}

/// A diagnostic generated by a plugin.
#[derive(Clone, Debug, Eq, Hash, PartialEq, salsa::Update)]
pub struct PluginDiagnostic<'db> {
    /// The stable pointer of the syntax node that caused the diagnostic.
    pub stable_ptr: SyntaxStablePtrId<'db>,
    pub message: String,
    /// The severity of the diagnostic.
    pub severity: Severity,
    /// An optional inner span inside the stable pointer that caused the diagnostic. Useful for
    /// diagnostics caused by inline macros, since the syntax of the arguments is a token tree and
    /// is not segmented into each argument.
    /// The tuple is (offset, width).
    pub inner_span: Option<(TextWidth, TextWidth)>,
}
impl<'db> PluginDiagnostic<'db> {
    pub fn error(
        stable_ptr: impl Into<SyntaxStablePtrId<'db>>,
        message: String,
    ) -> PluginDiagnostic<'db> {
        PluginDiagnostic {
            stable_ptr: stable_ptr.into(),
            message,
            severity: Severity::Error,
            inner_span: None,
        }
    }

    /// Creates a diagnostic, pointing to an inner span inside the given stable pointer.
    pub fn error_with_inner_span(
        db: &dyn Database,
        stable_ptr: impl Into<SyntaxStablePtrId<'db>>,
        inner_span: SyntaxNode<'_>,
        message: String,
    ) -> PluginDiagnostic<'db> {
        let stable_ptr = stable_ptr.into();
        let offset = inner_span.offset(db) - stable_ptr.lookup(db).offset(db);
        let width = inner_span.width(db);
        PluginDiagnostic {
            stable_ptr,
            message,
            severity: Severity::Error,
            inner_span: Some((offset, width)),
        }
    }

    pub fn warning(
        stable_ptr: impl Into<SyntaxStablePtrId<'db>>,
        message: String,
    ) -> PluginDiagnostic<'db> {
        PluginDiagnostic {
            stable_ptr: stable_ptr.into(),
            message,
            severity: Severity::Warning,
            inner_span: None,
        }
    }
}

/// A structure containing additional info about the current module item on which macro plugin
/// operates.
pub struct MacroPluginMetadata<'a> {
    /// Config set of the crate to which the current item belongs.
    pub cfg_set: &'a CfgSet,
    /// The possible derives declared by any plugin.
    pub declared_derives: &'a OrderedHashSet<SmolStrId<'a>>,
    /// The allowed features at the macro activation site.
    pub allowed_features: &'a OrderedHashSet<SmolStrId<'a>>,
    /// The edition of the crate to which the current item belongs.
    pub edition: Edition,
}

// TODO(spapini): Move to another place.
/// A trait for a macro plugin: external plugin that generates additional code for items.
pub trait MacroPlugin: std::fmt::Debug + Sync + Send + Any {
    /// Generates code for an item. If no code should be generated returns None.
    /// Otherwise, returns `PluginResult` with the generated virtual submodule.
    fn generate_code<'db>(
        &self,
        db: &'db dyn Database,
        item_ast: ast::ModuleItem<'db>,
        metadata: &MacroPluginMetadata<'_>,
    ) -> PluginResult<'db>;

    /// Attributes this plugin uses.
    /// Attributes the plugin uses without declaring here are likely to cause a compilation error
    /// for unknown attribute.
    /// Note: They may not cause a diagnostic if some other plugin declares such attribute, but
    /// plugin writers should not rely on that.
    fn declared_attributes<'db>(&self, db: &'db dyn Database) -> Vec<SmolStrId<'db>>;

    /// Derives this plugin supplies.
    /// Any derived classes the plugin supplies without declaring here are likely to cause a
    /// compilation error for unknown derive.
    /// Note: They may not cause a diagnostic if some other plugin declares such derive, but
    /// plugin writers should not rely on that.
    fn declared_derives<'db>(&self, _db: &'db dyn Database) -> Vec<SmolStrId<'db>> {
        Vec::new()
    }

    /// Attributes that should mark the function as an executable.
    /// Functions marked with executable attributes will be listed
    /// in a dedicated field in the generated program.
    /// Must return a subset of `declared_attributes`.
    /// This mechanism is optional.
    fn executable_attributes<'db>(&self, _db: &'db dyn Database) -> Vec<SmolStrId<'db>> {
        Vec::new()
    }

    /// Attributes that mark a type as a phantom type. Must return a subset of
    /// `declared_attributes`.
    /// This mechanism is optional.
    fn phantom_type_attributes<'db>(&self, _db: &'db dyn Database) -> Vec<SmolStrId<'db>> {
        Vec::new()
    }

    /// A `TypeId` of the plugin, used to compare the concrete types
    /// of plugins given as trait objects.
    fn plugin_type_id(&self) -> any::TypeId {
        self.type_id()
    }
}

/// Result of plugin code generation.
#[derive(Default)]
pub struct InlinePluginResult<'db> {
    pub code: Option<PluginGeneratedFile>,
    /// Diagnostics.
    pub diagnostics: Vec<PluginDiagnostic<'db>>,
}

pub trait InlineMacroExprPlugin: std::fmt::Debug + Sync + Send + Any {
    /// Generates code for an item. If no code should be generated returns None.
    /// Otherwise, returns (virtual_module_name, module_content), and a virtual submodule
    /// with that name and content should be created.
    fn generate_code<'db>(
        &self,
        db: &'db dyn Database,
        item_ast: &ast::ExprInlineMacro<'db>,
        metadata: &MacroPluginMetadata<'_>,
    ) -> InlinePluginResult<'db>;

    /// Allows for the plugin to provide documentation for an inline macro.
    fn documentation(&self) -> Option<String> {
        None
    }

    /// A `TypeId` of the plugin, used to compare the concrete types
    /// of plugins given as trait objects.
    fn plugin_type_id(&self) -> any::TypeId {
        self.type_id()
    }
}

/// A trait for easier addition of macro plugins.
pub trait NamedPlugin: Default + 'static {
    const NAME: &'static str;
}
