/*
 *     Created by Angel Leon (@gubatron), Alden Torres (aldenml),
 *  *            Marcelina Knitter (@marcelinkaaa)
 *     Copyright (c) 2011-2026, FrostWire(R). All rights reserved.
 * 
 *     This program is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 * 
 *     This program is distributed in the hope that it will be useful,
 *     but WITHOUT ANY WARRANTY; without even the implied warranty of
 *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *     GNU General Public License for more details.
 * 
 *     You should have received a copy of the GNU General Public License
 *     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package org.team_hermes.wireshare.android.gui.adapters;

import android.app.Dialog;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnLongClickListener;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;

import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.RecyclerView;

import org.team_hermes.wireshare.concurrent.concurrent.ExecutorsHelper;

import org.team_hermes.wireshare.android.AndroidPaths;
import org.team_hermes.wireshare.android.AndroidPlatform;
import org.team_hermes.wireshare.android.R;
import org.team_hermes.wireshare.android.core.ConfigurationManager;
import org.team_hermes.wireshare.android.core.Constants;
import org.team_hermes.wireshare.android.core.FWFileDescriptor;
import org.team_hermes.wireshare.android.core.MediaType;
import org.team_hermes.wireshare.android.gui.Librarian;
import org.team_hermes.wireshare.android.gui.NetworkManager;
import org.team_hermes.wireshare.android.gui.adapters.menu.CancelMenuAction;
import org.team_hermes.wireshare.android.gui.adapters.menu.CopyToClipboardMenuAction;
import org.team_hermes.wireshare.android.gui.adapters.menu.OpenMenuAction;
import org.team_hermes.wireshare.android.gui.adapters.menu.PauseDownloadMenuAction;
import org.team_hermes.wireshare.android.gui.adapters.menu.ResumeDownloadMenuAction;
import org.team_hermes.wireshare.android.gui.adapters.menu.RetryDownloadAction;
import org.team_hermes.wireshare.android.gui.adapters.menu.SeedAction;
import org.team_hermes.wireshare.android.gui.adapters.menu.SendBitcoinTipAction;
import org.team_hermes.wireshare.android.gui.adapters.menu.SendFiatTipAction;
import org.team_hermes.wireshare.android.gui.adapters.menu.StopSeedingAction;
import org.team_hermes.wireshare.android.gui.adapters.menu.TransferDetailsMenuAction;
import org.team_hermes.wireshare.android.gui.transfers.InvalidTransfer;
import org.team_hermes.wireshare.android.gui.transfers.TorrentFetcherDownload;
import org.team_hermes.wireshare.android.gui.transfers.UIBittorrentDownload;
import org.team_hermes.wireshare.android.gui.util.TransferStateStrings;
import org.team_hermes.wireshare.android.gui.util.UIUtils;
import org.team_hermes.wireshare.android.gui.views.ClickAdapter;
import org.team_hermes.wireshare.android.gui.views.MenuAction;
import org.team_hermes.wireshare.android.gui.views.MenuAdapter;
import org.team_hermes.wireshare.android.gui.views.MenuBuilder;
import org.team_hermes.wireshare.android.util.SystemUtils;
import org.team_hermes.wireshare.bittorrent.BTDownloadItem;
import org.team_hermes.wireshare.bittorrent.BTEngine;
import org.team_hermes.wireshare.bittorrent.PaymentOptions;
import org.team_hermes.wireshare.search.StreamableUtils;
import org.team_hermes.wireshare.transfers.BittorrentDownload;
import org.team_hermes.wireshare.transfers.HttpDownload;
import org.team_hermes.wireshare.transfers.SoundcloudDownload;
import org.team_hermes.wireshare.transfers.Transfer;
import org.team_hermes.wireshare.transfers.TransferItem;
import org.team_hermes.wireshare.transfers.TransferState;
import org.team_hermes.wireshare.util.Logger;
import org.team_hermes.wireshare.util.Ref;

import org.apache.commons.io.FilenameUtils;

import java.io.File;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * @author gubatron
 * @author aldenml
 */
public class TransferListAdapter extends RecyclerView.Adapter<TransferListAdapter.ViewHolder> {
    private static final Logger LOG = Logger.getLogger(TransferListAdapter.class);
    // Dedicated executor for DiffUtil calculations to avoid blocking sequential handler queues
    private static java.util.concurrent.ExecutorService transferDiffExecutor;

    private static java.util.concurrent.ExecutorService getTransferDiffExecutor() {
        if (transferDiffExecutor == null || transferDiffExecutor.isShutdown()) {
            transferDiffExecutor = ExecutorsHelper.newFixedSizeThreadPool(2, "TransferDiff");
        }
        return transferDiffExecutor;
    }

    private final WeakReference<Context> contextRef;

    private final ViewOnClickListener viewOnClickListener;
    private final ViewOnLongClickListener viewOnLongClickListener;
    private final OpenOnClickListener openOnClickListener;
    private final TransferDetailsClickListener transferDetailsClickListener;

    /**
     * Keep track of all dialogs ever opened so we dismiss when we leave to avoid memory leaks
     */
    private final List<Dialog> dialogs;
    private final List<TransferUiState> previousUiStates;
    private List<Transfer> list;

    /**
     * Track whether an update is in progress to prevent concurrent updates during animations.
     * This prevents the "Tmp detached view should be removed from RecyclerView" crash.
     */
    private volatile boolean updateInProgress = false;
    private volatile List<Transfer> pendingUpdate = null;

    /**
     * Track IDs of removed transfers to prevent them from being re-added by updateList().
     */
    private final Set<String> removedTransferIds = Collections.synchronizedSet(new HashSet<>());

    public TransferListAdapter(Context context, List<Transfer> list) {
        this.contextRef = new WeakReference<>(context);
        this.dialogs = new ArrayList<>();
        List<Transfer> initialList = list == null ? Collections.emptyList() : list;
        this.list = new ArrayList<>(initialList);
        this.previousUiStates = new ArrayList<>(captureUiStates(this.list));
        viewOnClickListener = new ViewOnClickListener();
        viewOnLongClickListener = new ViewOnLongClickListener();
        openOnClickListener = new OpenOnClickListener(context);
        transferDetailsClickListener = new TransferDetailsClickListener(context);
    }

    // RecyclerView.Adapter methods
    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int i) {
        LinearLayout convertView =
                (LinearLayout) LayoutInflater.from(parent.getContext()).inflate(R.layout.view_transfer_list_item, parent, false);
        // Store context once and check for null before passing to ViewHolder
        Context context = Ref.alive(contextRef) ? contextRef.get() : parent.getContext();
        return new ViewHolder(this,
                context,
                convertView,
                viewOnClickListener,
                viewOnLongClickListener,
                openOnClickListener,
                transferDetailsClickListener);
    }

    @Override
    public void onBindViewHolder(ViewHolder viewHolder, int i) {
        if (list == null || list.isEmpty()) {
            return;
        }
        viewHolder.updateView(i);
    }

    @Override
    public int getItemCount() {
        return list == null ? 0 : list.size();
    }

    public List<Transfer> getList() {
        return list != null ? new ArrayList<>(list) : Collections.emptyList();
    }

    public void updateList(List<Transfer> newList) {
        updateList(newList, false);
    }

    public void updateList(List<Transfer> newList, boolean forceImmediate) {
        // For forced updates (like user cancelling a transfer), use notifyDataSetChanged()
        // for instant feedback, bypassing DiffUtil calculation which can take several seconds
        if (forceImmediate) {
            pendingUpdate = null;  // Clear any queued updates
            updateInProgress = true;
            try {
                list.clear();
                list.addAll(newList == null ? Collections.emptyList() : newList);
                previousUiStates.clear();
                // Try to notify immediately, but if RecyclerView is computing layout, defer it
                try {
                    notifyDataSetChanged();  // Instant refresh, no DiffUtil calculation
                } catch (IllegalStateException e) {
                    // RecyclerView is computing layout or scrolling, defer the notification
                    LOG.warn("RecyclerView computing layout during forceImmediate, deferring notifyDataSetChanged: " + e.getMessage());
                    deferNotifyDataSetChanged();
                }
            } finally {
                updateInProgress = false;
                // Process any pending update that came in while we were updating
                if (pendingUpdate != null) {
                    List<Transfer> next = pendingUpdate;
                    pendingUpdate = null;
                    prepareUpdateOnBackgroundThread(next);
                }
            }
            return;
        }

        // If update is already in progress, queue the pending update instead of trying
        // to dispatch immediately. This prevents concurrent modifications during animations,
        // which causes the "Tmp detached view should be removed from RecyclerView" crash.
        if (updateInProgress) {
            pendingUpdate = newList;
            return;
        }

        // Prepare diff calculation on background thread to avoid blocking JNI calls
        prepareUpdateOnBackgroundThread(newList);
    }

    /**
     * Prepares the adapter update on background thread to avoid blocking JNI calls
     * when capturing UI states. Once done, posts result back to main thread.
     * Filters out any transfers that have been explicitly removed.
     */
    private void prepareUpdateOnBackgroundThread(List<Transfer> newList) {
        if (newList == null) {
            newList = Collections.emptyList();
        }

        // Keep original unfiltered list to check against for cleanup
        final List<Transfer> originalList = new ArrayList<>(newList);

        // Filter out any transfers that have been marked as removed
        // This prevents removed transfers from being re-added by stale data
        final List<Transfer> filteredList = new ArrayList<>(newList);
        if (!removedTransferIds.isEmpty()) {
            filteredList.removeIf(t -> removedTransferIds.contains(resolveTransferId(t)));
        }

        final List<Transfer> finalSafeNewList = (filteredList == list) ? new ArrayList<>(filteredList) : filteredList;

        // Do expensive work on background thread using parallel executor
        try {
            getTransferDiffExecutor().execute(() -> {
                try {
                    // Capture UI states on background thread to avoid blocking JNI calls
                    final List<TransferUiState> newUiStates = captureUiStates(finalSafeNewList);
                    // Calculate diff on background thread
                    final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new TransferDiffCallback(previousUiStates, newUiStates), false);

                    // Post result back to main thread for UI update
                    // Pass filtered list (for display) and original list (for cleanup decision)
                    if (Ref.alive(contextRef) && contextRef.get() instanceof android.app.Activity) {
                        ((android.app.Activity) contextRef.get()).runOnUiThread(() -> applyDiffResult(finalSafeNewList, newUiStates, diffResult, originalList));
                    }
                } catch (Throwable e) {
                    LOG.error("Error preparing adapter update on background thread", e);
                }
            });
        } catch (java.util.concurrent.RejectedExecutionException e) {
            // Executor queue full, fall back to handler
            LOG.warn("TransferDiff executor queue full, using fallback handler: " + e.getMessage());
            SystemUtils.postToHandler(SystemUtils.HandlerThreadName.DOWNLOADER, () -> {
                try {
                    final List<TransferUiState> newUiStates = captureUiStates(finalSafeNewList);
                    final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new TransferDiffCallback(previousUiStates, newUiStates), false);
                    if (Ref.alive(contextRef) && contextRef.get() instanceof android.app.Activity) {
                        ((android.app.Activity) contextRef.get()).runOnUiThread(() -> applyDiffResult(finalSafeNewList, newUiStates, diffResult, originalList));
                    }
                } catch (Throwable e2) {
                    LOG.error("Error in fallback handler", e2);
                }
            });
        }
    }

    /**
     * Applies the diff result on main thread (safe for RecyclerView updates).
     * @param newTransferList the filtered list (removed transfers excluded)
     * @param originalList the unfiltered list from TransferManager (for cleanup decisions)
     */
    private void applyDiffResult(List<Transfer> newTransferList, List<TransferUiState> newUiStates, DiffUtil.DiffResult diffResult, List<Transfer> originalList) {
        updateInProgress = true;
        try {
            list.clear();
            list.addAll(newTransferList);
            previousUiStates.clear();
            previousUiStates.addAll(newUiStates);

            // Clean up removedTransferIds: keep only transfers that are STILL in TransferManager
            // (not yet deleted). Remove tracking for transfers that are confirmed gone.
            // Use originalList (unfiltered) to check what's really in TransferManager
            if (!removedTransferIds.isEmpty()) {
                Set<String> originalIds = new HashSet<>();
                for (Transfer t : originalList) {
                    originalIds.add(resolveTransferId(t));
                }
                // Keep tracking only for transfers still in TransferManager
                removedTransferIds.retainAll(originalIds);
            }

            try {
                diffResult.dispatchUpdatesTo(this);
            } catch (IllegalStateException e) {
                // RecyclerView is computing layout or scrolling, cannot apply diff.
                // Fall back to full refresh, deferred until RecyclerView is safe.
                LOG.warn("RecyclerView computing layout, deferring full refresh: " + e.getMessage());
                deferNotifyDataSetChanged();
            }
        } finally {
            updateInProgress = false;

            // If a new update was queued while this one was in progress, process it now
            if (pendingUpdate != null) {
                List<Transfer> next = pendingUpdate;
                pendingUpdate = null;
                prepareUpdateOnBackgroundThread(next);
            }
        }
    }

    /**
     * Safely defers notifyDataSetChanged() to a time when RecyclerView is not computing layout.
     * Retries with a small delay if it's still computing layout.
     */
    private void deferNotifyDataSetChanged() {
        SystemUtils.postToHandler(SystemUtils.HandlerThreadName.DOWNLOADER, () -> {
            if (Ref.alive(contextRef) && contextRef.get() instanceof android.app.Activity) {
                ((android.app.Activity) contextRef.get()).runOnUiThread(() -> {
                    try {
                        notifyDataSetChanged();
                    } catch (IllegalStateException e) {
                        // Still computing layout, try again later with a small delay
                        LOG.warn("RecyclerView still computing layout, retrying notifyDataSetChanged");
                        // Retry after a small delay to avoid busy-waiting
                        SystemUtils.postToHandler(SystemUtils.HandlerThreadName.DOWNLOADER, () -> {
                            try {
                                Thread.sleep(100);
                            } catch (InterruptedException ignored) {
                            }
                            deferNotifyDataSetChanged();
                        });
                    }
                });
            }
        });
    }

    /**
     * Performs the actual update, applying DiffUtil and dispatching changes.
     * Called from updateList() when safe, or from dispatchUpdatesTo listener when previous update completes.
     */
    private void performUpdate(List<Transfer> newList) {
        prepareUpdateOnBackgroundThread(newList);
    }

    /**
     * DiffUtil.Callback for calculating differences between old and new Transfer lists.
     * This enables RecyclerView to use granular notifications instead of notifyDataSetChanged().
     *
     * Performance impact:
     * - Before: notifyDataSetChanged() rebinds ALL items (~50-100ms for 20 transfers)
     * - After: Only changed items rebind (~5-10ms for typical updates)
     * - Bonus: Smooth item animations (fade, move) visible to user
     */
    private static class TransferDiffCallback extends DiffUtil.Callback {
        private final List<TransferUiState> oldList;
        private final List<TransferUiState> newList;

        TransferDiffCallback(List<TransferUiState> oldList, List<TransferUiState> newList) {
            this.oldList = oldList;
            this.newList = newList;
        }

        @Override
        public int getOldListSize() {
            return oldList.size();
        }

        @Override
        public int getNewListSize() {
            return newList.size();
        }

        @Override
        public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
            TransferUiState oldState = oldList.get(oldItemPosition);
            TransferUiState newState = newList.get(newItemPosition);
            return oldState.id.equals(newState.id);
        }

        @Override
        public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
            TransferUiState oldState = oldList.get(oldItemPosition);
            TransferUiState newState = newList.get(newItemPosition);
            return oldState.hasSameContent(newState);
        }
    }

    private static List<TransferUiState> captureUiStates(List<Transfer> transfers) {
        if (transfers == null || transfers.isEmpty()) {
            return Collections.emptyList();
        }
        List<TransferUiState> snapshot = new ArrayList<>(transfers.size());
        for (Transfer transfer : transfers) {
            snapshot.add(new TransferUiState(transfer));
        }
        return snapshot;
    }

    private static String resolveTransferId(Transfer transfer) {
        if (transfer instanceof BittorrentDownload) {
            String hash = ((BittorrentDownload) transfer).getInfoHash();
            if (hash != null && !hash.isEmpty()) {
                return "bt:" + hash;
            }
        }
        File savePath = transfer.getSavePath();
        if (savePath != null) {
            return "path:" + savePath.getAbsolutePath();
        }
        String displayName = transfer.getDisplayName();
        if (displayName != null && !displayName.isEmpty()) {
            return "name:" + displayName;
        }
        return "instance:" + System.identityHashCode(transfer);
    }

    private static final class TransferUiState {
        private final String id;
        private final TransferState state;
        private final int progress;
        private final long downloadSpeed;
        private final int connectedSeeds;
        private final int totalSeeds;
        private final int connectedPeers;
        private final int totalPeers;

        private TransferUiState(Transfer transfer) {
            this.id = resolveTransferId(transfer);
            this.state = transfer.getState();
            this.progress = transfer.getProgress();
            this.downloadSpeed = transfer.getDownloadSpeed();
            if (transfer instanceof BittorrentDownload) {
                BittorrentDownload bt = (BittorrentDownload) transfer;
                this.connectedSeeds = bt.getConnectedSeeds();
                this.totalSeeds = bt.getTotalSeeds();
                this.connectedPeers = bt.getConnectedPeers();
                this.totalPeers = bt.getTotalPeers();
            } else {
                this.connectedSeeds = -1;
                this.totalSeeds = -1;
                this.connectedPeers = -1;
                this.totalPeers = -1;
            }
        }

        private boolean hasSameContent(TransferUiState other) {
            if (other == null) {
                return false;
            }
            return state == other.state &&
                    progress == other.progress &&
                    downloadSpeed == other.downloadSpeed &&
                    connectedSeeds == other.connectedSeeds &&
                    totalSeeds == other.totalSeeds &&
                    connectedPeers == other.connectedPeers &&
                    totalPeers == other.totalPeers;
        }
    }

    public void dismissDialogs() {
        for (Dialog dialog : dialogs) {
            try {
                dialog.dismiss();
            } catch (Throwable e) {
                LOG.warn("Error dismissing dialog", e);
            }
        }
    }

    private MenuAdapter getMenuAdapter(View view) {
        // Store context once to avoid repeated dereferencing
        if (!Ref.alive(contextRef)) {
            return null;
        }
        Context context = contextRef.get();

        Object tag = view.getTag();
        String title = "";
        List<MenuAction> items = new ArrayList<>();
        if (tag instanceof Transfer && ((Transfer) tag).getState().name().contains("ERROR")) {
            if (tag instanceof InvalidTransfer || tag instanceof TorrentFetcherDownload) {
                items.add(new RetryDownloadAction(context, (Transfer) tag));
            }
        }
        if (tag instanceof BittorrentDownload) {
            title = populateBittorrentDownloadMenuActions(context, (BittorrentDownload) tag, items);
        } else if (tag instanceof Transfer) {
            title = populateCloudDownloadMenuActions(context, tag, items);
        }
        return items.size() > 0 ? new MenuAdapter(context, title, items) : null;
    }

    private String populateCloudDownloadMenuActions(Context context, Object tag, List<MenuAction> items) {
        Transfer download = (Transfer) tag;
        String title = download.getDisplayName();
        boolean errored = download.getState().name().contains("ERROR");
        boolean finishedSuccessfully = !errored && download.isComplete() && isCloudDownload(tag);
        if (finishedSuccessfully) {
            File savePath = download.getSavePath();
            if (!AndroidPlatform.saf(savePath)) {
                items.add(new SeedAction(context));
            }
            String mime = extractMime(download);
            items.add(new OpenMenuAction(context, download.getDisplayName(), savePath.getAbsolutePath(), mime));
        }
        items.add(new CancelMenuAction(context, download, !finishedSuccessfully));
        return title;
    }

    private boolean isCloudDownload(Object tag) {
        return tag instanceof HttpDownload;
    }

    private String populateBittorrentDownloadMenuActions(Context context, BittorrentDownload bittorrentDownload, List<MenuAction> items) {
        String title;
        title = bittorrentDownload.getDisplayName();
        //If it's a torrent download with a single file, we should be able to open it.
        if (bittorrentDownload.isComplete() && bittorrentDownload.getItems().size() > 0) {
            TransferItem transferItem = bittorrentDownload.getItems().get(0);
            String path = transferItem.getFile().getAbsolutePath();
            String mimeType = UIUtils.getMimeType(path);
            items.add(new OpenMenuAction(context, path, mimeType));
        }
        if (!bittorrentDownload.isComplete() && !bittorrentDownload.isSeeding()) {
            if (!bittorrentDownload.isPaused()) {
                items.add(new PauseDownloadMenuAction(context, bittorrentDownload));
            } else {
                boolean wifiIsUp = NetworkManager.instance().isDataWIFIUp();
                boolean bittorrentOnMobileData = !ConfigurationManager.instance().getBoolean(Constants.PREF_KEY_NETWORK_USE_WIFI_ONLY);
                if (wifiIsUp || bittorrentOnMobileData) {
                    if (!bittorrentDownload.isComplete()) {
                        items.add(new ResumeDownloadMenuAction(context, bittorrentDownload, R.string.resume_torrent_menu_action));
                    }
                }
            }
        }
        if (bittorrentDownload.getState() == TransferState.FINISHED) {
            items.add(new SeedAction(context, bittorrentDownload));
        }
        if (bittorrentDownload.getState() == TransferState.SEEDING) {
            items.add(new StopSeedingAction(context, bittorrentDownload));
        }
        items.add(new CancelMenuAction(context, bittorrentDownload, !bittorrentDownload.isComplete()));
        items.add(new CopyToClipboardMenuAction(context,
                R.drawable.contextmenu_icon_magnet,
                R.string.transfers_context_menu_copy_magnet,
                R.string.transfers_context_menu_copy_magnet_copied,
                bittorrentDownload.magnetUri() + BTEngine.getInstance().magnetPeers()
        ));
        items.add(new CopyToClipboardMenuAction(context,
                R.drawable.contextmenu_icon_copy,
                R.string.transfers_context_menu_copy_infohash,
                R.string.transfers_context_menu_copy_infohash_copied,
                bittorrentDownload.getInfoHash()
        ));
        if (bittorrentDownload.isComplete()) {
            // Remove Torrent and Data action.
            items.add(new CancelMenuAction(context, bittorrentDownload, true, true));
        }
        if (bittorrentDownload instanceof UIBittorrentDownload) {
            UIBittorrentDownload uidl = (UIBittorrentDownload) bittorrentDownload;
            if (uidl.hasPaymentOptions()) {
                PaymentOptions po = uidl.getPaymentOptions();
                if (po.bitcoin != null) {
                    items.add(new SendBitcoinTipAction(context, po.bitcoin));
                }
                if (po.paypalUrl != null) {
                    items.add(new SendFiatTipAction(context, po.paypalUrl));
                }
                if (po.bitcoin != null) {
                    items.add(new SendBitcoinTipAction(context, po.bitcoin));
                }
            }
            if (bittorrentDownload.getInfoHash() != null && !"".equals(bittorrentDownload.getInfoHash())) {
                items.add(new TransferDetailsMenuAction(context, R.string.show_torrent_details, bittorrentDownload.getInfoHash()));
            }
        }
        return title;
    }

    private String extractMime(Transfer download) {
        return UIUtils.getMimeType(download.getSavePath().getAbsolutePath());
    }

    private void trackDialog(Dialog dialog) {
        dialogs.add(dialog);
    }

    private boolean showTransferItemMenu(View v) {
        try {
            MenuAdapter adapter = getMenuAdapter(v);
            if (adapter != null) {
                trackDialog(new MenuBuilder(adapter).show());
                return true;
            }
        } catch (Throwable e) {
            LOG.error("Failed to create the menu", e);
        }
        return false;
    }

    public static final class ViewHolder extends RecyclerView.ViewHolder {
        private WeakReference<Resources> resourcesRef;
        private WeakReference<TransferListAdapter> adapterRef;
        private OnClickListener viewOnClickListener;
        private ViewOnLongClickListener viewOnLongClickListener;
        private OpenOnClickListener playOnClickListener;
        private TransferDetailsClickListener transferDetailsClickListener;
        private final TransferStateStrings transferStateStrings;

        // Performance: Cache formatted strings to avoid repeated allocations
        private String cachedSpeedString = "";
        private long lastSpeedValue = -1;
        private String cachedSizeString = "";
        private long lastSizeValue = -1;

        private TextView title;
        private ProgressBar progress;
        private TextView status;
        private TextView speed;
        private TextView size;
        private TextView seeds;
        private TextView peers;
        private ImageButton buttonPlay;
        private ImageButton buttonDetails;
        private ImageView fileTypeIndicatorImageView;

        public ViewHolder(TransferListAdapter adapter,
                          Context context,
                          View itemView,
                          ViewOnClickListener viewOnClickListener,
                          ViewOnLongClickListener viewOnLongClickListener,
                          OpenOnClickListener openOnClickListener,
                          TransferDetailsClickListener transferDetailsClickListener) {
            super(itemView);
            this.resourcesRef = Ref.weak(context.getResources());
            this.adapterRef = Ref.weak(adapter);
            this.viewOnClickListener = viewOnClickListener;
            this.viewOnLongClickListener = viewOnLongClickListener;
            this.playOnClickListener = openOnClickListener;
            this.transferDetailsClickListener = transferDetailsClickListener;
            this.transferStateStrings = TransferStateStrings.getInstance(context);
        }

        public void updateView(int position) {
            if (Ref.alive(adapterRef)) {
                TransferListAdapter transferListAdapter = adapterRef.get();
                if (transferListAdapter.list != null && !transferListAdapter.list.isEmpty()) {
                    Transfer transfer = transferListAdapter.list.get(position);
                    LinearLayout listItemLinearLayoutHolder = (LinearLayout) itemView;
                    listItemLinearLayoutHolder.setOnClickListener(viewOnClickListener);
                    listItemLinearLayoutHolder.setOnLongClickListener(viewOnLongClickListener);
                    listItemLinearLayoutHolder.setClickable(true);
                    listItemLinearLayoutHolder.setLongClickable(true);
                    listItemLinearLayoutHolder.setTag(transfer);
                    try {
                        if (transfer instanceof BittorrentDownload) {
                            populateBittorrentDownload(listItemLinearLayoutHolder, (BittorrentDownload) transfer);
                        } else if (transfer instanceof SoundcloudDownload) {
                            populateCloudDownload(listItemLinearLayoutHolder, transfer);
                        } else if (transfer instanceof HttpDownload) {
                            populateHttpDownload(listItemLinearLayoutHolder, (HttpDownload) transfer);
                        }
                    } catch (Throwable e) {
                        LOG.error("Not able to populate group view in expandable list:" + e.getMessage());
                    }
                }
            }
        }

        /**
         * Performance optimization: Get cached speed string or format new one if speed changed.
         * Avoids repeated String.format() and UIUtils.getBytesInHuman() calls when speed unchanged.
         */
        private String getSpeedString(long speedBytes) {
            if (speedBytes != lastSpeedValue) {
                lastSpeedValue = speedBytes;
                cachedSpeedString = UIUtils.getBytesInHuman(speedBytes) + "/s";
            }
            return cachedSpeedString;
        }

        /**
         * Performance optimization: Get cached size string or format new one if size changed.
         * Avoids repeated UIUtils.getBytesInHuman() calls when size unchanged.
         */
        private String getSizeString(double sizeBytes) {
            long sizeLong = (long) sizeBytes;
            if (sizeLong != lastSizeValue) {
                lastSizeValue = sizeLong;
                cachedSizeString = UIUtils.getBytesInHuman(sizeLong);
            }
            return cachedSizeString;
        }

        private void ensureComponentsReferenced(View view) {
            if (title == null) {
                title = view.findViewById(R.id.view_transfer_list_item_title);
            }
            if (progress == null) {
                progress = view.findViewById(R.id.view_transfer_list_item_progress);
            }
            if (status == null) {
                status = view.findViewById(R.id.view_transfer_list_item_status);
            }
            if (speed == null) {
                speed = view.findViewById(R.id.view_transfer_list_item_speed);
            }
            if (size == null) {
                size = view.findViewById(R.id.view_transfer_list_item_size);
            }
            if (seeds == null) {
                seeds = view.findViewById(R.id.view_transfer_list_item_seeds);
            }
            if (peers == null) {
                peers = view.findViewById(R.id.view_transfer_list_item_peers);
            }
            if (buttonPlay == null) {
                buttonPlay = view.findViewById(R.id.view_transfer_list_item_button_play);
            }
            if (buttonDetails == null) {
                buttonDetails = view.findViewById(R.id.view_transfer_list_item_button_details);
            }
            if (fileTypeIndicatorImageView == null) {
                fileTypeIndicatorImageView = view.findViewById(R.id.view_transfer_list_item_download_type_indicator);
            }
        }

        private void populateHttpDownload(LinearLayout view, HttpDownload download) {
            ensureComponentsReferenced(view);
            seeds.setText("");
            peers.setText("");
            title.setText(download.getDisplayName());
            title.setCompoundDrawables(null, null, null, null);
            setProgress(progress, download.getProgress());
            String downloadStatus = transferStateStrings.get(download.getState());
            status.setText(downloadStatus);
            // Performance: Use cached string formatting
            speed.setText(getSpeedString(download.getDownloadSpeed()));
            size.setText(getSizeString(download.getSize()));
            buttonDetails.setVisibility(View.GONE);
            File previewFile = download.previewFile();
            if (previewFile != null && StreamableUtils.isStreamable(previewFile.getName())) {
                buttonPlay.setTag(previewFile);
                buttonPlay.setVisibility(View.VISIBLE);
                buttonPlay.setOnClickListener(playOnClickListener);
            } else {
                buttonPlay.setVisibility(View.GONE);
            }
            populateFileTypeIndicatorImageView(download);
        }

        private void populateBittorrentDownload(LinearLayout view, BittorrentDownload download) {
            ensureComponentsReferenced(view);
            Context context = view.getContext();
            seeds.setText(context.getString(R.string.seeds_n, formatSeeds(download)));
            peers.setText(context.getString(R.string.peers_n, formatPeers(download)));
            seeds.setVisibility(View.VISIBLE);
            peers.setVisibility(View.VISIBLE);
            title.setText(download.getDisplayName());
            setProgress(progress, download.getProgress());
            title.setCompoundDrawables(null, null, null, null);
            if (download instanceof UIBittorrentDownload && download.getInfoHash() != null && !"".equals(download.getInfoHash())) {
                buttonDetails.setTag(download);
                buttonDetails.setVisibility(View.VISIBLE);
                buttonDetails.setOnClickListener(transferDetailsClickListener);
                ((UIBittorrentDownload) download).checkSequentialDownload();
            } else {
                buttonDetails.setVisibility(View.GONE);
                buttonDetails.setOnClickListener(null);
            }

            buttonPlay.setVisibility(View.GONE);
            buttonPlay.setOnClickListener(null);

            final String downloadStatus = transferStateStrings.get(download.getState());
            // Performance: Cache network status check result in NetworkManager to avoid repeated system calls
            if (!NetworkManager.instance().isInternetDataConnectionUp()) {
                // Only format this compound string when network is down (rare case)
                status.setText(downloadStatus + " (" + view.getResources().getText(R.string.check_internet_connection) + ")");
                seeds.setText("");
                peers.setText("");
            } else {
                status.setText(downloadStatus);
            }
            // Performance: Use cached string formatting
            speed.setText(getSpeedString(download.getDownloadSpeed()));
            size.setText(getSizeString(download.getSize()));
            if (download instanceof UIBittorrentDownload) {
                UIBittorrentDownload uidl = (UIBittorrentDownload) download;
                if (uidl.hasPaymentOptions()) {
                    setPaymentOptionDrawable(uidl, title);
                }
            }
            populateFileTypeIndicatorImageView(download);
        }

        private void populateCloudDownload(LinearLayout view, Transfer download) {
            ensureComponentsReferenced(view);
            seeds.setText("");
            peers.setText("");
            title.setText(download.getDisplayName());
            title.setCompoundDrawables(null, null, null, null);
            setProgress(progress, download.getProgress());
            status.setText(transferStateStrings.get(download.getState()));
            // Performance: Use cached string formatting
            speed.setText(getSpeedString(download.getDownloadSpeed()));
            size.setText(getSizeString(download.getSize()));
            buttonDetails.setVisibility(View.GONE);
            File previewFile = download.previewFile();
            if (previewFile != null) {
                buttonPlay.setTag(previewFile);
                buttonPlay.setVisibility(View.VISIBLE);
                buttonPlay.setOnClickListener(playOnClickListener);
            } else {
                buttonPlay.setVisibility(View.GONE);
            }
            populateFileTypeIndicatorImageView(download);
        }

        private void populateFileTypeIndicatorImageView(Transfer download) {
            File savePath = download.getSavePath();
            String ext = null;
            if (download instanceof BittorrentDownload) {
                ext = ((BittorrentDownload) download).getPredominantFileExtension();
            } else if (savePath != null) {
                ext = FilenameUtils.getExtension(savePath.getAbsolutePath());
            }
            fileTypeIndicatorImageView.setImageResource(MediaType.getFileTypeIconId(ext));
        }

        private void setPaymentOptionDrawable(UIBittorrentDownload download, TextView title) {
            // BUGFIX: Logic was inverted - should return when NOT alive, not when alive
            if (!Ref.alive(resourcesRef)) {
                return;
            }
            final PaymentOptions paymentOptions = download.getPaymentOptions();
            final Resources r = resourcesRef.get();
            Drawable tipDrawable = androidx.core.content.ContextCompat.getDrawable(itemView.getContext(),
                    paymentOptions.bitcoin != null ? R.drawable.contextmenu_icon_donation_bitcoin : R.drawable.contextmenu_icon_donation_fiat);
            if (tipDrawable != null) {
                final int iconHeightInPixels = r.getDimensionPixelSize(R.dimen.view_transfer_list_item_title_left_drawable);
                //noinspection SuspiciousNameCombination
                tipDrawable.setBounds(0, 0, iconHeightInPixels, iconHeightInPixels);
                title.setCompoundDrawables(tipDrawable, null, null, null);
            }
        }
    } // ViewHolder


    // at least one phone does not provide this trivial optimization
    // TODO: move this for a more framework like place, like a Views (utils) class
    private static void setProgress(ProgressBar v, int progress) {
        int old = v.getProgress();
        if (old != progress) {
            v.setProgress(progress);
        }
    }

    private final class ViewOnClickListener implements OnClickListener {
        public void onClick(View v) {
            showTransferItemMenu(v);
        }
    }

    private final class ViewOnLongClickListener implements OnLongClickListener {
        public boolean onLongClick(View v) {
            return showTransferItemMenu(v);
        }
    }

    private static final class OpenOnClickListener extends ClickAdapter<Context> {

        public OpenOnClickListener(Context ctx) {
            super(ctx);
        }

        public void onClick(Context ctx, View v) {
            Object tag = v.getTag();
            if (tag instanceof TransferItem) {
                TransferItem item = (TransferItem) tag;
                File path = item.isComplete() ? item.getFile() : null;
                if (path == null && item instanceof BTDownloadItem) {
                    path = previewFile((BTDownloadItem) item);
                }
                if (path != null) {
                    UIUtils.openFile(ctx, path);
                }
            } else if (tag instanceof File) {
                UIUtils.openFile(ctx, (File) tag);
            }
        }
    }

    private static final class TransferDetailsClickListener extends ClickAdapter<Context> {
        public TransferDetailsClickListener(Context owner) {
            super(owner);
        }

        @Override
        public void onClick(Context owner, View v) {
            Object transfer = v.getTag();
            if (transfer instanceof UIBittorrentDownload) {
                String infoHash = ((UIBittorrentDownload) transfer).getInfoHash();
                new TransferDetailsMenuAction(owner, R.string.show_torrent_details, infoHash).setClickedView(v).onClick();
            }
        }
    }

    private static File previewFile(BTDownloadItem item) {
        if (item != null) {
            long downloaded = item.getSequentialDownloaded();
            long size = item.getSize();
            if (size > 0) {
                long percent = (100 * downloaded) / size;
                if (percent > 30 || downloaded > 10 * 1024 * 1024) {
                    return item.getFile();
                } else {
                    return null;
                }
            }
        }
        return null;
    }

    /**
     * Removes a transfer item from the adapter immediately and notifies observers.
     * Marks the transfer ID as removed to prevent it from being re-added by updateList() calls.
     * Must be called from the UI thread.
     */
    public void removeTransferItem(Transfer transfer) {
        if (transfer == null || list == null) {
            return;
        }

        // Track this transfer ID as removed to prevent re-adding
        String transferId = resolveTransferId(transfer);
        removedTransferIds.add(transferId);

        // Clear any pending updates to prevent re-adding this transfer
        pendingUpdate = null;

        // Remove from underlying data structures
        list.remove(transfer);

        // Clear UI state cache to prevent this transfer from being restored
        previousUiStates.clear();

        // Notify the UI
        try {
            notifyDataSetChanged();
        } catch (IllegalStateException e) {
            // RecyclerView is computing layout or scrolling, defer the notification
            LOG.warn("RecyclerView computing layout during removeTransferItem, deferring refresh: " + e.getMessage());
            deferNotifyDataSetChanged();
        }
    }

    /**
     * Format peers count efficiently without excessive string allocations.
     * Optimized to avoid String.replaceAll() which creates multiple intermediate strings.
     */
    private static String formatPeers(BittorrentDownload dl) {
        int connectedPeers = dl.getConnectedPeers();
        int peers = dl.getTotalPeers();
        if (connectedPeers > peers) {
            return String.valueOf(connectedPeers);
        }
        return connectedPeers + " / " + peers;
    }

    /**
     * Format seeds count efficiently without excessive string allocations.
     * Optimized to avoid String.replaceAll() which creates multiple intermediate strings.
     */
    private static String formatSeeds(BittorrentDownload dl) {
        int connectedSeeds = dl.getConnectedSeeds();
        int seeds = dl.getTotalSeeds();
        if (connectedSeeds > seeds) {
            return String.valueOf(connectedSeeds);
        }
        String seedsStr = (seeds == -1) ? "?" : String.valueOf(seeds);
        return connectedSeeds + " / " + seedsStr;
    }
}




