use std::cell::RefCell;
use std::collections::{HashMap, HashSet};
use std::ops::Range;

use devise::ext::PathExt;
use proc_macro2::{Span, TokenStream};
use syn::{parse::Parser, punctuated::Punctuated};

macro_rules! declare_lints {
    ($($name:ident ( $string:literal) ),* $(,)?) => (
        #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
        pub enum Lint {
            $($name),*
        }

        impl Lint {
            fn from_str(string: &str) -> Option<Self> {
                $(if string.eq_ignore_ascii_case($string) {
                    return Some(Lint::$name);
                })*

                None
            }

            fn as_str(&self) -> &'static str {
                match self {
                    $(Lint::$name => $string),*
                }
            }

            fn lints() -> &'static str {
                concat!("[" $(,$string,)", "* "]")
            }
        }
    )
}

declare_lints! {
    UnknownFormat("unknown_format"),
    DubiousPayload("dubious_payload"),
    SegmentChars("segment_chars"),
    ArbitraryMain("arbitrary_main"),
    SyncSpawn("sync_spawn"),
}

thread_local! {
    static SUPPRESSIONS: RefCell<HashMap<Lint, HashSet<Range<usize>>>> = RefCell::default();
}

fn span_to_range(span: Span) -> Option<Range<usize>> {
    let string = format!("{span:?}");
    let i = string.find('(')?;
    let j = string[i..].find(')')?;
    let (start, end) = string[(i + 1)..(i + j)].split_once("..")?;
    Some(Range { start: start.parse().ok()?, end: end.parse().ok()? })
}

impl Lint {
    pub fn suppress_attrs(attrs: &[syn::Attribute], ctxt: Span) {
        let _ = attrs.iter().try_for_each(|attr| Lint::suppress_attr(attr, ctxt));
    }

    pub fn suppress_attr(attr: &syn::Attribute, ctxt: Span) -> Result<(), syn::Error> {
        let syn::Meta::List(list) = &attr.meta else {
            return Ok(());
        };

        if !list.path.last_ident().map_or(false, |i| i == "suppress") {
            return Ok(());
        }

        Self::suppress_tokens(list.tokens.clone(), ctxt)
    }

    pub fn suppress_tokens(attr_tokens: TokenStream, ctxt: Span) -> Result<(), syn::Error> {
        let lints = Punctuated::<Lint, syn::Token![,]>::parse_terminated.parse2(attr_tokens)?;
        lints.iter().for_each(|lint| lint.suppress(ctxt));
        Ok(())
    }

    pub fn suppress(self, ctxt: Span) {
        SUPPRESSIONS.with_borrow_mut(|s| {
            let range = span_to_range(ctxt).unwrap_or_default();
            s.entry(self).or_default().insert(range);
        })
    }

    pub fn is_suppressed(self, ctxt: Span) -> bool {
        SUPPRESSIONS.with_borrow(|s| {
            let this = span_to_range(ctxt).unwrap_or_default();
            s.get(&self).map_or(false, |set| {
                set.iter().any(|r| this.start >= r.start && this.end <= r.end)
            })
        })
    }

    pub fn enabled(self, ctxt: Span) -> bool {
        !self.is_suppressed(ctxt)
    }

    pub fn how_to_suppress(self) -> String {
        format!("apply `#[suppress({})]` before the item to suppress this lint", self.as_str())
    }
}

impl syn::parse::Parse for Lint {
    fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result<Self> {
        let ident: syn::Ident = input.parse()?;
        let name = ident.to_string();
        Lint::from_str(&name).ok_or_else(|| {
            let msg = format!("invalid lint `{name}` (known lints: {})", Lint::lints());
            syn::Error::new(ident.span(), msg)
        })
    }
}
