pub mod contents;
pub mod context;
pub mod media;
pub mod options;
pub mod reply_description;

use crate::{app_images, secondary_label};
use crate::{widgets::x_button, ProfilePic, ProfilePreview, PulseAlpha, Username};

pub use contents::{render_note_preview, NoteContents};
pub use context::NoteContextButton;
use notedeck::note::{reaction_sent_id, ZapTargetAmount};
use notedeck::ui::is_narrow;
use notedeck::Accounts;
use notedeck::GlobalWallet;
use notedeck::Images;
use notedeck::Localization;
use notedeck::MediaAction;
use notedeck::{get_current_wallet, MediaJobSender};
pub use options::NoteOptions;
pub use reply_description::reply_desc;

use egui::emath::{pos2, Vec2};
use egui::{Id, Pos2, Rect, Response, Sense};
use enostr::{KeypairUnowned, NoteId, Pubkey};
use nostrdb::{Ndb, Note, NoteKey, NoteMetadataEntryVariant, ProfileRecord, Transaction};
use notedeck::{
    note::{NoteAction, NoteContext, NoteDetail, ReactAction, ZapAction},
    tr, AnyZapState, ContextSelection, NoteZapTarget, NoteZapTargetOwned, ZapTarget, Zaps,
};

pub struct NoteView<'a, 'd> {
    note_context: &'a mut NoteContext<'d>,
    parent: Option<NoteKey>,
    note: &'a nostrdb::Note<'a>,
    flags: NoteOptions,
}

pub struct NoteResponse {
    pub response: egui::Response,
    pub action: Option<NoteAction>,
    pub pfp_rect: Option<egui::Rect>,
}

impl NoteResponse {
    pub fn new(response: egui::Response) -> Self {
        Self {
            response,
            action: None,
            pfp_rect: None,
        }
    }

    pub fn with_action(mut self, action: Option<NoteAction>) -> Self {
        self.action = action;
        self
    }

    pub fn with_pfp(mut self, pfp_rect: egui::Rect) -> Self {
        self.pfp_rect = Some(pfp_rect);
        self
    }
}

/*
impl View for NoteView<'_, '_> {
    fn ui(&mut self, ui: &mut egui::Ui) {
        self.show(ui);
    }
}
*/

impl egui::Widget for &mut NoteView<'_, '_> {
    fn ui(self, ui: &mut egui::Ui) -> egui::Response {
        self.show(ui).response
    }
}

impl<'a, 'd> NoteView<'a, 'd> {
    pub fn new(
        note_context: &'a mut NoteContext<'d>,
        note: &'a nostrdb::Note<'a>,
        flags: NoteOptions,
    ) -> Self {
        let parent: Option<NoteKey> = None;

        Self {
            note_context,
            parent,
            note,
            flags,
        }
    }

    pub fn preview_style(self) -> Self {
        self.actionbar(false)
            .small_pfp(true)
            .frame(true)
            .wide(true)
            .note_previews(false)
            .options_button(true)
            .is_preview(true)
            .full_date(false)
            .client_name(false)
    }

    pub fn selected_style(self, selected: bool) -> Self {
        self.wide(selected)
            .full_date(selected)
            .client_name(selected)
    }

    #[inline]
    pub fn textmode(mut self, enable: bool) -> Self {
        self.options_mut().set(NoteOptions::Textmode, enable);
        self
    }

    #[inline]
    pub fn client_name(mut self, enable: bool) -> Self {
        self.options_mut().set(NoteOptions::ClientName, enable);
        self
    }

    #[inline]
    pub fn full_date(mut self, enable: bool) -> Self {
        self.options_mut().set(NoteOptions::FullCreatedDate, enable);
        self
    }

    #[inline]
    pub fn actionbar(mut self, enable: bool) -> Self {
        self.options_mut().set(NoteOptions::ActionBar, enable);
        self
    }

    #[inline]
    pub fn hide_media(mut self, enable: bool) -> Self {
        self.options_mut().set(NoteOptions::HideMedia, enable);
        self
    }

    #[inline]
    pub fn frame(mut self, enable: bool) -> Self {
        self.options_mut().set(NoteOptions::Framed, enable);
        self
    }

    #[inline]
    pub fn truncate(mut self, enable: bool) -> Self {
        self.options_mut().set(NoteOptions::Truncate, enable);
        self
    }

    #[inline]
    pub fn small_pfp(mut self, enable: bool) -> Self {
        self.options_mut().set(NoteOptions::SmallPfp, enable);
        self
    }

    #[inline]
    pub fn medium_pfp(mut self, enable: bool) -> Self {
        self.options_mut().set(NoteOptions::MediumPfp, enable);
        self
    }

    #[inline]
    pub fn note_previews(mut self, enable: bool) -> Self {
        self.options_mut().set(NoteOptions::HasNotePreviews, enable);
        self
    }

    #[inline]
    pub fn selectable_text(mut self, enable: bool) -> Self {
        self.options_mut().set(NoteOptions::SelectableText, enable);
        self
    }

    #[inline]
    pub fn wide(mut self, enable: bool) -> Self {
        self.options_mut().set(NoteOptions::Wide, enable);
        self
    }

    #[inline]
    pub fn options_button(mut self, enable: bool) -> Self {
        self.options_mut().set(NoteOptions::OptionsButton, enable);
        self
    }

    #[inline]
    pub fn unread_indicator(mut self, enable: bool) -> Self {
        self.options_mut().set(NoteOptions::UnreadIndicator, enable);
        self
    }

    #[inline]
    pub fn detailed_counts(mut self, enable: bool) -> Self {
        self.options_mut().set(NoteOptions::DetailedCounts, enable);
        self
    }

    #[inline]
    pub fn options(&self) -> NoteOptions {
        self.flags
    }

    #[inline]
    pub fn options_mut(&mut self) -> &mut NoteOptions {
        &mut self.flags
    }

    #[inline]
    pub fn parent(mut self, parent: NoteKey) -> Self {
        self.parent = Some(parent);
        self
    }

    #[inline]
    pub fn is_preview(mut self, is_preview: bool) -> Self {
        self.options_mut().set(NoteOptions::IsPreview, is_preview);
        self
    }

    fn textmode_ui(&mut self, ui: &mut egui::Ui) -> egui::Response {
        let txn = self.note.txn().expect("todo: implement non-db notes");

        ui.with_layout(egui::Layout::left_to_right(egui::Align::TOP), |ui| {
            let profile = self
                .note_context
                .ndb
                .get_profile_by_pubkey(txn, self.note.pubkey());

            //ui.horizontal(|ui| {
            ui.spacing_mut().item_spacing.x = 2.0;

            let (_id, rect) = ui.allocate_space(egui::vec2(50.0, 20.0));
            ui.allocate_rect(rect, Sense::hover());
            ui.put(rect, |ui: &mut egui::Ui| {
                render_notetime(ui, self.note_context.i18n, self.note.created_at(), false)
            });
            let (_id, rect) = ui.allocate_space(egui::vec2(150.0, 20.0));
            ui.allocate_rect(rect, Sense::hover());
            ui.put(rect, |ui: &mut egui::Ui| {
                ui.add(
                    Username::new(
                        self.note_context.i18n,
                        profile.as_ref().ok(),
                        self.note.pubkey(),
                    )
                    .abbreviated(6)
                    .pk_colored(true),
                )
            });

            ui.add(&mut NoteContents::new(
                self.note_context,
                txn,
                self.note,
                self.flags,
            ));
            //});
        })
        .response
    }

    pub fn expand_size() -> i8 {
        5
    }

    fn pfp(
        &mut self,
        note_key: NoteKey,
        profile: &Result<nostrdb::ProfileRecord<'_>, nostrdb::Error>,
        ui: &mut egui::Ui,
    ) -> PfpResponse {
        if !self.options().contains(NoteOptions::Wide) {
            ui.spacing_mut().item_spacing.x = 16.0;
        } else {
            ui.spacing_mut().item_spacing.x = 4.0;
        }

        let pfp_size = self.options().pfp_size();

        match profile
            .as_ref()
            .ok()
            .and_then(|p| p.record().profile()?.picture())
        {
            // these have different lifetimes and types,
            // so the calls must be separate
            Some(pic) => show_actual_pfp(
                ui,
                self.note_context.img_cache,
                self.note_context.jobs,
                pic,
                pfp_size,
                note_key,
                profile,
            ),

            None => show_fallback_pfp(
                ui,
                self.note_context.img_cache,
                self.note_context.jobs,
                pfp_size,
            ),
        }
    }

    pub fn show(&mut self, ui: &mut egui::Ui) -> NoteResponse {
        if !self.flags.contains(NoteOptions::TrustMedia) {
            let acc = self.note_context.accounts.get_selected_account();
            if self.note.pubkey() == acc.key.pubkey.bytes()
                || matches!(
                    acc.is_following(self.note.pubkey()),
                    notedeck::IsFollowing::Yes
                )
            {
                self.flags = self.flags.union(NoteOptions::TrustMedia);
            }
        }

        if self.options().contains(NoteOptions::Textmode) {
            NoteResponse::new(self.textmode_ui(ui))
        } else if self.options().contains(NoteOptions::Framed) {
            egui::Frame::new()
                .fill(ui.visuals().noninteractive().weak_bg_fill)
                .inner_margin(egui::Margin::same(notedeck::tokens::SPACING_SM as i8))
                .outer_margin(egui::Margin::symmetric(
                    0,
                    notedeck::tokens::SPACING_SM as i8,
                ))
                .corner_radius(egui::CornerRadius::same(notedeck::tokens::RADIUS_LG as u8))
                .stroke(egui::Stroke::new(
                    notedeck::tokens::STROKE_THIN,
                    ui.visuals().noninteractive().bg_stroke.color,
                ))
                .show(ui, |ui| {
                    if is_narrow(ui.ctx()) {
                        ui.set_width(ui.available_width());
                    }
                    self.show_standard(ui)
                })
                .inner
        } else {
            self.show_standard(ui)
        }
    }

    #[profiling::function]
    fn note_header(
        ui: &mut egui::Ui,
        txn: &Transaction,
        note_context: &mut NoteContext,
        note: &Note,
        profile: &Result<nostrdb::ProfileRecord<'_>, nostrdb::Error>,
        flags: NoteOptions,
    ) {
        let horiz_resp = ui
            .horizontal_wrapped(|ui| {
                ui.spacing_mut().item_spacing.x = if is_narrow(ui.ctx()) { 1.0 } else { 2.0 };
                let response = ui.add(
                    Username::new(note_context.i18n, profile.as_ref().ok(), note.pubkey())
                        .abbreviated(20),
                );
                if !flags.contains(NoteOptions::FullCreatedDate) {
                    return render_notetime(ui, note_context.i18n, note.created_at(), true);
                }
                response
            })
            .response;

        if flags.contains(NoteOptions::UnreadIndicator) {
            let radius = 4.0;
            let circle_center = {
                let mut center = horiz_resp.rect.right_center();
                center.x += radius + 4.0;
                center
            };

            ui.painter()
                .circle_filled(circle_center, radius, crate::colors::PINK);
        }

        if note.is_rumor() {
            ui.horizontal_wrapped(|ui| {
                ui.spacing_mut().item_spacing.x = if is_narrow(ui.ctx()) { 1.0 } else { 2.0 };

                secondary_label(ui, "encrypted privately to");

                crate::Mention::new(
                    note_context.ndb,
                    note_context.img_cache,
                    note_context.jobs,
                    txn,
                    note.rumor_receiver_pubkey().expect("expected pubkey"),
                )
                .size(10.0)
                .selectable(true)
                .show(ui);
            });
        }
    }

    fn wide_ui(
        &mut self,
        ui: &mut egui::Ui,
        txn: &Transaction,
        note_key: NoteKey,
        profile: &Result<ProfileRecord, nostrdb::Error>,
    ) -> egui::InnerResponse<NoteUiResponse> {
        ui.with_layout(egui::Layout::top_down(egui::Align::LEFT), |ui| {
            let mut note_action: Option<NoteAction> = None;
            let mut pfp_rect = None;

            if !self.flags.contains(NoteOptions::NotificationPreview) {
                ui.horizontal(|ui| {
                    let pfp_resp = self.pfp(note_key, profile, ui);
                    pfp_rect = Some(pfp_resp.bounding_rect);
                    note_action = pfp_resp
                        .into_action(self.note.pubkey())
                        .or(note_action.take());

                    let size = ui.available_size();

                    ui.vertical(|ui| {
                        ui.add_sized(
                            [size.x, self.options().pfp_size() as f32],
                            |ui: &mut egui::Ui| {
                                ui.horizontal_centered(|ui| {
                                    NoteView::note_header(
                                        ui,
                                        txn,
                                        self.note_context,
                                        self.note,
                                        profile,
                                        self.flags,
                                    );
                                })
                                .response
                            },
                        );

                        let note_reply = self
                            .note_context
                            .note_cache
                            .cached_note_or_insert_mut(note_key, self.note)
                            .reply
                            .borrow(self.note.tags());

                        if note_reply.reply().is_none() {
                            return;
                        }

                        ui.horizontal_wrapped(|ui| {
                            ui.spacing_mut().item_spacing.x = 0.0;

                            note_action =
                                reply_desc(ui, txn, &note_reply, self.note_context, self.flags)
                                    .or(note_action.take());
                        });
                    });
                });
            }

            let mut contents = NoteContents::new(self.note_context, txn, self.note, self.flags);

            ui.add(&mut contents);

            note_action = contents.action.or(note_action);

            let show_actionbar = self.options().contains(NoteOptions::ActionBar);
            let show_detailed = self.options().contains(NoteOptions::DetailedCounts);

            if show_actionbar || show_detailed {
                let stats = NoteStats::get(self.note_context.ndb, txn, self.note.id());

                if show_detailed {
                    note_action = detailed_counts_ui(ui, self.note.id(), &stats).or(note_action);
                }

                if show_actionbar {
                    note_action = ui
                        .horizontal_wrapped(|ui| {
                            // NOTE(jb55): without this we get a weird artifact where
                            // there subsequent lines start sinking leftward off the screen.
                            // question: WTF? question 2: WHY?
                            ui.allocate_space(egui::vec2(0.0, 0.0));

                            actionbar_ui(
                                ui,
                                &stats,
                                get_zapper(
                                    self.note_context.accounts,
                                    self.note_context.global_wallet,
                                    self.note_context.zaps,
                                ),
                                self.note,
                                self.note_context.accounts.selected_account_pubkey(),
                                note_key,
                                self.note_context.i18n,
                                self.note_context.sound,
                            )
                        })
                        .inner
                        .or(note_action);
                }
            }

            NoteUiResponse {
                action: note_action,
                pfp_rect,
            }
        })
    }

    fn standard_ui(
        &mut self,
        ui: &mut egui::Ui,
        txn: &Transaction,
        note_key: NoteKey,
        profile: &Result<ProfileRecord, nostrdb::Error>,
    ) -> egui::InnerResponse<NoteUiResponse> {
        // main design
        ui.with_layout(egui::Layout::left_to_right(egui::Align::TOP), |ui| {
            let (mut note_action, pfp_rect) =
                if self.flags.contains(NoteOptions::NotificationPreview) {
                    // do not render pfp
                    (None, None)
                } else {
                    let pfp_resp = self.pfp(note_key, profile, ui);
                    let pfp_rect = pfp_resp.bounding_rect;
                    (pfp_resp.into_action(self.note.pubkey()), Some(pfp_rect))
                };

            ui.with_layout(egui::Layout::top_down(egui::Align::LEFT), |ui| {
                if !self.flags.contains(NoteOptions::NotificationPreview) {
                    NoteView::note_header(
                        ui,
                        txn,
                        self.note_context,
                        self.note,
                        profile,
                        self.flags,
                    );

                    ui.horizontal_wrapped(|ui| {
                        ui.spacing_mut().item_spacing.x = 1.0;

                        let note_reply = self
                            .note_context
                            .note_cache
                            .cached_note_or_insert_mut(note_key, self.note)
                            .reply
                            .borrow(self.note.tags());

                        if note_reply.reply().is_none() {
                            return;
                        }

                        note_action =
                            reply_desc(ui, txn, &note_reply, self.note_context, self.flags)
                                .or(note_action.take());
                    });
                }

                let mut contents = NoteContents::new(self.note_context, txn, self.note, self.flags);
                ui.add(&mut contents);

                note_action = contents.action.or(note_action);

                let show_actionbar = self.options().contains(NoteOptions::ActionBar);
                let show_detailed = self.options().contains(NoteOptions::DetailedCounts);

                if show_actionbar || show_detailed {
                    let stats = NoteStats::get(self.note_context.ndb, txn, self.note.id());

                    if show_detailed {
                        note_action =
                            detailed_counts_ui(ui, self.note.id(), &stats).or(note_action);
                    }

                    if show_actionbar {
                        note_action = ui
                            .horizontal_wrapped(|ui| {
                                actionbar_ui(
                                    ui,
                                    &stats,
                                    get_zapper(
                                        self.note_context.accounts,
                                        self.note_context.global_wallet,
                                        self.note_context.zaps,
                                    ),
                                    self.note,
                                    self.note_context.accounts.selected_account_pubkey(),
                                    note_key,
                                    self.note_context.i18n,
                                    self.note_context.sound,
                                )
                            })
                            .inner
                            .or(note_action);
                    }
                }

                NoteUiResponse {
                    action: note_action,
                    pfp_rect,
                }
            })
            .inner
        })
    }

    #[profiling::function]
    fn show_standard(&mut self, ui: &mut egui::Ui) -> NoteResponse {
        let note_key = self.note.key().expect("todo: support non-db notes");
        let txn = self.note.txn().expect("todo: support non-db notes");

        let profile = self
            .note_context
            .ndb
            .get_profile_by_pubkey(txn, self.note.pubkey());

        let hitbox_id = note_hitbox_id(note_key, self.options(), self.parent);
        let maybe_hitbox = maybe_note_hitbox(ui, hitbox_id);

        // wide design
        let response = if self.options().contains(NoteOptions::Wide) {
            self.wide_ui(ui, txn, note_key, &profile)
        } else {
            self.standard_ui(ui, txn, note_key, &profile)
        };

        let note_ui_resp = response.inner;
        let mut note_action = note_ui_resp.action;

        if self.options().contains(NoteOptions::OptionsButton) {
            let context_pos = {
                let size = NoteContextButton::max_width();
                let top_right = response.response.rect.right_top();
                let min = Pos2::new(top_right.x - size, top_right.y);
                Rect::from_min_size(min, egui::vec2(size, size))
            };

            let resp = ui.add(NoteContextButton::new(note_key).place_at(context_pos));
            let can_sign = self
                .note_context
                .accounts
                .get_selected_account()
                .key
                .secret_key
                .is_some();
            let is_muted = self
                .note_context
                .accounts
                .mute()
                .is_pk_muted(self.note.pubkey());
            let note_id = NoteId::new(*self.note.id());
            if let Some(action) = NoteContextButton::menu(
                ui,
                self.note_context.i18n,
                resp.clone(),
                note_id,
                can_sign,
                is_muted,
            ) {
                note_action = Some(NoteAction::Context(ContextSelection { note_key, action }));
            }
        }

        note_action = note_hitbox_clicked(ui, hitbox_id, &response.response.rect, maybe_hitbox)
            .then_some(NoteAction::note(NoteId::new(*self.note.id())))
            .or(note_action);

        let mut resp = NoteResponse::new(response.response).with_action(note_action);
        if let Some(pfp_rect) = note_ui_resp.pfp_rect {
            resp = resp.with_pfp(pfp_rect);
        }

        resp
    }
}

fn get_zapper<'a>(
    accounts: &'a Accounts,
    global_wallet: &'a GlobalWallet,
    zaps: &'a Zaps,
) -> Option<Zapper<'a>> {
    let has_wallet = get_current_wallet(accounts, global_wallet).is_some();
    let cur_acc = accounts.get_selected_account();

    has_wallet.then_some(Zapper {
        zaps,
        cur_acc: cur_acc.keypair(),
    })
}

pub fn get_reposted_note<'a>(ndb: &Ndb, txn: &'a Transaction, note: &Note) -> Option<Note<'a>> {
    if note.kind() != 6 {
        return None;
    }

    let new_note_id: &[u8; 32] = {
        let mut res = None;
        for tag in note.tags().iter() {
            if tag.count() == 0 {
                continue;
            }

            if let Some("e") = tag.get(0).and_then(|t| t.variant().str()) {
                if let Some(note_id) = tag.get(1).and_then(|f| f.variant().id()) {
                    res = Some(note_id);
                    break;
                }
            }
        }
        res?
    };

    let note = ndb.get_note_by_id(txn, new_note_id).ok();
    note.filter(|note| note.kind() == 1)
}

struct NoteUiResponse {
    action: Option<NoteAction>,
    pfp_rect: Option<egui::Rect>,
}

struct PfpResponse {
    action: Option<MediaAction>,
    response: egui::Response,
    bounding_rect: egui::Rect,
}

impl PfpResponse {
    fn into_action(self, note_pk: &[u8; 32]) -> Option<NoteAction> {
        if self.response.clicked() {
            return Some(NoteAction::Profile(Pubkey::new(*note_pk)));
        }

        self.action.map(NoteAction::Media)
    }
}

fn show_actual_pfp(
    ui: &mut egui::Ui,
    images: &mut Images,
    jobs: &MediaJobSender,
    pic: &str,
    pfp_size: i8,
    note_key: NoteKey,
    profile: &Result<nostrdb::ProfileRecord<'_>, nostrdb::Error>,
) -> PfpResponse {
    let anim_speed = 0.05;
    let profile_key = profile.as_ref().unwrap().record().note_key();
    let note_key = note_key.as_u64();

    let (rect, size, resp) = crate::anim::hover_expand(
        ui,
        egui::Id::new((profile_key, note_key)),
        pfp_size as f32,
        NoteView::expand_size() as f32,
        anim_speed,
    );

    let resp = resp.on_hover_cursor(egui::CursorIcon::PointingHand);

    let mut pfp = ProfilePic::new(images, jobs, pic).size(size);
    let pfp_resp = ui.put(rect, &mut pfp);
    let action = pfp.action;

    pfp_resp.on_hover_ui_at_pointer(|ui| {
        ui.set_max_width(300.0);
        ui.add(ProfilePreview::new(profile.as_ref().unwrap(), images, jobs));
    });

    PfpResponse {
        response: resp,
        action,
        bounding_rect: rect.shrink((rect.width() - size) / 2.0),
    }
}

fn show_fallback_pfp(
    ui: &mut egui::Ui,
    images: &mut Images,
    jobs: &MediaJobSender,
    pfp_size: i8,
) -> PfpResponse {
    let sense = Sense::click();
    // This has to match the expand size from the above case to
    // prevent bounciness
    let size = (pfp_size + NoteView::expand_size()) as f32;
    let (rect, _response) = ui.allocate_exact_size(egui::vec2(size, size), sense);

    let mut pfp =
        ProfilePic::new(images, jobs, notedeck::profile::no_pfp_url()).size(pfp_size as f32);
    let response = ui.put(rect, &mut pfp).interact(sense);

    PfpResponse {
        action: pfp.action,
        response,
        bounding_rect: rect.shrink((rect.width() - size) / 2.0),
    }
}

fn note_hitbox_id(
    note_key: NoteKey,
    note_options: NoteOptions,
    parent: Option<NoteKey>,
) -> egui::Id {
    Id::new(("note_size", note_key, note_options, parent))
}

fn maybe_note_hitbox(ui: &mut egui::Ui, hitbox_id: egui::Id) -> Option<Response> {
    ui.ctx()
        .data_mut(|d| d.get_temp(hitbox_id))
        .map(|note_size: Vec2| {
            // The hitbox should extend the entire width of the
            // container.  The hitbox height was cached last layout.
            let container_rect = ui.max_rect();
            let rect = Rect {
                min: pos2(container_rect.min.x, container_rect.min.y),
                max: pos2(container_rect.max.x, container_rect.min.y + note_size.y),
            };

            let response = ui.interact(rect, ui.id().with(hitbox_id), egui::Sense::click());

            response
                .widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Other, true, "hitbox"));

            response
        })
}

fn note_hitbox_clicked(
    ui: &mut egui::Ui,
    hitbox_id: egui::Id,
    note_rect: &Rect,
    maybe_hitbox: Option<Response>,
) -> bool {
    // Stash the dimensions of the note content so we can render the
    // hitbox in the next frame
    ui.ctx().data_mut(|d| {
        d.insert_temp(hitbox_id, note_rect.size());
    });

    // If there was an hitbox and it was clicked open the thread
    match maybe_hitbox {
        Some(hitbox) => hitbox.clicked(),
        _ => false,
    }
}

struct NoteStats {
    counts: Option<NoteCounts>,
    zap_msats: u64,
}

struct NoteCounts {
    reactions: u32,
    thread_replies: u32,
    direct_replies: u16,
    quotes: u16,
    reposts: u16,
}

impl NoteCounts {
    fn reply_count(&self, note: &Note) -> u32 {
        if is_root_note(note) {
            self.thread_replies
        } else {
            self.direct_replies as u32
        }
    }

    fn repost_count(&self) -> u32 {
        (self.quotes + self.reposts) as u32
    }
}

impl NoteStats {
    fn get(ndb: &Ndb, txn: &Transaction, note_id: &[u8; 32]) -> Self {
        let mut counts = None;
        let mut zap_msats = 0u64;

        if let Ok(md) = ndb.get_note_metadata(txn, note_id) {
            for e in md {
                match e {
                    NoteMetadataEntryVariant::Counts(ce) => {
                        counts = Some(NoteCounts {
                            reactions: ce.reactions(),
                            thread_replies: ce.thread_replies(),
                            direct_replies: ce.direct_replies(),
                            quotes: ce.quotes(),
                            reposts: ce.reposts(),
                        });
                    }
                    NoteMetadataEntryVariant::Zap(ze) => {
                        zap_msats = ze.msats();
                    }
                    _ => {}
                }
            }
        }

        NoteStats { counts, zap_msats }
    }

    fn zap_sats(&self) -> u64 {
        self.zap_msats / 1000
    }
}

struct Zapper<'a> {
    zaps: &'a Zaps,
    cur_acc: KeypairUnowned<'a>,
}

fn zap_actionbar_button(
    ui: &mut egui::Ui,
    note_id: &[u8; 32],
    note_pubkey: &[u8; 32],
    zapper: Option<Zapper<'_>>,
    zap_sats: u64,
    i18n: &mut Localization,
    sound: &notedeck::SoundManager,
) -> Option<NoteAction> {
    let mut action: Option<NoteAction> = None;
    let Zapper { zaps, cur_acc } = zapper?;

    let zap_target = ZapTarget::Note(NoteZapTarget {
        note_id,
        zap_recipient: note_pubkey,
    });

    let zap_state = zaps.any_zap_state_for(cur_acc.pubkey.bytes(), zap_target);

    let target = NoteZapTargetOwned {
        note_id: NoteId::new(*note_id),
        zap_recipient: Pubkey::new(*note_pubkey),
    };

    cur_acc.secret_key.as_ref()?;

    match zap_state {
        Ok(any_zap_state) => {
            // Play sound on transition to confirmed/local-confirmed
            let is_confirmed = matches!(
                any_zap_state,
                AnyZapState::Confirmed | AnyZapState::LocalOnly
            );
            if notedeck::state_entered(
                ui.ctx(),
                egui::Id::new(("zap_confirm_sound", note_id)),
                is_confirmed,
            ) {
                sound.play(notedeck::SoundEffect::ZapConfirm);
            }

            let zap_resp = ui.add(zap_button(i18n, any_zap_state, note_id));

            if zap_resp.secondary_clicked() {
                action = Some(NoteAction::Zap(ZapAction::CustomizeAmount(target.clone())));
            }

            if notedeck::hover_entered(ui, zap_resp.id, zap_resp.hovered()) {
                sound.play(notedeck::SoundEffect::Hover);
            }

            if zap_resp.clicked() {
                sound.play(notedeck::SoundEffect::Zap);
                action = Some(NoteAction::Zap(ZapAction::Send(ZapTargetAmount {
                    target,
                    specified_msats: None,
                })));
            }

            zap_resp
        }
        Err(err) => {
            let (rect, _) = ui.allocate_at_least(egui::vec2(10.0, 10.0), egui::Sense::click());
            let x_button = ui.add(x_button(rect)).on_hover_text(err.to_string());

            if x_button.clicked() {
                action = Some(NoteAction::Zap(ZapAction::ClearError(target.clone())));
            }
            x_button
        }
    }
    .on_hover_cursor(egui::CursorIcon::PointingHand);

    if zap_sats > 0 {
        ui.weak(format_sats_abbreviated(zap_sats));
    }

    action
}

fn format_sats_abbreviated(sats: u64) -> String {
    if sats >= 1_000_000 {
        let m = sats as f64 / 1_000_000.0;
        if m >= 10.0 {
            format!("{}M", m as u64)
        } else {
            format!("{:.1}M", m)
        }
    } else if sats >= 1_000 {
        let k = sats as f64 / 1_000.0;
        if k >= 10.0 {
            format!("{}K", k as u64)
        } else {
            format!("{:.1}K", k)
        }
    } else {
        format!("{sats}")
    }
}

fn is_root_note(note: &Note) -> bool {
    for tag in note.tags() {
        if tag.count() < 2 {
            continue;
        }

        // any reference to an e tag is a non-root note
        if tag.get_str(0) == Some("e") {
            return false;
        }
    }

    true
}

/// Renders a detailed counts row showing reposts, reactions, zaps, etc.
/// Styled like Damus iOS: "3 Reposts  11 Reactions  21K Sats"
/// Each stat is clickable and navigates to a detail view.
fn detailed_counts_ui(
    ui: &mut egui::Ui,
    note_id: &[u8; 32],
    stats: &NoteStats,
) -> Option<NoteAction> {
    let (reposts, reactions) = stats
        .counts
        .as_ref()
        .map(|c| (c.repost_count(), c.reactions))
        .unwrap_or((0, 0));
    let zap_sats = stats.zap_sats();

    // Only show if there's something to display
    if reposts == 0 && reactions == 0 && zap_sats == 0 {
        return None;
    }

    ui.add_space(4.0);
    ui.separator();

    let action = ui
        .horizontal_wrapped(|ui| {
            ui.spacing_mut().item_spacing.x = 2.0;
            let text_color = ui.style().visuals.text_color();
            let weak_color = ui.style().visuals.weak_text_color();
            let font_size = 13.0;
            let spacing = 12.0;

            let mut first = true;
            let mut action: Option<NoteAction> = None;

            let mut stat = |ui: &mut egui::Ui, value: &str, label: &str, pluralize: bool| {
                if !first {
                    ui.add_space(spacing);
                }
                first = false;

                let label_text = if pluralize {
                    format!("{label}s")
                } else {
                    label.to_string()
                };

                let r1 = ui.label(
                    egui::RichText::new(value)
                        .size(font_size)
                        .strong()
                        .color(text_color),
                );
                let r2 = ui
                    .label(
                        egui::RichText::new(label_text)
                            .size(font_size)
                            .color(weak_color),
                    )
                    .on_hover_cursor(egui::CursorIcon::PointingHand);

                r1.union(r2)
            };

            let nid = NoteId::new(*note_id);

            if reposts > 0 && stat(ui, &reposts.to_string(), "Repost", reposts != 1).clicked() {
                action = Some(NoteAction::Detail(NoteDetail::Reposts(nid)));
            }
            if reactions > 0
                && stat(ui, &reactions.to_string(), "Reaction", reactions != 1).clicked()
            {
                action = Some(NoteAction::Detail(NoteDetail::Reactions(nid)));
            }
            if zap_sats > 0
                && stat(ui, &format_sats_abbreviated(zap_sats), "Sat", zap_sats != 1).clicked()
            {
                action = Some(NoteAction::Detail(NoteDetail::Zaps(nid)));
            }

            action
        })
        .inner;

    ui.separator();
    ui.add_space(4.0);

    action
}

#[allow(clippy::too_many_arguments)]
#[profiling::function]
fn actionbar_ui(
    ui: &mut egui::Ui,
    stats: &NoteStats,
    zapper: Option<Zapper<'_>>,
    note: &Note,
    current_user_pubkey: &Pubkey,
    note_key: NoteKey,
    i18n: &mut Localization,
    sound: &notedeck::SoundManager,
) -> Option<NoteAction> {
    let mut action = None;
    let spacing = notedeck::tokens::SPACING_XL;

    ui.spacing_mut().item_spacing.x = 2.0;
    ui.set_min_height(26.0);

    let reply_resp =
        reply_button(ui, i18n, note_key).on_hover_cursor(egui::CursorIcon::PointingHand);

    if let Some(c) = &stats.counts {
        let count = c.reply_count(note);
        if count > 0 {
            crate::anim::rolling_number(ui, egui::Id::new((note_key, "replies")), count);
        }
    }

    ui.add_space(spacing);

    let filled = ui
        .ctx()
        .data(|d| d.get_temp(reaction_sent_id(current_user_pubkey, note.id())))
        == Some(true);

    let like_resp =
        like_button(ui, i18n, note_key, filled).on_hover_cursor(egui::CursorIcon::PointingHand);

    if let Some(c) = &stats.counts {
        if c.reactions > 0 {
            crate::anim::rolling_number(ui, egui::Id::new((note_key, "likes")), c.reactions);
        }
    }

    ui.add_space(spacing);

    let quote_resp =
        quote_repost_button(ui, i18n, note_key).on_hover_cursor(egui::CursorIcon::PointingHand);

    if let Some(c) = &stats.counts {
        let count = c.repost_count();
        if count > 0 {
            crate::anim::rolling_number(ui, egui::Id::new((note_key, "quotes")), count);
        }
    }

    ui.add_space(spacing);

    // Hover-enter sound on action bar buttons
    for resp in [&reply_resp, &like_resp, &quote_resp] {
        if notedeck::hover_entered(ui, resp.id, resp.hovered()) {
            sound.play(notedeck::SoundEffect::Hover);
        }
    }

    if reply_resp.clicked() {
        sound.play(notedeck::SoundEffect::Click);
        action = Some(NoteAction::Reply(NoteId::new(*note.id())));
    }

    if like_resp.clicked() {
        sound.play(notedeck::SoundEffect::Like);
        action = Some(NoteAction::React(ReactAction::new(
            NoteId::new(*note.id()),
            "🤙🏻",
        )));
    }

    if quote_resp.clicked() {
        sound.play(notedeck::SoundEffect::Repost);
        action = Some(NoteAction::Repost(NoteId::new(*note.id())));
    }

    action = zap_actionbar_button(
        ui,
        note.id(),
        note.pubkey(),
        zapper,
        stats.zap_sats(),
        i18n,
        sound,
    )
    .or(action);

    action
}

#[profiling::function]
fn render_notetime(
    ui: &mut egui::Ui,
    i18n: &mut Localization,
    created_at: u64,
    before: bool,
) -> Response {
    if before {
        secondary_label(
            ui,
            format!(" ⋅ {}", notedeck::time_ago_since(i18n, created_at)),
        )
    } else {
        secondary_label(
            ui,
            format!("{} ⋅ ", notedeck::time_ago_since(i18n, created_at)),
        )
    }
}

fn reply_button(ui: &mut egui::Ui, i18n: &mut Localization, note_key: NoteKey) -> egui::Response {
    let img = if ui.style().visuals.dark_mode {
        app_images::reply_dark_image()
    } else {
        app_images::reply_light_image()
    };

    let (rect, size, resp) =
        crate::anim::hover_expand_small(ui, ui.id().with(("reply_anim", note_key)));

    // align rect to note contents
    let expand_size = 5.0; // from hover_expand_small
    let rect = rect.translate(egui::vec2(-(expand_size / 2.0), 0.0));

    let put_resp = ui.put(rect, img.max_width(size)).on_hover_text(tr!(
        i18n,
        "Reply to this note",
        "Hover text for reply button"
    ));

    resp.union(put_resp)
}

fn like_button(
    ui: &mut egui::Ui,
    i18n: &mut Localization,
    note_key: NoteKey,
    filled: bool,
) -> egui::Response {
    let img = {
        let img = if filled {
            app_images::like_image_filled()
        } else {
            app_images::like_image()
        };

        img.tint(ui.visuals().text_color())
    };

    let (rect, size, resp) =
        crate::anim::hover_expand_small(ui, ui.id().with(("like_anim", note_key)));

    // align rect to note contents
    let expand_size = 5.0; // from hover_expand_small
    let rect = rect.translate(egui::vec2(-(expand_size / 2.0), 0.0));

    let put_resp = ui.put(rect, img.max_width(size)).on_hover_text(tr!(
        i18n,
        "Like this note",
        "Hover text for like button"
    ));

    resp.union(put_resp)
}

fn repost_icon(dark_mode: bool) -> egui::Image<'static> {
    if dark_mode {
        app_images::repost_dark_image()
    } else {
        app_images::repost_light_image()
    }
}

fn quote_repost_button(
    ui: &mut egui::Ui,
    i18n: &mut Localization,
    note_key: NoteKey,
) -> egui::Response {
    let size = crate::anim::hover_small_size() + 4.0;
    let expand_size = 5.0;
    let anim_speed = 0.05;
    let id = ui.id().with(("repost_anim", note_key));

    let (rect, size, resp) = crate::anim::hover_expand(ui, id, size, expand_size, anim_speed);

    let rect = rect.translate(egui::vec2(-(expand_size / 2.0), -1.0));

    let put_resp = ui
        .put(rect, repost_icon(ui.visuals().dark_mode).max_width(size))
        .on_hover_text(tr!(
            i18n,
            "Repost this note",
            "Hover text for repost button"
        ));

    resp.union(put_resp)
}

fn zap_button<'a>(
    i18n: &'a mut Localization,
    state: AnyZapState,
    noteid: &'a [u8; 32],
) -> impl egui::Widget + use<'a> {
    move |ui: &mut egui::Ui| -> egui::Response {
        let (rect, size, resp) = crate::anim::hover_expand_small(ui, ui.id().with("zap"));

        let mut img = app_images::zap_dark_image().max_width(size);
        let id = ui.id().with(("pulse", noteid));
        let ctx = ui.ctx().clone();

        match state {
            AnyZapState::None => {
                if !ui.visuals().dark_mode {
                    img = app_images::zap_light_image();
                }
            }
            AnyZapState::Pending => {
                let alpha_min = if ui.visuals().dark_mode { 50 } else { 180 };
                let cur_alpha = PulseAlpha::new(&ctx, id, alpha_min, 255)
                    .with_speed(0.35)
                    .animate();

                let cur_color = egui::Color32::from_rgba_unmultiplied(0xFF, 0xB7, 0x57, cur_alpha);
                img = img.tint(cur_color);
            }
            AnyZapState::LocalOnly => {
                img = img.tint(egui::Color32::from_rgb(0xFF, 0xB7, 0x57));
            }
            AnyZapState::Confirmed => {}
        }

        // align rect to note contents
        let expand_size = 5.0; // from hover_expand_small
        let rect = rect.translate(egui::vec2(-(expand_size / 2.0), 0.0));

        let put_resp = ui.put(rect, img).on_hover_text(tr!(
            i18n,
            "Zap this note",
            "Hover text for zap button"
        ));

        resp.union(put_resp)
    }
}
