use std::path::{Path, PathBuf};
use std::sync::Arc;

use bone_app::{AppCore, FrameTarget, InputEvent};
use bone_render::{PickIndex, Picker, SurfaceContext, ViewportExtent};
use bone_ui::gallery::{GALLERY_CANVAS, GalleryState, render};
use bone_ui::input::FrameInstant;
use bone_ui::raster::{PngError, encode_png, rasterize};
use bone_ui::strings::StringTable;
use bone_ui::theme::{Theme, ThemeMode};
use tracing_subscriber::EnvFilter;
use winit::{
    application::ApplicationHandler,
    event::WindowEvent,
    event_loop::{ActiveEventLoop, ControlFlow, EventLoop},
    window::{Window, WindowId},
};

mod event;
mod redraw;

#[derive(Debug, thiserror::Error)]
enum AppError {
    #[error("event loop: {0}")]
    EventLoop(#[from] winit::error::EventLoopError),
    #[error("missing argument for {flag}")]
    MissingArg { flag: &'static str },
    #[error("unknown argument: {0}")]
    UnknownArg(String),
    #[error("encode png ({mode}): {source}")]
    GalleryEncode {
        mode: ThemeMode,
        #[source]
        source: PngError,
    },
    #[error("write {path}: {source}")]
    GalleryWrite {
        path: PathBuf,
        #[source]
        source: std::io::Error,
    },
    #[error("create directory {path}: {source}")]
    GalleryDir {
        path: PathBuf,
        #[source]
        source: std::io::Error,
    },
}

const DEFAULT_LOG_FILTER: &str = "bone_app=info,bone_render=info,bone_document=info,bone_kernel=info,bone_types=info,wgpu_core=warn,wgpu_hal=warn";

struct WindowSurface {
    surface: SurfaceContext,
    window: Arc<Window>,
}

impl FrameTarget for WindowSurface {
    fn picker(&self, index: PickIndex) -> Picker<'_> {
        self.surface.picker(index)
    }

    fn render(
        &mut self,
        build_passes: impl FnOnce(
            &mut wgpu::CommandEncoder,
            &wgpu::TextureView,
            &wgpu::TextureView,
            &wgpu::TextureView,
        ),
    ) {
        let window = &self.window;
        self.surface
            .render(build_passes, || window.pre_present_notify());
    }
}

struct Running {
    scheduler: redraw::Scheduler,
    surface: WindowSurface,
    core: AppCore,
    epoch: std::time::Instant,
}

struct App {
    running: Option<Running>,
}

fn create_surface(
    event_loop: &ActiveEventLoop,
) -> Option<(Arc<Window>, SurfaceContext, ViewportExtent)> {
    let attrs = Window::default_attributes().with_title("bone");
    let window = match event_loop.create_window(attrs) {
        Ok(w) => Arc::new(w),
        Err(e) => {
            tracing::error!(error = %e, "create_window failed");
            event_loop.exit();
            return None;
        }
    };
    let extent = event::viewport_extent(window.inner_size());
    let surface = match pollster::block_on(SurfaceContext::new(window.clone(), extent)) {
        Ok(s) => s,
        Err(e) => {
            tracing::error!(error = %e, "SurfaceContext::new failed");
            event_loop.exit();
            return None;
        }
    };
    Some((window, surface, extent))
}

impl ApplicationHandler for App {
    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
        if self.running.is_some() {
            return;
        }
        let Some((window, surface, extent)) = create_surface(event_loop) else {
            return;
        };
        let core = match AppCore::new(surface.gpu(), surface.color_format(), extent) {
            Ok(core) => core,
            Err(e) => {
                tracing::error!(error = %e, "app core init failed");
                event_loop.exit();
                return;
            }
        };
        window.request_redraw();
        self.running = Some(Running {
            scheduler: redraw::Scheduler::new(Arc::clone(&window)),
            surface: WindowSurface { surface, window },
            core,
            epoch: std::time::Instant::now(),
        });
    }

    fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
        match event::AppEvent::from_winit(event) {
            event::AppEvent::Input(input) => {
                let Some(running) = self.running.as_mut() else {
                    return;
                };
                if let InputEvent::Resize(extent) = &input {
                    running.surface.surface.resize(*extent);
                }
                let ack = running.core.handle_input(&running.surface, input);
                running.scheduler.schedule(ack);
            }
            event::AppEvent::Redraw => self.dispatch_redraw(event_loop),
            event::AppEvent::Close => event_loop.exit(),
            event::AppEvent::Ignored => {}
        }
    }

    fn new_events(&mut self, _event_loop: &ActiveEventLoop, cause: winit::event::StartCause) {
        if matches!(cause, winit::event::StartCause::ResumeTimeReached { .. })
            && let Some(running) = self.running.as_mut()
        {
            running.scheduler.kick();
            running.scheduler.window().request_redraw();
        }
    }
}

impl App {
    fn dispatch_redraw(&mut self, event_loop: &ActiveEventLoop) {
        let Some(running) = self.running.as_mut() else {
            return;
        };
        running
            .core
            .clock_mut()
            .feed(FrameInstant::from_duration(running.epoch.elapsed()));
        let report = running.core.render_frame(&mut running.surface);
        if report.exit {
            event_loop.exit();
            return;
        }
        if report.kick {
            running.scheduler.kick();
        }
        running.scheduler.consume_kick();
        if let Some(deadline) = running.core.next_wake_deadline() {
            event_loop
                .set_control_flow(ControlFlow::WaitUntil(running.epoch + deadline.duration()));
        } else {
            event_loop.set_control_flow(ControlFlow::Wait);
        }
    }
}

enum RunMode {
    Window,
    Gallery(PathBuf),
}

fn parse_mode(args: impl IntoIterator<Item = String>) -> Result<RunMode, AppError> {
    let collected: Vec<String> = args.into_iter().skip(1).collect();
    parse_args(&collected, RunMode::Window)
}

fn parse_args(args: &[String], acc: RunMode) -> Result<RunMode, AppError> {
    match args {
        [] => Ok(acc),
        [first, second, rest @ ..] if first == "--gallery" => {
            parse_args(rest, RunMode::Gallery(PathBuf::from(second)))
        }
        [only] if only == "--gallery" => Err(AppError::MissingArg {
            flag: "--gallery <out-dir>",
        }),
        [unknown, ..] => Err(AppError::UnknownArg(unknown.clone())),
    }
}

fn emit_gallery(out_dir: &Path) -> Result<(), AppError> {
    std::fs::create_dir_all(out_dir).map_err(|e| AppError::GalleryDir {
        path: out_dir.to_path_buf(),
        source: e,
    })?;
    [
        (ThemeMode::Light, Theme::light()),
        (ThemeMode::Dark, Theme::dark()),
    ]
    .into_iter()
    .try_for_each(|(mode, theme)| -> Result<(), AppError> {
        let mut state = GalleryState::new();
        let paint = render(Arc::new(theme.clone()), &mut state);
        let rgba = rasterize(&theme, &paint, GALLERY_CANVAS, StringTable::empty());
        let png = encode_png(&rgba, GALLERY_CANVAS)
            .map_err(|source| AppError::GalleryEncode { mode, source })?;
        let target = out_dir.join(format!("gallery_{mode}.png"));
        std::fs::write(&target, &png).map_err(|source| AppError::GalleryWrite {
            path: target.clone(),
            source,
        })?;
        tracing::info!(path = %target.display(), "gallery png written");
        Ok(())
    })
}

fn main() -> Result<(), AppError> {
    tracing_subscriber::fmt()
        .with_env_filter(
            EnvFilter::try_from_default_env()
                .unwrap_or_else(|_| EnvFilter::new(DEFAULT_LOG_FILTER)),
        )
        .init();
    match parse_mode(std::env::args())? {
        RunMode::Gallery(dir) => emit_gallery(&dir),
        RunMode::Window => run_window(),
    }
}

fn run_window() -> Result<(), AppError> {
    let event_loop = EventLoop::new()?;
    event_loop.set_control_flow(ControlFlow::Wait);
    let mut app = App { running: None };
    event_loop.run_app(&mut app)?;
    Ok(())
}
