use brisk_it::{
    Either, Required,
    generator::{Generator, Result},
};
use quote::ToTokens;

#[derive(Default)]
pub(super) struct Main {}

impl Generator for Main {
    fn generate(
        &self,
        input: brisk_it::component::ComponentInput,
        manager: &brisk_it::generator::Manager,
    ) -> Result<proc_macro2::TokenStream> {
        let mut options = Vec::<proc_macro2::TokenStream>::default();
        let mut extras = Vec::<proc_macro2::TokenStream>::default();
        let mut title: Option<syn::Expr> = None;
        let mut target: Option<syn::Expr> = None;
        let visibility = input.visibility;
        let mut function_name = syn::Ident::new("main", input.name.span());

        for prop in input.properties.into_iter() {
            if prop.name == "title" {
                title = Some(prop.expr)
            } else if prop.name == "target" {
                target = Some(prop.expr)
            } else if prop.name == "app" {
                match &prop.expr {
                    syn::Expr::Path(path) => {
                        if let Some(path) = path.path.get_ident() {
                            function_name = path.to_owned();
                        } else {
                            return Err(brisk_it::errors::unexpected(
                                &prop.expr,
                                "Expected an identifier.",
                            ));
                        }
                    }
                    o => return Err(brisk_it::errors::unexpected(o, "Expected an identifier.")),
                }
            } else if prop.name == "image_support" {
                let expr = prop.expr;
                extras.push(quote::quote! { if #expr {
                    // This gives us image support:
                    egui_extras::install_image_loaders(&cc.egui_ctx);
                }});
            } else if prop.name == "viewport" {
                let expr_ts: proc_macro::TokenStream = prop.expr.to_token_stream().into();
                let viewport_comp = syn::parse::<brisk_it::component::ComponentInput>(expr_ts);
                match viewport_comp {
                    Ok(viewport_comp) => {
                        let viewport = manager.generate(viewport_comp).either();
                        options.push(quote::quote! { viewport: #viewport });
                    }
                    Err(e) => return Err(e.to_compile_error()),
                }
            } else {
                return Err(brisk_it::errors::unknown_property(prop));
            }
        }

        let app = if target.is_some() {
            proc_macro2::TokenStream::new()
        } else {
            if input.children.len() != 1 {
                return Err(
                    syn::Error::new(input.name.span(), "Requires a single child.")
                        .into_compile_error(),
                );
            }
            let app_child = input.children.into_iter().next().unwrap();
            for prop in app_child.properties.iter() {
                if prop.name == "target" {
                    target = Some(prop.expr.to_owned());
                }
            }
            manager.generate(app_child).either()
        };

        let title = title.required(&input.name, "title")?;
        let target = target.required(&input.name, "target")?;

        let q = quote::quote! {
            use eframe::egui;
            #[cfg(not(target_arch = "wasm32"))]
            #visibility fn #function_name() -> eframe::Result {
                let options = eframe::NativeOptions {
                    #(#options,)*
                    ..Default::default()
                };
                eframe::run_native(
                    #title,
                    options,
                    Box::new(|cc| {
                        #(#extras)*
                        Ok(Box::new(#target::new(cc)))
                    }),
                )
            }
            #[cfg(target_arch = "wasm32")]
            fn main() {
                use eframe::wasm_bindgen::JsCast as _;

                // Redirect `log` message to `console.log` and friends:
                eframe::WebLogger::init(log::LevelFilter::Debug).ok();

                let web_options = eframe::WebOptions::default();

                wasm_bindgen_futures::spawn_local(async {
                    let document = web_sys::window()
                        .expect("No window")
                        .document()
                        .expect("No document");

                    let canvas = document
                        .get_element_by_id("the_canvas_id")
                        .expect("Failed to find the_canvas_id")
                        .dyn_into::<web_sys::HtmlCanvasElement>()
                        .expect("the_canvas_id was not a HtmlCanvasElement");

                    let start_result = eframe::WebRunner::new()
                        .start(
                            canvas,
                            web_options,
                            Box::new(|cc| Ok(Box::new(#target::new(cc)))),
                        )
                        .await;

                    // Remove the loading text and spinner:
                    if let Some(loading_text) = document.get_element_by_id("loading_text") {
                        match start_result {
                            Ok(_) => {
                                loading_text.remove();
                            }
                            Err(e) => {
                                loading_text.set_inner_html(
                                    "<p> The app has crashed. See the developer console for details. </p>",
                                );
                                panic!("Failed to start eframe: {e:?}");
                            }
                        }
                    }
                });
            }
            #app
        };
        Ok(q)
    }
    fn boxed_clone(&self) -> Box<dyn Generator> {
        Box::new(Self {})
    }
}
