use std::panic::AssertUnwindSafe;
use std::path::PathBuf;

use cushy::figures::units::Px;
use cushy::figures::Size;
use cushy::widget::MakeWidget;
use cushy::widgets::container::ContainerShadow;
use cushy::window::{AnimationRecorder, Rgba8, VirtualRecorder, VirtualRecorderBuilder};

pub struct ExampleBuilder {
    name: &'static str,
    recorder: VirtualRecorderBuilder<Rgba8>,
}

impl ExampleBuilder {
    #[must_use]
    pub fn finish(self) -> Example {
        Example {
            name: self.name,
            recorder: self.recorder.finish().expect("error creating recorder"),
        }
    }

    pub fn untested_still_frame(self) {
        self.finish().untested_still_frame();
    }

    pub fn prepare_with<Prepare>(self, prepare: Prepare) -> Example
    where
        Prepare: FnOnce(&mut VirtualRecorder<Rgba8>),
    {
        self.finish().prepare_with(prepare)
    }

    pub fn still_frame<Test>(self, test: Test)
    where
        Test: FnOnce(&mut VirtualRecorder<Rgba8>),
    {
        self.finish().still_frame(test);
    }

    pub fn animated<Test>(self, test: Test)
    where
        Test: FnOnce(&mut AnimationRecorder<'_, Rgba8>),
    {
        self.finish().animated(test);
    }
}

fn target_dir() -> PathBuf {
    let current_dir = std::env::current_dir().expect("missing current dir");
    let mut target_dir = current_dir.join("guide").join("src").join("examples");
    if !target_dir.is_dir() {
        target_dir = current_dir
            .parent()
            .expect("missing guide folder")
            .join("src")
            .join("examples");
    }
    assert!(
        target_dir.is_dir(),
        "current directory is not guide-examples or the root directory"
    );

    target_dir
}

pub struct Example {
    name: &'static str,
    recorder: VirtualRecorder<Rgba8>,
}

impl Example {
    pub fn build(
        name: &'static str,
        interface: impl MakeWidget,
        width: u16,
        height: Option<u16>,
    ) -> ExampleBuilder {
        let mut contents = interface
            .contain()
            .shadow(ContainerShadow::drop(Px::new(16)))
            .width(Px::new(i32::from(width)));
        if let Some(height) = height {
            contents = contents.height(Px::new(i32::from(height)));
        }
        ExampleBuilder {
            name,
            recorder: contents
                .build_recorder()
                .with_alpha()
                .resize_to_fit()
                .size(Size::new(
                    u32::from(width),
                    u32::from(height.unwrap_or(432)),
                )),
        }
    }

    pub fn untested_still_frame(self) {
        self.still_frame(|_| {});
    }

    #[must_use]
    pub fn prepare_with<Prepare>(mut self, prepare: Prepare) -> Self
    where
        Prepare: FnOnce(&mut VirtualRecorder<Rgba8>),
    {
        prepare(&mut self.recorder);
        self
    }

    pub fn still_frame<Test>(mut self, test: Test)
    where
        Test: FnOnce(&mut VirtualRecorder<Rgba8>),
    {
        let capture = std::env::var("CAPTURE").is_ok();
        let errored =
            std::panic::catch_unwind(AssertUnwindSafe(|| test(&mut self.recorder))).is_err();
        if errored || capture {
            let path = target_dir().join(format!("{}.png", self.name));
            self.recorder
                .image()
                .save(&path)
                .expect("error saving file");
            println!("Wrote {}", path.display());

            if errored {
                std::process::exit(-1);
            }
        }
    }

    pub fn animated<Test>(mut self, test: Test)
    where
        Test: FnOnce(&mut AnimationRecorder<'_, Rgba8>),
    {
        self.recorder.refresh().expect("error refreshing recorder");
        let mut animation = self.recorder.record_animated_png(60);
        let capture = std::env::var("CAPTURE").is_ok();
        let errored = std::panic::catch_unwind(AssertUnwindSafe(|| test(&mut animation))).is_err();
        if errored || capture {
            let path = target_dir().join(format!("{}.png", self.name));
            animation.write_to(&path).expect("error saving file");
            println!("Wrote {}", path.display());

            if errored {
                std::process::exit(-1);
            }
        }
    }
}

#[macro_export]
macro_rules! example {
    ($name:ident) => {
        $crate::example!($name, 750)
    };
    ($name:ident, $width:expr) => {
        $crate::example::Example::build(stringify!($name), $name(), $width, None)
    };
    ($name:ident, $width:expr, $height:expr) => {
        $crate::example::Example::build(stringify!($name), $name(), $width, Some($height))
    };
}
