pub use gstreamer as gst;
use gstreamer::{
    glib::object::Cast,
    prelude::{ElementExt, GstBinExtManual},
};
pub use gstreamer_app as gst_app;
pub use gstreamer_video as gst_video;
use image::RgbaImage;

use crate::renderer::types::Config;

// GStreamer video encoder
pub struct VideoEncoder {
    video_info: gst_video::VideoInfo,
    pipeline: gst::Pipeline,
    appsrc: gst_app::AppSrc,
    config: Config,
}

impl VideoEncoder {
    pub fn new(config: &Config, output_path: &str) -> Result<Self, Box<dyn std::error::Error>> {
        gst::init()?;

        let pipeline = gst::Pipeline::new();

        let video_info = gst_video::VideoInfo::builder(
            gst_video::VideoFormat::Rgba,
            config.width as u32,
            config.height as u32,
        )
        .fps(gst::Fraction::new(config.fps as i32, 1))
        .build()
        .expect("Failed to create video info");

        // Configure appsrc
        let appsrc = gst_app::AppSrc::builder()
            .caps(&video_info.to_caps()?)
            .property("stream-type", gst_app::AppStreamType::Stream)
            .property("format", gst::Format::Time)
            .build();

        // Create pipeline elements
        let videoconvert = gst::ElementFactory::make("videoconvert").build()?;

        // Configure encoder
        let x264enc = gst::ElementFactory::make("x264enc")
            .property("bitrate", 2000u32) // 2Mbps
            .property_from_str("speed-preset", "fast")
            .build()?;

        let mp4mux = gst::ElementFactory::make("mp4mux").build()?;
        let filesink = gst::ElementFactory::make("filesink")
            .property("location", output_path)
            .build()?;

        // Add elements to pipeline
        pipeline.add_many(&[
            appsrc.upcast_ref(),
            &videoconvert,
            &x264enc,
            &mp4mux,
            &filesink,
        ])?;

        // Link elements
        gst::Element::link_many(&[
            appsrc.upcast_ref(),
            &videoconvert,
            &x264enc,
            &mp4mux,
            &filesink,
        ])?;

        Ok(VideoEncoder {
            video_info,
            pipeline,
            appsrc,
            config: config.clone(),
        })
    }

    pub fn start(&self) -> Result<(), gst::StateChangeError> {
        self.pipeline.set_state(gst::State::Playing)?;
        Ok(())
    }

    pub fn push_frame(
        &self,
        image: &RgbaImage,
        millis: u64,
    ) -> Result<(), Box<dyn std::error::Error>> {
        let data = image.as_raw().clone();
        let buffer_size = data.len();

        let mut buffer = gst::Buffer::with_size(buffer_size)?;
        {
            buffer
                .get_mut()
                .unwrap()
                .set_pts(millis * gst::ClockTime::MSECOND);

            let buffer_ref = buffer.get_mut().unwrap();
            let mut buffer_map = buffer_ref.map_writable()?;
            buffer_map.copy_from_slice(&data);
        }

        self.appsrc.push_buffer(buffer)?;
        Ok(())
    }

    pub fn finish(&self) -> Result<(), Box<dyn std::error::Error>> {
        self.appsrc.end_of_stream()?;

        // Wait for EOS
        let bus = self.pipeline.bus().unwrap();
        for msg in bus.iter_timed(gst::ClockTime::NONE) {
            match msg.view() {
                gst::MessageView::Eos(..) => break,
                gst::MessageView::Error(err) => {
                    self.pipeline
                        .set_state(gst::State::Null)
                        .expect("Unable to set the pipeline to the `Null` state");
                    // return err.message().into();
                    return Err(format!("Error: {}", err.error()).into());
                }
                _ => (),
            }
        }

        self.pipeline
            .set_state(gst::State::Null)
            .expect("Unable to set the pipeline to the `Null` state");

        Ok(())
    }
}
