//! Auth-dialog presentation: texture caching, layout and hit-testing. Reads the
//! model state on [`PolkitAgent`]; holds no logic of its own.

use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Point, Rectangle, Size};

use crate::drawing::{ForgeElement, RenderTexture, Texture};
use crate::output::Canvas;
use crate::text::TextRenderer;

use super::{PolkitAction, PolkitAgent};

const TEXT_COLOR: [u8; 4] = [255, 255, 255, 255];
const LABEL_COLOR: [u8; 4] = [200, 200, 200, 255];

const DIALOG_WIDTH: i32 = 360;
const DIALOG_HEIGHT: i32 = 280;
const BUTTON_HEIGHT: i32 = 44;
const INPUT_HEIGHT: i32 = 40;
const PADDING: i32 = 16;

/// Dialog geometry, computed once so hit-testing and rendering can never
/// disagree about where the buttons are.
struct PolkitLayout {
    dialog: Rectangle<i32, Logical>,
    auth: Rectangle<i32, Logical>,
    cancel: Rectangle<i32, Logical>,
}

impl PolkitLayout {
    fn compute(canvas: &Canvas) -> Self {
        let size = canvas.size();
        let dw = if canvas.is_mobile() { size.w - 32 } else { DIALOG_WIDTH };
        let dx = (size.w - dw) / 2;
        let dy = (size.h - DIALOG_HEIGHT) / 2;
        let auth_y = dy + DIALOG_HEIGHT - BUTTON_HEIGHT - PADDING;
        let half_w = (dw - 3 * PADDING) / 2;
        Self {
            dialog: Rectangle::new(Point::from((dx, dy)), Size::from((dw, DIALOG_HEIGHT))),
            auth: Rectangle::new(Point::from((dx + PADDING, auth_y)), Size::from((half_w, BUTTON_HEIGHT))),
            cancel: Rectangle::new(Point::from((dx + dw / 2 + PADDING / 2, auth_y)), Size::from((half_w, BUTTON_HEIGHT))),
        }
    }
}

impl PolkitAgent {
    pub fn handle_tap(&self, canvas: &Canvas, point: Point<f64, Logical>) -> Option<PolkitAction> {
        if !self.visible || self.authenticating {
            return None;
        }

        let l = PolkitLayout::compute(canvas);
        if l.auth.to_f64().contains(point) {
            return Some(PolkitAction::Authenticate);
        }
        if l.cancel.to_f64().contains(point) {
            return Some(PolkitAction::Cancel);
        }

        Some(PolkitAction::None)
    }

    fn update_textures(
        &mut self,
        renderer: &mut GlesRenderer,
        canvas: &Canvas,
        text_renderer: &mut TextRenderer,
    ) {
        let scale = canvas.scale();
        if !self.dirty && (self.last_scale - scale).abs() < 0.01 {
            return;
        }
        self.dirty = false;
        self.last_scale = scale;

        let title_font = crate::text::text_px(16.0, scale).round();
        let msg_font = crate::text::text_px(12.0, scale).round();
        let btn_font = crate::text::text_px(14.0, scale).round();
        let dot_font = crate::text::text_px(18.0, scale).round();

        // Title
        let (buf, w, h) = text_renderer.render_line("Authentication Required", title_font, TEXT_COLOR);
        if w > 0 && h > 0 {
            self.title_texture = Some(RenderTexture::new(
                Texture::from_buffer(renderer, scale, &buf, w as i32, h as i32, false),
            ));
        }

        // Message from the frontmost polkit request
        if let Some(req) = self.queue.front() {
            let (buf, w, h) = text_renderer.render_line(&req.message, msg_font, LABEL_COLOR);
            if w > 0 && h > 0 {
                self.message_texture = Some(RenderTexture::new(
                    Texture::from_buffer(renderer, scale, &buf, w as i32, h as i32, false),
                ));
            }
        }

        // Input field content: status while verifying, dots otherwise
        if self.authenticating {
            let (buf, w, h) = text_renderer.render_line("Authenticating…", dot_font, LABEL_COLOR);
            self.dots_texture = (w > 0 && h > 0).then(|| RenderTexture::new(
                Texture::from_buffer(renderer, scale, &buf, w as i32, h as i32, false),
            ));
        } else if !self.password_buffer.is_empty() {
            let dots: String = "* ".repeat(self.password_buffer.len()).trim_end().to_string();
            let (buf, w, h) = text_renderer.render_line(&dots, dot_font, TEXT_COLOR);
            self.dots_texture = (w > 0 && h > 0).then(|| RenderTexture::new(
                Texture::from_buffer(renderer, scale, &buf, w as i32, h as i32, false),
            ));
        } else {
            self.dots_texture = None;
        }

        // Error
        if let Some(err) = &self.error {
            let (buf, w, h) = text_renderer.render_line(err, msg_font, [255, 80, 80, 255]);
            if w > 0 && h > 0 {
                self.error_texture = Some(RenderTexture::new(
                    Texture::from_buffer(renderer, scale, &buf, w as i32, h as i32, false),
                ));
            }
        } else {
            self.error_texture = None;
        }

        // Button labels
        let (buf, w, h) = text_renderer.render_line("Authenticate", btn_font, TEXT_COLOR);
        if w > 0 && h > 0 {
            self.auth_label = Some(RenderTexture::new(
                Texture::from_buffer(renderer, scale, &buf, w as i32, h as i32, false),
            ));
        }
        let (buf, w, h) = text_renderer.render_line("Cancel", btn_font, TEXT_COLOR);
        if w > 0 && h > 0 {
            self.cancel_label = Some(RenderTexture::new(
                Texture::from_buffer(renderer, scale, &buf, w as i32, h as i32, false),
            ));
        }
    }

    pub fn textures(
        &mut self,
        textures: &mut Vec<ForgeElement>,
        canvas: &Canvas,
        renderer: &mut GlesRenderer,
        text_renderer: &mut TextRenderer,
    ) {
        if !self.visible {
            return;
        }

        self.update_textures(renderer, canvas, text_renderer);

        let scale = canvas.scale();
        let size = canvas.size();

        let l = PolkitLayout::compute(canvas);
        let (dx, dy, dw) = (l.dialog.loc.x, l.dialog.loc.y, l.dialog.size.w);
        let auth_y = l.auth.loc.y;
        let half_w = l.auth.size.w;

        // The element list is front-to-back: text and dots first, then the
        // input field and buttons, then the dialog panel, and the
        // screen-dimming overlay behind it all.
        let mut text_y = dy + PADDING;

        // Title
        if let Some(title) = &self.title_texture {
            let ts = title.size();
            let tx = dx + (dw - ts.w) / 2;
            let rect: Rectangle<i32, Logical> = Rectangle::new(Point::from((tx, text_y)), ts);
            let physical = rect.to_physical_precise_round(scale);
            ForgeElement::add_element(textures, title.clone(), physical.loc, physical, None, scale);
            text_y += ts.h + 12;
        }

        // Message
        if let Some(msg) = &self.message_texture {
            let ms = msg.size();
            let mx = dx + PADDING;
            let rect: Rectangle<i32, Logical> = Rectangle::new(Point::from((mx, text_y)), ms);
            let physical = rect.to_physical_precise_round(scale);
            ForgeElement::add_element(textures, msg.clone(), physical.loc, physical, None, scale);
            text_y += ms.h + 16;
        }

        // Password input field: dots first (in front of the field fill)
        let input_rect: Rectangle<i32, Logical> = Rectangle::new(
            Point::from((dx + PADDING, text_y)),
            Size::from((dw - 2 * PADDING, INPUT_HEIGHT)),
        );
        if let Some(dots) = &self.dots_texture {
            let ds = dots.size();
            let dots_x = dx + PADDING + 8;
            let dots_y = text_y + (INPUT_HEIGHT - ds.h) / 2;
            let rect: Rectangle<i32, Logical> = Rectangle::new(Point::from((dots_x, dots_y)), ds);
            let physical = rect.to_physical_precise_round(scale);
            ForgeElement::add_element(textures, dots.clone(), physical.loc, physical, None, scale);
        }

        text_y += INPUT_HEIGHT + 8;

        // Error message
        if let Some(err) = &self.error_texture {
            let es = err.size();
            let ex = dx + PADDING;
            let rect: Rectangle<i32, Logical> = Rectangle::new(Point::from((ex, text_y)), es);
            let physical = rect.to_physical_precise_round(scale);
            ForgeElement::add_element(textures, err.clone(), physical.loc, physical, None, scale);
        }

        // Button labels
        if let Some(label) = &self.auth_label {
            let ls = label.size();
            let lx = dx + PADDING + (half_w - ls.w) / 2;
            let ly = auth_y + (BUTTON_HEIGHT - ls.h) / 2;
            let rect: Rectangle<i32, Logical> = Rectangle::new(Point::from((lx, ly)), ls);
            let physical = rect.to_physical_precise_round(scale);
            ForgeElement::add_element(textures, label.clone(), physical.loc, physical, None, scale);
        }

        if let Some(label) = &self.cancel_label {
            let ls = label.size();
            let lx = dx + dw / 2 + PADDING / 2 + (half_w - ls.w) / 2;
            let ly = auth_y + (BUTTON_HEIGHT - ls.h) / 2;
            let rect: Rectangle<i32, Logical> = Rectangle::new(Point::from((lx, ly)), ls);
            let physical = rect.to_physical_precise_round(scale);
            ForgeElement::add_element(textures, label.clone(), physical.loc, physical, None, scale);
        }

        // Field and button fills
        let physical_input = input_rect.to_physical_precise_round(scale);
        ForgeElement::add_element(textures, self.input_bg.clone(), physical_input.loc, physical_input, None, scale);

        let physical_auth = l.auth.to_physical_precise_round(scale);
        ForgeElement::add_element(textures, self.button_bg.clone(), physical_auth.loc, physical_auth, None, scale);

        let physical_cancel = l.cancel.to_physical_precise_round(scale);
        ForgeElement::add_element(textures, self.cancel_bg.clone(), physical_cancel.loc, physical_cancel, None, scale);

        // Dialog box
        let physical_dialog = l.dialog.to_physical_precise_round(scale);
        ForgeElement::add_element(textures, self.dialog_bg.clone(), physical_dialog.loc, physical_dialog, None, scale);

        // Overlay
        let overlay_rect: Rectangle<i32, Logical> = Rectangle::new(Point::from((0, 0)), size);
        let physical_overlay = overlay_rect.to_physical_precise_round(scale);
        ForgeElement::add_element(textures, self.overlay_bg.clone(), physical_overlay.loc, physical_overlay, None, scale);
    }
}
