use std::collections::HashMap;
use std::path::PathBuf;
use std::time::Duration;

use smithay::backend::renderer::element::{Element, Id};
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::backend::renderer::utils::CommitCounter;
use smithay::backend::renderer::{utils, Frame, ImportDma, Renderer};
use smithay::backend::winit::{
    self as winit_backend, WinitEvent, WinitEventLoop, WinitGraphicsBackend,
};
use smithay::output::{Mode, PhysicalProperties, Subpixel};
use smithay::reexports::calloop::timer::{TimeoutAction, Timer};
use smithay::reexports::calloop::{EventLoop, LoopHandle, RegistrationToken};
use smithay::utils::{Physical, Rectangle, Scale};
use tracing::{error, info};
use winit::dpi::LogicalSize;
use winit::window::Window as WinitWindow;

use crate::backend::Backend;
use crate::drawing::Graphics;
use crate::forge::Forge;
use crate::output::Output;
use crate::text::TextRenderer;
use crate::udev::RenderedFrame;
use crate::windows::Windows;

const CLEAR_COLOR: [f32; 4] = [0.0, 0.0, 0.0, 1.0];

pub fn run(size: (u32, u32), force_size: bool, exec_override: Option<PathBuf>) {
    let mut event_loop = EventLoop::try_new().expect("event loop");

    let (width, height) = size;

    // Capture the host compositor's WAYLAND_DISPLAY before `Forge::new`
    // (which calls `register_socket` and overwrites the env var with our
    // own nested socket name). Used below to push a `resize_app_id` IPC
    // back to the host when `--size` is set, so saved-geometry restores
    // for `app_id=forge` cannot override the requested size.
    let host_wayland_display = if force_size {
        std::env::var("WAYLAND_DISPLAY").ok()
    } else {
        None
    };

    use winit::platform::wayland::WindowAttributesExtWayland;
    let attributes = WinitWindow::default_attributes()
        .with_inner_size(LogicalSize::new(width as f64, height as f64))
        .with_title("Forge")
        .with_name("forge", "forge")
        .with_visible(true);

    let (graphics_backend, winit_event_loop): (WinitGraphicsBackend<GlesRenderer>, WinitEventLoop) =
        winit_backend::init_from_attributes(attributes).expect("winit backend");

    let window_size = graphics_backend.window_size();
    let mode = Mode {
        size: window_size,
        refresh: 60_000,
    };

    let winit_state = WinitBackend::new(event_loop.handle(), graphics_backend);
    let backend = Backend::Winit(winit_state);
    let mut forge = Forge::new(event_loop.handle(), backend, exec_override);

    let mut output = Output::new(
        &forge.display_handle,
        "winit-0".to_string(),
        mode,
        PhysicalProperties {
            size: (0, 0).into(),
            subpixel: Subpixel::Unknown,
            serial_number: "winit-0".into(),
            model: "Winit Window".into(),
            make: "Forge".into(),
        },
    );
    // Winit reports a 0×0 EDID size, so `compute_scale` always lands on 1.0.
    // `FORGE_SCALE` lets a test session force the output scale (e.g. =2) to
    // exercise HiDPI / buffer-scale paths the Librem 5 hits but a 1×
    // laptop never does.
    if let Some(scale) = std::env::var("FORGE_SCALE")
        .ok()
        .and_then(|s| s.parse::<f64>().ok())
        .filter(|s| *s > 0.0)
    {
        output.set_scale(scale);
    }
    forge.windows.set_output(output);

    // Set up dmabuf support from renderer formats
    if let Backend::Winit(ref mut w) = forge.backend {
        let renderer = w.graphics_backend.renderer();
        let dmabuf_formats = renderer.dmabuf_formats();
        forge
            .dmabuf_state
            .create_global::<Forge>(&forge.display_handle, dmabuf_formats);
    }

    forge.run_init_if_needed();

    // When `--size WxH` is explicit, force the host compositor to that
    // size on the first Resized event. By then the host has already
    // applied any saved geometry it had for our app_id ("forge"); the
    // standard `request_inner_size` is a hint that smithay does not
    // turn into a server configure, so we drive the host directly
    // through its IPC (`resize_app_id`) using the WAYLAND_DISPLAY we
    // captured before `Forge::new` mutated the env.
    let mut pending_force_size = force_size;
    let host_display_for_force = host_wayland_display.clone();
    event_loop
        .handle()
        .insert_source(winit_event_loop, move |event, _, forge: &mut Forge| match event {
            WinitEvent::Resized { size, .. } => {
                let mode = Mode { size, refresh: 60_000 };
                forge.windows.resize_output( mode );
                forge.force_redraw(true);
                if pending_force_size {
                    pending_force_size = false;
                    if let Some(host) = host_display_for_force.as_deref() {
                        let payload = format!(
                            r#"{{"action":"resize_app_id","app_id":"forge","w":{},"h":{}}}"#,
                            width, height
                        );
                        let _ = send_host_ipc(host, &payload);
                    }
                }
            },
            WinitEvent::Input(event) => forge.handle_input(event),
            WinitEvent::Redraw => forge.create_frame(),
            WinitEvent::CloseRequested => forge.terminated = true,
            WinitEvent::Focus(_) => {},
        })
        .expect("insert winit source");

    // Initial frame
    forge.create_frame();

    info!("Winit backend running ({}x{})", width, height);

    while !forge.terminated {
        // Adaptive timeout: when idle (stalled) we block until an event arrives
        // and only wake up on a 100 ms heartbeat to handle cursor blink / timers.
        // When animating we poll at 4 ms so the frame-pacer can hit 60+ fps.
        let timeout = if forge.is_stalled() {
            Some(Duration::from_millis(100))
        } else {
            Some(Duration::from_millis(4))
        };
        if let Err(e) = event_loop.dispatch(timeout, &mut forge) {
            error!("Event loop error: {e}");
            break;
        }
        forge.display_handle.flush_clients().expect("flushing clients");
        // Skip create_frame when stalled and no time-based work is pending.
        // Event handlers call unstall() → create_frame() themselves, so a
        // stalled loop iteration that processed no real events needs no render.
        // needs_heartbeat() keeps the 100 ms tick alive for OSD expiry checks.
        if !forge.is_stalled() || forge.needs_heartbeat() {
            forge.create_frame();
        }
    }

    info!("Compositor shutting down");
}

/// Connect to the host forge's IPC socket and write a single JSON line.
/// Used by the `--size` force path to ask the host to resize our window
/// after it has applied any saved geometry it had for `app_id=forge`.
fn send_host_ipc(host_display: &str, payload: &str) -> std::io::Result<()> {
    use std::io::Write;
    use std::os::unix::net::UnixStream;
    let runtime = std::env::var("XDG_RUNTIME_DIR")
        .unwrap_or_else(|_| std::env::temp_dir().to_string_lossy().into_owned());
    let path = format!("{}/forge-{}.sock", runtime, host_display);
    let mut s = UnixStream::connect(&path)?;
    s.write_all(payload.as_bytes())?;
    s.write_all(b"\n")?;
    Ok(())
}

pub struct WinitBackend {
    pub graphics_backend: WinitGraphicsBackend<GlesRenderer>,
    graphics: Graphics,
    event_loop: LoopHandle<'static, Forge>,
    scheduled_redraws: Vec<RegistrationToken>,
    element_states: HashMap<Id, (CommitCounter, Rectangle<i32, Physical>)>,
}

impl WinitBackend {
    fn new(
        event_loop: LoopHandle<'static, Forge>,
        graphics_backend: WinitGraphicsBackend<GlesRenderer>,
    ) -> Self {
        Self {
            graphics_backend,
            graphics: Graphics::new(),
            event_loop,
            scheduled_redraws: Vec::new(),
            element_states: HashMap::new(),
        }
    }

    pub fn renderer(&mut self) -> &mut GlesRenderer {
        self.graphics_backend.renderer()
    }

    pub fn render(
        &mut self,
        windows: &mut Windows,
        cursor_position: Option<crate::cursor::CursorRender>,
        text_renderer: &mut TextRenderer,
        mpris: &mut crate::mpris::MprisState,
        polkit: &mut crate::polkit::PolkitAgent,
        power_menu: &mut crate::powermenu::PowerMenu,
        osd: &mut crate::osd::Osd,
        topbar: &mut crate::topbar::Topbar,
        sys_state: &crate::sysmon::SystemState,
        app_switcher: &mut crate::app_switcher::AppSwitcher,
        screenshot: &mut crate::screenshot::ScreenshotSelector,
    ) -> RenderedFrame {
        let scale = windows.canvas().scale();
        let output_size = windows.canvas().physical_resolution();
        // Winit/OpenGL has inverted Y-axis; Flipped180 compensates
        let transform = smithay::utils::Transform::Flipped180;
        let output_rect = Rectangle::from_size(output_size);

        // Import textures using the renderer before bind
        let textures = {
            let renderer = self.graphics_backend.renderer();
            windows.textures(
                renderer,
                &mut self.graphics,
                cursor_position,
                text_renderer,
                mpris,
                polkit,
                power_menu,
                osd,
                topbar,
                sys_state,
                app_switcher,
                screenshot,
            )
        };

        // Compute per-element damage for submit, so the outer compositor
        // only copies regions that actually changed.
        let submit_damage = {
            let render_scale = Scale::from(scale);
            let mut rects: Vec<Rectangle<i32, Physical>> = Vec::new();
            let mut next: HashMap<Id, (CommitCounter, Rectangle<i32, Physical>)> =
                HashMap::with_capacity(textures.len());

            for el in textures.iter() {
                let id = el.id().clone();
                let commit = el.current_commit();
                let geo = el.geometry(render_scale);

                match self.element_states.remove(&id) {
                    Some((prev_commit, prev_geo)) if prev_geo == geo => {
                        // Same element, same position: collect element-local damage and
                        // offset to screen coordinates.
                        for r in el.damage_since(render_scale, Some(prev_commit)).iter() {
                            rects.push(Rectangle::new(geo.loc + r.loc, r.size));
                        }
                    },
                    Some((_, prev_geo)) => {
                        // Element moved or resized — dirty both old and new positions.
                        rects.push(prev_geo);
                        rects.push(geo);
                    },
                    None => {
                        // New element — full geometry is dirty.
                        rects.push(geo);
                    },
                }

                next.insert(id, (commit, geo));
            }

            // Elements that disappeared — mark their last position dirty.
            for (_, (_, old_geo)) in self.element_states.drain() {
                rects.push(old_geo);
            }
            self.element_states = next;

            // Clamp to output bounds. Fall back to full-screen if nothing
            // was tracked (e.g. first frame or all-unstable-ID elements).
            let clamped: Vec<Rectangle<i32, Physical>> =
                rects.into_iter().filter_map(|r| r.intersection(output_rect)).collect();
            if clamped.is_empty() { vec![output_rect] } else { clamped }
        };

        let result: Result<(), Box<dyn std::error::Error>> = (|| {
            {
                let (renderer, mut framebuffer) = self.graphics_backend.bind()?;
                let mut frame = renderer.render(&mut framebuffer, output_size, transform)?;
                frame.clear(CLEAR_COLOR.into(), &[output_rect])?;
                utils::draw_render_elements(&mut frame, scale, &textures, &[output_rect])?;
                let _ = frame.finish()?;
            }
            self.graphics_backend.submit(Some(&submit_damage))?;
            let _ = self.graphics_backend.renderer().cleanup_texture_cache();
            Ok(())
        })();

        if let Some(region) = windows.pending_screenshot.take() {
            let region_phys = region.to_physical_precise_round(scale);
            let renderer = self.graphics_backend.renderer();
            let png = {
                let clean = windows.textures(
                    renderer, &mut self.graphics, None, text_renderer, mpris, polkit, power_menu,
                    osd, topbar, sys_state, app_switcher, screenshot,
                );
                crate::screenshot::encode_region_png(renderer, output_size, scale, clean, region_phys)
            };
            match png {
                Ok(bytes) => windows.screenshot_result = Some(bytes),
                Err(e) => error!("screenshot capture failed: {e}"),
            }
        }

        match result {
            Ok(()) => RenderedFrame { rendered: true, gpu_fence: None },
            Err(e) => {
                error!("Winit render error: {e}");
                RenderedFrame { rendered: false, gpu_fence: None }
            },
        }
    }

    pub fn schedule_redraw(&mut self, duration: Duration) {
        let token = self
            .event_loop
            .insert_source(Timer::from_duration(duration), |_, _, forge| {
                forge.create_frame();
                TimeoutAction::Drop
            })
            .expect("insert render timer");
        self.scheduled_redraws.push(token);
    }

    pub fn cancel_scheduled_redraws(&mut self) {
        for token in self.scheduled_redraws.drain(..) {
            self.event_loop.remove(token);
        }
    }
}
