package net.sf.jabref.gui.mergeentries;

import java.awt.Font;
import java.io.IOException;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;

import javax.swing.BorderFactory;
import javax.swing.ButtonGroup;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.JScrollPane;
import javax.swing.JSeparator;
import javax.swing.JTextArea;
import javax.swing.JTextPane;
import javax.swing.ScrollPaneConstants;
import javax.swing.SwingUtilities;

import net.sf.jabref.Globals;
import net.sf.jabref.gui.PreviewPanel;
import net.sf.jabref.gui.util.component.DiffHighlightingTextPane;
import net.sf.jabref.logic.bibtex.BibEntryWriter;
import net.sf.jabref.logic.bibtex.LatexFieldFormatter;
import net.sf.jabref.logic.formatter.casechanger.SentenceCaseFormatter;
import net.sf.jabref.logic.l10n.Localization;
import net.sf.jabref.logic.util.strings.DiffHighlighting;
import net.sf.jabref.model.database.BibDatabaseMode;
import net.sf.jabref.model.entry.BibEntry;
import net.sf.jabref.model.entry.InternalBibtexFields;
import net.sf.jabref.preferences.JabRefPreferences;

import com.jgoodies.forms.layout.CellConstraints;
import com.jgoodies.forms.layout.ColumnSpec;
import com.jgoodies.forms.layout.FormLayout;
import com.jgoodies.forms.layout.RowSpec;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * @author Oscar Gustafsson
 *
 *         Class for dealing with merging entries
 */

public class MergeEntries {

    private static final Log LOGGER = LogFactory.getLog(MergeEntries.class);


    // Headings
    private final List<String> columnHeadings = Arrays.asList(Localization.lang("Field"),
            Localization.lang("Left entry"),
            Localization.lang("Left"),
            Localization.lang("None"),
            Localization.lang("Right"),
            Localization.lang("Right entry"));
    private static final String[] DIFF_MODES = {Localization.lang("Plain text"),
            Localization.lang("Show diff") + " - " + Localization.lang("word"),
            Localization.lang("Show diff") + " - " + Localization.lang("character"),
            Localization.lang("Show symmetric diff") + " - " + Localization.lang("word"),
            Localization.lang("Show symmetric diff") + " - " + Localization.lang("character")};

    private final Set<String> identicalFields = new HashSet<>();
    private final Set<String> differentFields = new HashSet<>();
    private final BibEntry mergedEntry = new BibEntry();
    private final BibEntry leftEntry;
    private final BibEntry rightEntry;
    private final BibDatabaseMode databaseType;
    private JScrollPane scrollPane;
    private JTextArea sourceView;
    private PreviewPanel entryPreview;
    private Boolean doneBuilding;
    private Boolean identicalTypes;
    private List<JRadioButton> typeRadioButtons;
    private final Set<String> allFields = new TreeSet<>();
    private final JComboBox<String> diffMode = new JComboBox<>();
    private final Map<String, JTextPane> leftTextPanes = new HashMap<>();
    private final Map<String, JTextPane> rightTextPanes = new HashMap<>();
    private final Map<String, List<JRadioButton>> radioButtons = new HashMap<>();

    private final JPanel mainPanel = new JPanel();

    private static final String MARGIN = "10px";

    private static final List<JLabel> HEADING_LABELS = new ArrayList<>(6);

    private static final CellConstraints CELL_CONSTRAINTS = new CellConstraints();



    /**
     * Constructor taking two entries
     *
     * @param entryLeft Left entry
     * @param entryRight Right entry
     * @param type Bib database mode
     */
    public MergeEntries(BibEntry entryLeft, BibEntry entryRight, BibDatabaseMode type) {
        leftEntry = entryLeft;
        rightEntry = entryRight;
        this.databaseType = type;
        initialize();
    }

    /**
     * Constructor with optional column captions for the two entries
     *
     * @param entryLeft Left entry
     * @param entryRight Right entry
     * @param headingLeft Heading for left entry
     * @param headingRight Heading for right entry
     * @param type Bib database mode
     */
    public MergeEntries(BibEntry entryLeft, BibEntry entryRight, String headingLeft, String headingRight, BibDatabaseMode type) {
        columnHeadings.set(1, headingLeft);
        columnHeadings.set(5, headingRight);
        this.leftEntry = entryLeft;
        this.rightEntry = entryRight;

        this.databaseType = type;

        initialize();
    }

    /**
     * Main function for building the merge entry JPanel
     */
    private void initialize() {
        doneBuilding = false;
        setupFields();

        fillDiffModes();

        // Create main layout
        String colSpecMain = "left:pref, 5px, center:3cm:grow, 5px, center:pref, 3px, center:pref, 3px, center:pref, 5px, center:3cm:grow";
        String colSpecMerge = "left:pref, 5px, fill:3cm:grow, 5px, center:pref, 3px, center:pref, 3px, center:pref, 5px, fill:3cm:grow";
        String rowSpec = "pref, pref, 10px, fill:5cm:grow, 10px, pref, 10px, fill:3cm:grow";
        StringBuilder rowBuilder = new StringBuilder("");
        for (int i = 0; i < allFields.size(); i++) {
            rowBuilder.append("pref, 2dlu, ");
        }
        rowBuilder.append("pref");

        JPanel mergePanel = new JPanel();
        FormLayout mainLayout = new FormLayout(colSpecMain, rowSpec);
        FormLayout mergeLayout = new FormLayout(colSpecMerge, rowBuilder.toString());
        mainPanel.setLayout(mainLayout);
        mergePanel.setLayout(mergeLayout);

        setupHeadingRows();

        mainPanel.add(new JSeparator(), CELL_CONSTRAINTS.xyw(1, 3, 11));

        setupEntryTypeRow(mergePanel);

        int maxLabelWidth = setupFieldRows(mergePanel);


        // Create and add scrollpane
        scrollPane = new JScrollPane(mergePanel, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED,
                ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
        scrollPane.setBorder(BorderFactory.createEmptyBorder());
        updateTextPanes(allFields);
        mainPanel.add(scrollPane, CELL_CONSTRAINTS.xyw(1, 4, 11));
        mainPanel.add(new JSeparator(), CELL_CONSTRAINTS.xyw(1, 5, 11));

        synchronizeColumnWidths(mainLayout, mergeLayout, maxLabelWidth);

        // Setup a PreviewPanel and a Bibtex source box for the merged entry
        mainPanel.add(boldFontLabel(Localization.lang("Merged entry")), CELL_CONSTRAINTS.xyw(1, 6, 6));

        entryPreview = new PreviewPanel(null, mergedEntry, null);
        mainPanel.add(entryPreview, CELL_CONSTRAINTS.xyw(1, 8, 6));

        mainPanel.add(boldFontLabel(Localization.lang("Merged BibTeX source code")), CELL_CONSTRAINTS.xyw(8, 6, 4));

        sourceView = new JTextArea();
        sourceView.setLineWrap(true);
        sourceView.setFont(new Font("Monospaced", Font.PLAIN, Globals.prefs.getInt(JabRefPreferences.FONT_SIZE)));
        mainPanel.add(new JScrollPane(sourceView), CELL_CONSTRAINTS.xyw(8, 8, 4));
        sourceView.setEditable(false);

        // Add some margin around the layout
        mainLayout.appendRow(RowSpec.decode(MARGIN));
        mainLayout.appendColumn(ColumnSpec.decode(MARGIN));
        mainLayout.insertRow(1, RowSpec.decode(MARGIN));
        mainLayout.insertColumn(1, ColumnSpec.decode(MARGIN));

        // Everything done, allow any action to actually update the merged entry
        doneBuilding = true;

        updateAll();

        // Show what we've got
        mainPanel.setVisible(true);
        SwingUtilities.invokeLater(() -> scrollPane.getVerticalScrollBar().setValue(0));
    }

    private int setupFieldRows(JPanel mergePanel) {
        // For all fields in joint add a row and possibly radio buttons
        int row = 2;
        int maxLabelWidth = -1;
        for (String field : allFields) {
            JLabel label = boldFontLabel(new SentenceCaseFormatter().format(field));
            mergePanel.add(label, CELL_CONSTRAINTS.xy(1, (2 * row) - 1, "left, top"));
            Optional<String> leftString = leftEntry.getField(field);
            Optional<String> rightString = rightEntry.getField(field);
            if (leftString.equals(rightString)) {
                identicalFields.add(field);
            } else {
                differentFields.add(field);
            }

            maxLabelWidth = Math.max(maxLabelWidth, label.getPreferredSize().width);

            // Left text pane
            if (leftString.isPresent()) {
                JTextPane tf = new DiffHighlightingTextPane();
                mergePanel.add(tf, CELL_CONSTRAINTS.xy(3, (2 * row) - 1, "f, f"));
                leftTextPanes.put(field, tf);
            }

            // Add radio buttons if the two entries do not have identical fields
            if (identicalFields.contains(field)) {
                mergedEntry.setField(field, leftString.get()); // Will only happen if both entries have the field and the content is identical
            } else {
                ButtonGroup group = new ButtonGroup();
                List<JRadioButton> list = new ArrayList<>(3);
                for (int k = 0; k < 3; k++) {
                    JRadioButton button = new JRadioButton();
                    group.add(button);
                    mergePanel.add(button, CELL_CONSTRAINTS.xy(5 + (k * 2), (2 * row) - 1));
                    button.addChangeListener(e -> updateAll());
                    list.add(button);
                }
                radioButtons.put(field, list);
                if (leftString.isPresent()) {
                    list.get(0).setSelected(true);
                    if (!rightString.isPresent()) {
                        list.get(2).setEnabled(false);
                    }
                } else {
                    list.get(0).setEnabled(false);
                    list.get(2).setSelected(true);
                }
            }

            // Right text pane
            if (rightString.isPresent()) {
                JTextPane tf = new DiffHighlightingTextPane();
                mergePanel.add(tf, CELL_CONSTRAINTS.xy(11, (2 * row) - 1, "f, f"));
                rightTextPanes.put(field, tf);
            }
            row++;
        }
        return maxLabelWidth;
    }

    private void setupEntryTypeRow(JPanel mergePanel) {
        // Start with entry type
        mergePanel.add(boldFontLabel(Localization.lang("Entry type")), CELL_CONSTRAINTS.xy(1, 1));

        JTextPane leftTypeDisplay = new DiffHighlightingTextPane();
        leftTypeDisplay.setText(DiffHighlighting.HTML_START + leftEntry.getType() + DiffHighlighting.HTML_END);
        mergePanel.add(leftTypeDisplay, CELL_CONSTRAINTS.xy(3, 1));
        if (leftEntry.getType().equals(rightEntry.getType())) {
            identicalTypes = true;
        } else {
            identicalTypes = false;
            ButtonGroup group = new ButtonGroup();
            typeRadioButtons = new ArrayList<>(2);
            for (int k = 0; k < 3; k += 2) {
                JRadioButton button = new JRadioButton();
                typeRadioButtons.add(button);
                group.add(button);
                mergePanel.add(button, CELL_CONSTRAINTS.xy(5 + (k * 2), 1));
                button.addChangeListener(e -> updateAll());
            }
            typeRadioButtons.get(0).setSelected(true);
        }
        JTextPane rightTypeDisplay = new DiffHighlightingTextPane();
        rightTypeDisplay.setText(DiffHighlighting.HTML_START + rightEntry.getType() + DiffHighlighting.HTML_END);
        mergePanel.add(rightTypeDisplay, CELL_CONSTRAINTS.xy(11, 1));
    }

    private void setupHeadingRows() {
        mainPanel.add(boldFontLabel(Localization.lang("Use")), CELL_CONSTRAINTS.xyw(4, 1, 7, "center, bottom"));
        mainPanel.add(diffMode, CELL_CONSTRAINTS.xy(11, 1, "right, bottom"));

        // Set headings
        for (int i = 0; i < 6; i++) {
            HEADING_LABELS.add(boldFontLabel(columnHeadings.get(i)));
            mainPanel.add(HEADING_LABELS.get(i), CELL_CONSTRAINTS.xy(1 + (i * 2), 2));
        }
    }

    private void fillDiffModes() {
        // Fill diff mode combo box
        for (String diffText : DIFF_MODES) {
            diffMode.addItem(diffText);
        }
        diffMode.setSelectedIndex(
                Math.min(Globals.prefs.getInt(JabRefPreferences.MERGE_ENTRIES_DIFF_MODE), diffMode.getItemCount() - 1));
        diffMode.addActionListener(e -> {
            updateTextPanes(differentFields);
            storePreference();
        });
    }

    private void synchronizeColumnWidths(FormLayout mainLayout, FormLayout mergeLayout,
            int maxLabelWidth) {
        // Synchronize column widths
        String[] rbAlign = {"right", "center", "left"};
        mainLayout.setColumnSpec(1, ColumnSpec.decode(Integer.toString(maxLabelWidth) + "px"));
        Integer maxRBWidth = -1;
        for (int k = 2; k < 5; k++) {
            maxRBWidth = Math.max(maxRBWidth, HEADING_LABELS.get(k).getPreferredSize().width);
        }
        for (int k = 0; k < 3; k++) {
            mergeLayout.setColumnSpec(5 + (k * 2), ColumnSpec.decode(rbAlign[k] + ":" + maxRBWidth + "px"));
        }
    }

    private JLabel boldFontLabel(String text) {
        JLabel label = new JLabel(text);
        Font font = label.getFont();
        label.setFont(font.deriveFont(font.getStyle() | Font.BOLD));
        return label;
    }

    private void storePreference() {
        Globals.prefs.putInt(JabRefPreferences.MERGE_ENTRIES_DIFF_MODE, diffMode.getSelectedIndex());
    }

    private void setupFields() {
        allFields.addAll(leftEntry.getFieldNames());
        allFields.addAll(rightEntry.getFieldNames());

        // Remove internal fields
        Set<String> toberemoved = new TreeSet<>();
        for (String field : allFields) {
            if (InternalBibtexFields.isInternalField(field)) {
                toberemoved.add(field);
            }
        }
        allFields.removeAll(toberemoved);
    }

    private void updateTextPanes(Collection<String> fields) {
        int oldScrollPaneValue = scrollPane.getVerticalScrollBar().getValue();
        for (String field : fields) {
            String leftString = leftEntry.getField(field).orElse("");
            String rightString = rightEntry.getField(field).orElse("");
            switch (diffMode.getSelectedIndex()) {
            case 0: // Plain text
                break;
            case 1: // Latexdiff style - word
                rightString = DiffHighlighting.generateDiffHighlighting(leftString, rightString, " ");
                break;
            case 2: // Latexdiff style - character
                rightString = DiffHighlighting.generateDiffHighlighting(leftString, rightString, "");
                break;
            case 3: // Symmetric style - word
                String tmpLeftString = DiffHighlighting.generateSymmetricHighlighting(leftString, rightString, " ");
                rightString = DiffHighlighting.generateSymmetricHighlighting(rightString, leftString, " ");
                leftString = tmpLeftString;
                break;
            case 4: // Symmetric style - character
                tmpLeftString = DiffHighlighting.generateSymmetricHighlighting(leftString, rightString, "");
                rightString = DiffHighlighting.generateSymmetricHighlighting(rightString, leftString, "");
                leftString = tmpLeftString;
                break;
            default: // Shouldn't happen
                break;
            }
            if ((leftString != null) && leftTextPanes.containsKey(field)) {
                leftTextPanes.get(field).setText(DiffHighlighting.HTML_START + leftString + DiffHighlighting.HTML_END);
            }
            if ((rightString != null) && rightTextPanes.containsKey(field)) {
                rightTextPanes.get(field).setText(DiffHighlighting.HTML_START + rightString + DiffHighlighting.HTML_END);
            }
        }
        SwingUtilities.invokeLater(() -> scrollPane.getVerticalScrollBar()
                .setValue(Math.min(scrollPane.getVerticalScrollBar().getMaximum(), oldScrollPaneValue)));
    }


    /**
     * @return Merged BibEntry
     */
    public BibEntry getMergeEntry() {
        return mergedEntry;
    }

    /**
     * @return The merge entry JPanel
     */
    public JPanel getMergeEntryPanel() {
        return mainPanel;
    }

    /**
     * Update the merged BibEntry with source and preview panel every time something is changed
     */
    private void updateAll() {
        if (!doneBuilding) {
            // If we are not done adding everything, do not do anything...
            return;
        }
        // Check if the type has changed
        if (!identicalTypes && typeRadioButtons.get(0).isSelected()) {
            mergedEntry.setType(leftEntry.getType());
        } else {
            mergedEntry.setType(rightEntry.getType());
        }

        // Check the potentially different fields
        for (String field : differentFields) {
            if (radioButtons.get(field).get(0).isSelected()) {
                mergedEntry.setField(field, leftEntry.getField(field).get()); // Will only happen if field exists
            } else if (radioButtons.get(field).get(2).isSelected()) {
                mergedEntry.setField(field, rightEntry.getField(field).get()); // Will only happen if field exists
            } else {
                mergedEntry.clearField(field);
            }
        }

        // Update the PreviewPanel
        entryPreview.setEntry(mergedEntry);

        // Update the BibTeX source view
        StringWriter writer = new StringWriter();
        try {
            new BibEntryWriter(new LatexFieldFormatter(Globals.prefs.getLatexFieldFormatterPreferences()),
                    false).write(mergedEntry, writer, databaseType);
        } catch (IOException ex) {
            LOGGER.error("Error in entry", ex);
        }
        sourceView.setText(writer.getBuffer().toString());
        sourceView.setCaretPosition(0);
    }
}
