use wgpu::{Adapter, Backends, Device, Features, Instance, Limits, Queue};

use crate::{report::AdapterReport, TestParameters};

/// Initialize the logger for the test runner.
pub fn init_logger() {
    // We don't actually care if it fails
    #[cfg(not(target_arch = "wasm32"))]
    let _ = env_logger::try_init();
    #[cfg(target_arch = "wasm32")]
    let _ = console_log::init_with_level(log::Level::Info);
}

/// Initialize a wgpu instance with the options from the environment.
pub fn initialize_instance(backends: wgpu::Backends, params: &TestParameters) -> Instance {
    // We ignore `WGPU_BACKEND` for now, merely using test filtering to only run a single backend's tests.
    //
    // We can potentially work support back into the test runner in the future, but as the adapters are matched up
    // based on adapter index, removing some backends messes up the indexes in annoying ways.
    //
    // WORKAROUND for https://github.com/rust-lang/cargo/issues/7160:
    // `--no-default-features` is not passed through correctly to the test runner.
    // We use it whenever we want to explicitly run with webgl instead of webgpu.
    // To "disable" webgpu regardless, we do this by removing the webgpu backend whenever we see
    // the webgl feature.
    let backends = if cfg!(feature = "webgl") {
        backends - wgpu::Backends::BROWSER_WEBGPU
    } else {
        backends
    };
    // Some tests need to be able to force demote to FXC, to specifically test workarounds for FXC
    // behavior.
    let dx12_shader_compiler = if params.force_fxc {
        wgpu::Dx12Compiler::Fxc
    } else {
        wgpu::Dx12Compiler::from_env().unwrap_or(wgpu::Dx12Compiler::StaticDxc)
    };
    // The defaults for debugging, overridden by the environment, overridden by the test parameters.
    let flags = wgpu::InstanceFlags::debugging()
        .with_env()
        .union(params.required_instance_flags);

    Instance::new(&wgpu::InstanceDescriptor {
        backends,
        flags,
        backend_options: wgpu::BackendOptions {
            dx12: wgpu::Dx12BackendOptions {
                shader_compiler: dx12_shader_compiler,
            },
            gl: wgpu::GlBackendOptions {
                fence_behavior: if cfg!(target_family = "wasm") {
                    // On WebGL, you cannot call Poll(Wait) with any timeout. This is because the
                    // browser does not things to block. However all of our tests are written to
                    // expect this behavior. This is the workaround to allow this to work.
                    //
                    // However on native you can wait, so we want to ensure that behavior as well.
                    wgpu::GlFenceBehavior::AutoFinish
                } else {
                    wgpu::GlFenceBehavior::Normal
                },
                ..Default::default()
            }
            .with_env(),
            // TODO(https://github.com/gfx-rs/wgpu/issues/7119): Enable noop backend?
            noop: wgpu::NoopBackendOptions::default(),
        },
    })
}

/// Initialize a wgpu adapter, using the given adapter report to match the adapter.
pub async fn initialize_adapter(
    adapter_report: Option<&AdapterReport>,
    params: &TestParameters,
) -> (Instance, Adapter, Option<SurfaceGuard>) {
    let backends = adapter_report
        .map(|report| Backends::from(report.info.backend))
        .unwrap_or_default();

    let instance = initialize_instance(backends, params);
    #[allow(unused_variables)]
    let surface: Option<wgpu::Surface>;
    let surface_guard: Option<SurfaceGuard>;

    #[allow(unused_assignments)]
    // Create a canvas if we need a WebGL2RenderingContext to have a working device.
    #[cfg(not(all(
        target_arch = "wasm32",
        any(target_os = "emscripten", feature = "webgl")
    )))]
    {
        surface = None;
        surface_guard = None;
    }
    #[cfg(all(
        target_arch = "wasm32",
        any(target_os = "emscripten", feature = "webgl")
    ))]
    {
        // On wasm, append a canvas to the document body for initializing the adapter
        let canvas = initialize_html_canvas();

        surface = Some(
            instance
                .create_surface(wgpu::SurfaceTarget::Canvas(canvas.clone()))
                .expect("could not create surface from canvas"),
        );

        surface_guard = Some(SurfaceGuard { canvas });
    }

    cfg_if::cfg_if! {
        if #[cfg(not(target_arch = "wasm32"))] {
            let adapter_iter = instance.enumerate_adapters(backends);
            let adapter = adapter_iter.into_iter()
                // If we have a report, we only want to match the adapter with the same info.
                //
                // If we don't have a report, we just take the first adapter.
                .find(|adapter| if let Some(adapter_report) = adapter_report {
                    adapter.get_info() == adapter_report.info
                } else {
                    true
                });
            let Some(adapter) = adapter else {
                panic!(
                    "Could not find adapter with info {:#?} in {:#?}",
                    adapter_report.map(|r| &r.info),
                    instance.enumerate_adapters(backends).into_iter().map(|a| a.get_info()).collect::<Vec<_>>(),
                );
            };
        } else {
            let adapter = instance.request_adapter(&wgpu::RequestAdapterOptions {
                compatible_surface: surface.as_ref(),
                ..Default::default()
            }).await.unwrap();
        }
    }

    log::info!("Testing using adapter: {:#?}", adapter.get_info());

    (instance, adapter, surface_guard)
}

/// Initialize a wgpu device from a given adapter.
pub async fn initialize_device(
    adapter: &Adapter,
    features: Features,
    limits: Limits,
) -> (Device, Queue) {
    let bundle = adapter
        .request_device(&wgpu::DeviceDescriptor {
            label: None,
            required_features: features,
            required_limits: limits,
            memory_hints: wgpu::MemoryHints::MemoryUsage,
            trace: wgpu::Trace::Off,
        })
        .await;

    match bundle {
        Ok(b) => b,
        Err(e) => panic!("Failed to initialize device: {e}"),
    }
}

/// Create a canvas for testing.
#[cfg(target_arch = "wasm32")]
pub fn initialize_html_canvas() -> web_sys::HtmlCanvasElement {
    use wasm_bindgen::JsCast;

    web_sys::window()
        .and_then(|win| win.document())
        .and_then(|doc| {
            let canvas = doc.create_element("Canvas").unwrap();
            canvas.dyn_into::<web_sys::HtmlCanvasElement>().ok()
        })
        .expect("couldn't create canvas")
}

pub struct SurfaceGuard {
    #[cfg(target_arch = "wasm32")]
    #[allow(unused)]
    canvas: web_sys::HtmlCanvasElement,
}

impl SurfaceGuard {
    #[cfg(all(
        target_arch = "wasm32",
        any(target_os = "emscripten", feature = "webgl")
    ))]
    pub(crate) fn check_for_unreported_errors(&self) -> bool {
        use wasm_bindgen::JsCast;

        self.canvas
            .get_context("webgl2")
            .unwrap()
            .unwrap()
            .dyn_into::<web_sys::WebGl2RenderingContext>()
            .unwrap()
            .get_error()
            != web_sys::WebGl2RenderingContext::NO_ERROR
    }
}
