use cfg_if::cfg_if;
use proc_macro2::TokenStream;
use quote::quote;

use super::{Body, Headers, MacroKind, ensure_feature_presence};
use crate::util::{RumaCommon, RumaCommonReexport};

mod incoming;
mod outgoing;
mod parse;

pub(crate) use self::parse::ResponseAttrs;

const KIND: MacroKind = MacroKind::Response;

/// Expand the `#[response]` macro on a struct.
///
/// This uses the `#[derive(Response)]` macro internally.
pub fn expand_response(attrs: ResponseAttrs, item: syn::ItemStruct) -> TokenStream {
    let ruma_common = RumaCommon::new();
    let ruma_macros = ruma_common.reexported(RumaCommonReexport::RumaMacros);

    let maybe_feature_error = ensure_feature_presence().map(syn::Error::to_compile_error);

    let error_ty = attrs.error_ty_or_default(&ruma_common);
    let status_ident = attrs.status_or_default();

    cfg_if! {
        // Make the macro expand the internal derives, such that Rust Analyzer's expand macro helper can
        // render their output. Requires a nightly toolchain.
        if #[cfg(feature = "__internal_macro_expand")] {
            use syn::parse_quote;

            let mut derive_input = item.clone();
            derive_input.attrs.push(parse_quote! {
                #[ruma_api(error = #error_ty, status = #status_ident)]
            });
            crate::util::cfg_expand_struct(&mut derive_input);

            let extra_derive = quote! { #ruma_macros::_FakeDeriveRumaApi };
            let ruma_api_attribute = quote! {};
            let response_impls =
                expand_derive_response(derive_input).unwrap_or_else(syn::Error::into_compile_error);
        } else {
            let extra_derive = quote! { #ruma_macros::Response };
            let ruma_api_attribute = quote! {
                #[ruma_api(error = #error_ty, status = #status_ident)]
            };
            let response_impls = quote! {};
        }
    }

    quote! {
        #maybe_feature_error

        #[derive(Clone, Debug, #ruma_common::serde::_FakeDeriveSerde, #extra_derive)]
        #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
        #ruma_api_attribute
        #item

        #response_impls
    }
}

/// Expand the `#[derive(Response)]` macro.
pub fn expand_derive_response(input: syn::ItemStruct) -> syn::Result<TokenStream> {
    let response = Response::try_from(input)?;

    Ok(response.expand_impls())
}

/// A parsed struct representing an API response.
struct Response {
    /// The name of the struct.
    ident: syn::Ident,

    /// The generics of the struct.
    generics: syn::Generics,

    /// The fields representing HTTP headers.
    headers: Headers,

    /// The fields representing the body of the response.
    body: Body,

    /// The type used for the `EndpointError` associated type on `OutgoingResponse` and
    /// `IncomingResponse` implementations.
    error_ty: syn::Type,

    /// The HTTP status code to use for the response.
    status: syn::Ident,
}

impl Response {
    /// Expand the implementations generated by this macro.
    fn expand_impls(&self) -> TokenStream {
        let ruma_common = RumaCommon::new();

        let response_body_serde_struct =
            self.body.expand_serde_struct_definition(KIND, &ruma_common);

        let outgoing_response_impl = self.expand_outgoing(&ruma_common);
        let incoming_response_impl = self.expand_incoming(&ruma_common);

        quote! {
            #response_body_serde_struct

            #outgoing_response_impl
            #incoming_response_impl
        }
    }
}
