package net.sf.jabref.gui;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import javax.swing.SwingUtilities;

import net.sf.jabref.JabRefExecutorService;
import net.sf.jabref.JabRefGUI;
import net.sf.jabref.gui.DuplicateResolverDialog.DuplicateResolverResult;
import net.sf.jabref.gui.DuplicateResolverDialog.DuplicateResolverType;
import net.sf.jabref.gui.undo.NamedCompound;
import net.sf.jabref.gui.undo.UndoableInsertEntry;
import net.sf.jabref.gui.undo.UndoableRemoveEntry;
import net.sf.jabref.gui.worker.CallBack;
import net.sf.jabref.logic.l10n.Localization;
import net.sf.jabref.model.DuplicateCheck;
import net.sf.jabref.model.entry.BibEntry;

import spin.Spin;

public class DuplicateSearch implements Runnable {

    private final BasePanel panel;
    private List<BibEntry> bes;
    private final List<List<BibEntry>> duplicates = new ArrayList<>();


    public DuplicateSearch(BasePanel bp) {
        panel = bp;
    }

    @Override
    public void run() {

        panel.output(Localization.lang("Searching for duplicates..."));

        bes = panel.getDatabase().getEntries();
        if (bes.size() < 2) {
            return;
        }

        SearcherRunnable st = new SearcherRunnable();
        JabRefExecutorService.INSTANCE.executeInterruptableTask(st, "DuplicateSearcher");
        int current = 0;

        final List<BibEntry> toRemove = new ArrayList<>();
        final List<BibEntry> toAdd = new ArrayList<>();

        int duplicateCounter = 0;
        boolean autoRemoveExactDuplicates = false;

        synchronized (duplicates) {
            while (!st.finished() || (current < duplicates.size())) {

                if (current >= duplicates.size()) {
                    // wait until the search thread puts something into duplicates vector
                    // or finish its work

                    try {
                        duplicates.wait();
                    } catch (InterruptedException ignored) {
                        // Ignore
                    }

                } else { // duplicates found
                    List<BibEntry> be = duplicates.get(current);
                    current++;
                    if (!toRemove.contains(be.get(0)) && !toRemove.contains(be.get(1))) {
                        // Check if they are exact duplicates:
                        boolean askAboutExact = false;
                        if (DuplicateCheck.compareEntriesStrictly(be.get(0), be.get(1)) > 1) {
                            if (autoRemoveExactDuplicates) {
                                toRemove.add(be.get(1));
                                duplicateCounter++;
                                continue;
                            }
                            askAboutExact = true;
                        }

                        DuplicateCallBack cb = new DuplicateCallBack(JabRefGUI.getMainFrame(), be.get(0), be.get(1),
                                askAboutExact ? DuplicateResolverType.DUPLICATE_SEARCH_WITH_EXACT : DuplicateResolverType.DUPLICATE_SEARCH);
                        ((CallBack) Spin.over(cb)).update();

                        duplicateCounter++;
                        DuplicateResolverResult answer = cb.getSelected();
                        if ((answer == DuplicateResolverResult.KEEP_LEFT)
                                || (answer == DuplicateResolverResult.AUTOREMOVE_EXACT)) {
                            toRemove.add(be.get(1));
                            if (answer == DuplicateResolverResult.AUTOREMOVE_EXACT) {
                                autoRemoveExactDuplicates = true; // Remember choice
                            }
                        } else if (answer == DuplicateResolverResult.KEEP_RIGHT) {
                            toRemove.add(be.get(0));
                        } else if (answer == DuplicateResolverResult.BREAK) {
                            st.setFinished(); // thread killing
                            current = Integer.MAX_VALUE;
                            duplicateCounter--; // correct counter
                        } else if (answer == DuplicateResolverResult.KEEP_MERGE) {
                            toRemove.addAll(be);
                            toAdd.add(cb.getMergedEntry());
                        }
                    }
                }
            }
        }

        final NamedCompound ce = new NamedCompound(Localization.lang("duplicate removal"));

        final int dupliC = duplicateCounter;
        SwingUtilities.invokeLater(new Runnable() {

            @Override
            public void run() {
                // Now, do the actual removal:
                if (!toRemove.isEmpty()) {
                    for (BibEntry entry : toRemove) {
                        panel.getDatabase().removeEntry(entry);
                        ce.addEdit(new UndoableRemoveEntry(panel.getDatabase(), entry, panel));
                    }
                    panel.markBaseChanged();
                }
                // and adding merged entries:
                if (!toAdd.isEmpty()) {
                    for (BibEntry entry : toAdd) {
                        panel.getDatabase().insertEntry(entry);
                        ce.addEdit(new UndoableInsertEntry(panel.getDatabase(), entry, panel));
                    }
                    panel.markBaseChanged();
                }

                synchronized (duplicates) {
                    panel.output(Localization.lang("Duplicates found") + ": " + duplicates.size() + ' '
                            + Localization.lang("pairs processed") + ": " + dupliC);
                }
                ce.end();
                panel.getUndoManager().addEdit(ce);

            }

        });

    }


    class SearcherRunnable implements Runnable {

        private volatile boolean finished;

        @Override
        public void run() {
            for (int i = 0; (i < (bes.size() - 1)) && !finished; i++) {
                for (int j = i + 1; (j < bes.size()) && !finished; j++) {
                    BibEntry first = bes.get(i);
                    BibEntry second = bes.get(j);
                    boolean eq = DuplicateCheck.isDuplicate(first, second, panel.getBibDatabaseContext().getMode());

                    // If (suspected) duplicates, add them to the duplicates vector.
                    if (eq) {
                        synchronized (duplicates) {
                            duplicates.add(Arrays.asList(first, second));
                            duplicates.notifyAll(); // send wake up all
                        }
                    }
                }
            }
            finished = true;
            // if no duplicates found, the graphical thread will never wake up
            synchronized (duplicates) {
                duplicates.notifyAll();
            }
        }

        public boolean finished() {
            return finished;
        }

        // Thread cancel option
        // no synchronized used because no "really" critical situations expected
        public void setFinished() {
            finished = true;
        }
    }

    static class DuplicateCallBack implements CallBack {

        private DuplicateResolverResult reply = DuplicateResolverResult.NOT_CHOSEN;
        private final JabRefFrame frame;
        private final BibEntry one;
        private final BibEntry two;
        private final DuplicateResolverType dialogType;
        private BibEntry merged;


        public DuplicateCallBack(JabRefFrame frame, BibEntry one, BibEntry two, DuplicateResolverType dialogType) {
            this.frame = frame;
            this.one = one;
            this.two = two;
            this.dialogType = dialogType;
        }

        public DuplicateResolverResult getSelected() {
            return reply;
        }

        public BibEntry getMergedEntry() {
            return merged;
        }

        @Override
        public void update() {
            DuplicateResolverDialog diag = new DuplicateResolverDialog(frame, one, two, dialogType);
            diag.setVisible(true);
            diag.dispose();
            reply = diag.getSelected();
            merged = diag.getMergedEntry();
        }
    }

}
