// SPDX-License-Identifier: GPL-3.0-or-later
use anyhow::Result;
use gtk::{gio, glib, prelude::*, subclass::prelude::*};
pub use imp::QRCodeData;

use crate::qrcode_kind::QRCodeKind;

pub mod img {
    use qrcode::{QrCode, render::svg};

    use super::*;

    #[derive(Debug, Clone)]
    pub enum Format {
        Svg,
        Png,
    }

    fn into_bytes(content: &str, format: Format) -> Result<Vec<u8>> {
        let code = QrCode::new(content.as_bytes())?;

        match format {
            Format::Svg => Ok(code
                .render()
                .min_dimensions(250, 250)
                .dark_color(svg::Color("#000000"))
                .light_color(svg::Color("#ffffff"))
                .build()
                .into_bytes()),
            Format::Png => {
                let mut cursor = std::io::Cursor::new(vec![]);
                code.render::<image::Rgba<u8>>()
                    .min_dimensions(250, 250)
                    .build()
                    .write_to(&mut cursor, image::ImageFormat::Png)?;

                Ok(cursor.into_inner())
            }
        }
    }

    pub async fn save_as(content: &str, format: Format, output: &gio::File) -> Result<()> {
        let bytes = into_bytes(content, format)?;
        output
            .replace_contents_future(
                bytes,
                None,
                false,
                gio::FileCreateFlags::REPLACE_DESTINATION,
            )
            .await
            .map_err(|err| err.1)?;

        Ok(())
    }
}

pub mod db {
    #[derive(sqlx::FromRow, Hash, PartialEq, Eq, Debug, Clone)]
    pub struct Code {
        pub id: i32,
        pub content: String,
        pub created_at: chrono::NaiveDateTime,
    }
}

mod imp {
    use std::cell::{Cell, RefCell};

    use super::*;

    #[derive(Debug, Clone, glib::Boxed)]
    #[boxed_type(name = "QRCodeData")]
    pub struct QRCodeData {
        pub width: i32,
        pub height: i32,
        pub items: Vec<bool>,
    }

    impl TryFrom<&str> for QRCodeData {
        type Error = qrcode::types::QrError;

        fn try_from(data: &str) -> Result<Self, Self::Error> {
            let code = qrcode::QrCode::new(data.as_bytes())?;
            let width = code.width() as i32;
            let items = code
                .to_colors()
                .iter()
                .map(|color| matches!(color, qrcode::types::Color::Dark))
                .collect::<Vec<bool>>();

            Ok(Self {
                width,
                height: width,
                items,
            })
        }
    }

    #[derive(Debug, glib::Properties)]
    #[properties(wrapper_type = super::QRCode)]
    pub struct QRCode {
        #[property(get, set, construct_only)]
        id: Cell<i32>,
        #[property(get, set)]
        content: RefCell<String>,

        pub data: RefCell<Option<QRCodeData>>,
        pub kind: RefCell<Option<QRCodeKind>>,
        pub created_at: RefCell<chrono::DateTime<chrono::Utc>>,
    }

    #[glib::object_subclass]
    impl ObjectSubclass for QRCode {
        const NAME: &'static str = "QRCode";
        type ParentType = glib::Object;
        type Type = super::QRCode;

        fn new() -> Self {
            Self {
                id: Default::default(),
                content: Default::default(),
                data: Default::default(),
                kind: Default::default(),
                created_at: RefCell::new(chrono::Utc::now()),
            }
        }
    }

    #[glib::derived_properties]
    impl ObjectImpl for QRCode {}
}

glib::wrapper! {
    pub struct QRCode(ObjectSubclass<imp::QRCode>);
}

impl QRCode {
    pub fn new(
        id: i32,
        content: String,
        created_at: chrono::NaiveDateTime,
    ) -> Result<Self, qrcode::types::QrError> {
        let qr_code: QRCode = glib::Object::builder()
            .property("id", id)
            .property("content", &content)
            .build();
        let imp = qr_code.imp();
        let qrcode_data = imp::QRCodeData::try_from(content.as_str())?;
        imp.data.replace(Some(qrcode_data));
        let kind = QRCodeKind::from(content.as_str());
        imp.kind.replace(Some(kind));
        let time = chrono::DateTime::from_naive_utc_and_offset(created_at, chrono::Utc);
        imp.created_at.replace(time);

        Ok(qr_code)
    }

    pub async fn create(code_data: &str) -> Result<Self> {
        let mut conn = crate::database::connection().await?;

        sqlx::query(r#"INSERT INTO codes (content) VALUES ( ? )"#)
            .bind(code_data)
            .execute(&mut *conn)
            .await?;

        let code: db::Code = sqlx::query_as(r#"SELECT * FROM codes ORDER BY id DESC LIMIT 1"#)
            .fetch_one(&mut *conn)
            .await?;

        Ok(code.try_into()?)
    }

    pub async fn delete(&self) -> Result<()> {
        let mut conn = crate::database::connection().await?;

        sqlx::query("DELETE FROM codes WHERE id = ?")
            .bind(self.id())
            .execute(&mut *conn)
            .await?;

        Ok(())
    }

    pub async fn load() -> Result<impl Iterator<Item = Self>> {
        let mut conn = crate::database::connection().await?;

        let codes = sqlx::query_as::<_, db::Code>(r#"SELECT id, content, created_at FROM codes"#)
            .fetch_all(&mut *conn)
            .await?;

        Ok(codes
            .into_iter()
            .filter_map(|code| Self::try_from(code).ok()))
    }

    pub fn created_at(&self) -> chrono::DateTime<chrono::Utc> {
        *self.imp().created_at.borrow()
    }

    pub fn data(&self) -> QRCodeData {
        self.imp().data.borrow().as_ref().unwrap().clone()
    }

    pub fn kind(&self) -> QRCodeKind {
        self.imp().kind.borrow().as_ref().unwrap().clone()
    }

    // Returns the file chooser as the widget caller should hold a reference of it
}

impl TryFrom<db::Code> for QRCode {
    type Error = qrcode::types::QrError;

    fn try_from(code: db::Code) -> Result<Self, Self::Error> {
        Self::new(code.id, code.content, code.created_at)
    }
}
