// SPDX-License-Identifier: GPL-3.0-or-later
use std::sync::LazyLock;

use gettextrs::gettext;
use gtk::{gdk, glib, graphene, gsk, prelude::*, subclass::prelude::*};

use crate::qrcode::QRCodeData;

static INIT_QR_CODE: LazyLock<QRCodeData> =
    LazyLock::new(|| QRCodeData::try_from("Enjoy Decoder! :)").unwrap());

const DEFAULT_MAX_SIZE: i32 = 300;
const MIN_SIZE: i32 = 250;
const MIN_PADDING: i32 = 16;
const MAX_PADDING: i32 = 32;
const N_PADDING_SQUARES: i32 = 4;
const MIN_SQUARE_LENGTH: i32 = 4;

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

    use super::*;

    #[derive(Debug, glib::Properties)]
    #[properties(wrapper_type = super::QRCodeWidget)]
    pub struct QRCodeWidget {
        #[property(get, set = Self::set_max_size, explicit_notify)]
        pub max_size: Cell<u32>,

        pub qrcode: RefCell<Option<QRCodeData>>,
        pub texture: RefCell<gdk::Texture>,
    }

    impl Default for QRCodeWidget {
        fn default() -> Self {
            Self {
                max_size: Cell::new(DEFAULT_MAX_SIZE as u32),
                qrcode: RefCell::default(),
                texture: RefCell::new(texture(&INIT_QR_CODE)),
            }
        }
    }

    impl QRCodeWidget {
        fn set_max_size(&self, value: u32) {
            if value != self.max_size.replace(value) {
                self.obj().queue_resize();
                self.obj().notify_max_size();
            }
        }
    }

    #[glib::object_subclass]
    impl ObjectSubclass for QRCodeWidget {
        const NAME: &'static str = "QRCodeWidget";
        type Type = super::QRCodeWidget;
        type ParentType = gtk::Widget;
    }

    #[glib::derived_properties]
    impl ObjectImpl for QRCodeWidget {
        fn constructed(&self) {
            self.parent_constructed();

            let widget = self.obj();

            // Needed for .card class.
            widget.set_overflow(gtk::Overflow::Hidden);
            widget.set_accessible_role(gtk::AccessibleRole::Img);
            widget.update_property(&[gtk::accessible::Property::Label(&gettext("QR Code"))]);

            widget.connect_scale_factor_notify(WidgetExt::queue_draw);
        }
    }

    impl WidgetImpl for QRCodeWidget {
        fn measure(&self, _orientation: gtk::Orientation, _for_size: i32) -> (i32, i32, i32, i32) {
            let widget = self.obj();

            let max_size = widget.max_size() as f32;

            let nat_size = if let Some(ref qrcode) = *self.qrcode.borrow() {
                let square_length = widget.square_length(qrcode, max_size);
                let padding = padding(qrcode, max_size);

                // We don't need to clamp to max_size, but being explicit is
                // desired here.
                (square_length * qrcode.width + padding * 2).clamp(MIN_SIZE, max_size as i32)
            } else {
                MIN_SIZE
            };

            (nat_size, nat_size, -1, -1)
        }

        fn request_mode(&self) -> gtk::SizeRequestMode {
            gtk::SizeRequestMode::ConstantSize
        }

        fn snapshot(&self, snapshot: &gtk::Snapshot) {
            let widget = self.obj();

            let scale_factor = widget.scale_factor() as f32;
            let inv_scale_factor = 1.0 / scale_factor;

            // We scale up the image so it looks crisp on a factor factor of 2.
            snapshot.scale(inv_scale_factor, inv_scale_factor);

            let width = widget.width() as f32 * scale_factor;
            let height = widget.height() as f32 * scale_factor;

            if let Some(ref qrcode) = *self.qrcode.borrow() {
                widget.snapshot_qrcode(snapshot, qrcode, width, height);
            } else {
                widget.snapshot_qrcode(snapshot, &INIT_QR_CODE, width, height);
            }
        }
    }
}

glib::wrapper! {
    pub struct QRCodeWidget(ObjectSubclass<imp::QRCodeWidget>)
        @extends gtk::Widget,
        @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
}

impl QRCodeWidget {
    pub fn set_qrcode(&self, qrcode: QRCodeData) {
        let imp = self.imp();
        imp.texture.replace(texture(&qrcode));
        imp.qrcode.replace(Some(qrcode));
        self.queue_resize();
    }

    fn snapshot_qrcode(
        &self,
        snapshot: &gtk::Snapshot,
        qrcode: &QRCodeData,
        width: f32,
        height: f32,
    ) {
        let bounds = graphene::Rect::new(0.0, 0.0, width, height);
        snapshot.append_color(&gdk::RGBA::WHITE, &bounds);

        let square_length = self.square_length(qrcode, width);
        let t_length = (square_length * qrcode.width) as f32;
        let padding = ((width - t_length) / 2.0).floor();

        let rect = graphene::Rect::new(padding, padding, t_length, t_length);

        let texture = self.imp().texture.borrow();

        snapshot.append_scaled_texture(&*texture, gsk::ScalingFilter::Nearest, &rect);
    }

    // Compute an ideal square length under the following conditions:
    //
    //  - The QR code's length is no longer than the max-size property
    //  - The length is bigger than MIN_SQUARE_LENGTH if we can afford it
    //  - We require a padding of at last `N_PADDING_SQUARES` squares
    fn square_length(&self, qrcode: &QRCodeData, width: f32) -> i32 {
        let max_size = self.max_size() as i32;
        let mut length = MIN_SQUARE_LENGTH;
        for min in (1..=MIN_SQUARE_LENGTH).rev() {
            length = ((width as i32 - 2 * padding(qrcode, width)) / qrcode.width).max(min);
            if length * (qrcode.width + 2 * N_PADDING_SQUARES) > max_size {
                continue;
            } else {
                return length;
            }
        }
        length
    }
}

impl Default for QRCodeWidget {
    fn default() -> Self {
        glib::Object::new()
    }
}

// We check how many squares (taking into account minimal padding) can fit on
// the given width, then we clamp the padding between `MIN_PADDING` and
// `MAX_PADDING`.
fn padding(qrcode: &QRCodeData, width: f32) -> i32 {
    ((width as i32 / (qrcode.width + 2 * N_PADDING_SQUARES)) * MIN_SQUARE_LENGTH)
        .clamp(MIN_PADDING, MAX_PADDING)
}

fn texture(qrcode: &QRCodeData) -> gdk::Texture {
    let width = qrcode.width;

    const G8_SIZE: usize = 1;
    const WHITE: u8 = 0xff; // #ffffff
    const BLACK: u8 = 0x24; // #242424

    let bytes: Vec<u8> = qrcode
        .items
        .iter()
        .map(|is_black| if *is_black { BLACK } else { WHITE })
        .collect();

    let stride = G8_SIZE * width as usize;
    let bytes = glib::Bytes::from_owned(bytes);

    gdk::MemoryTexture::new(width, width, gdk::MemoryFormat::G8, &bytes, stride).upcast()
}
