package net.sf.jabref.gui.contentselector;

import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.DefaultListModel;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JList;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextField;

import net.sf.jabref.gui.BasePanel;
import net.sf.jabref.gui.JabRefFrame;
import net.sf.jabref.gui.help.HelpAction;
import net.sf.jabref.gui.keyboard.KeyBinder;
import net.sf.jabref.logic.help.HelpFile;
import net.sf.jabref.logic.l10n.Localization;
import net.sf.jabref.model.entry.FieldName;
import net.sf.jabref.model.metadata.ContentSelector;
import net.sf.jabref.model.metadata.ContentSelectors;
import net.sf.jabref.model.metadata.MetaData;

import com.jgoodies.forms.builder.ButtonBarBuilder;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class ContentSelectorDialog extends JDialog {

    private final GridBagLayout gbl = new GridBagLayout();
    private final GridBagConstraints con = new GridBagConstraints();
    private final JPanel fieldPan = new JPanel();
    private final JPanel wordPan = new JPanel();
    private final JPanel buttonPan = new JPanel();
    private final JPanel fieldNamePan = new JPanel();
    private final JPanel wordEditPan = new JPanel();

    private static final String WORD_FIRSTLINE_TEXT = Localization.lang("<select word>");
    private static final String FIELD_FIRST_LINE = Localization.lang("<field name>");
    private final MetaData metaData;
    private String currentField;
    private final JabRefFrame frame;
    private final BasePanel panel;
    private final JButton newField = new JButton(Localization.lang("New"));
    private final JButton removeField = new JButton(Localization.lang("Remove"));
    private final JButton newWord = new JButton(Localization.lang("New"));
    private final JButton removeWord = new JButton(Localization.lang("Remove"));
    private final JButton ok = new JButton(Localization.lang("OK"));
    private final JButton cancel = new JButton();
    private final JButton apply = new JButton(Localization.lang("Apply"));
    private final DefaultListModel<String> fieldListModel = new DefaultListModel<>();
    private DefaultListModel<String> wordListModel = new DefaultListModel<>();
    private final JList<String> fieldList = new JList<>(fieldListModel);
    private final JList<String> wordList = new JList<>(wordListModel);
    private final JTextField fieldNameField = new JTextField("", 20);
    private final JTextField wordEditField = new JTextField("", 20);
    private final JScrollPane fPane = new JScrollPane(fieldList);
    private final JScrollPane wPane = new JScrollPane(wordList);

    private final Map<String, DefaultListModel<String>> wordListModels = new HashMap<>();
    private final List<String> removedFields = new ArrayList<>();

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


    /**
     *
     * @param owner the parent Window (Dialog or Frame)
     * @param frame the JabRef Frame
     * @param panel the currently selected BasePanel
     * @param modal should this dialog be modal?
     * @param fieldName the field this selector is initialized for. May be null.
     */
    public ContentSelectorDialog(Window owner, JabRefFrame frame, BasePanel panel, boolean modal, String fieldName) {
        super(owner, Localization.lang("Manage content selectors"));
        this.setModal(modal);
        this.metaData = panel.getBibDatabaseContext().getMetaData();
        this.frame = frame;
        this.panel = panel;
        this.currentField = fieldName;

        initLayout();

        setupFieldSelector();
        if (currentField != null) {
            int fieldInd = fieldListModel.indexOf(currentField);
            if (fieldInd >= 0) {
                fieldList.setSelectedIndex(fieldInd);
            }
        } else {
            if (!fieldListModel.isEmpty()) {
                fieldList.setSelectedIndex(0);
                currentField = fieldList.getSelectedValue();
            }
        }

        setupWordSelector();
        setupActions();
        KeyBinder.bindCloseDialogKeyToCancelAction(this.rootPane, cancel.getAction());

        pack();
    }

    private void setupActions() {

        wordList.addListSelectionListener(e -> {
            wordEditField.setText(wordList.getSelectedValue());
            wordEditField.selectAll();
            wordEditField.requestFocus();
        });

        newWord.addActionListener(e -> newWordAction());

        ActionListener wordEditFieldListener = e -> actOnWordEdit();
        wordEditField.addActionListener(wordEditFieldListener);

        removeWord.addActionListener(e -> {
            int index = wordList.getSelectedIndex();
            if (index == -1) {
                return;
            }
            wordListModel.remove(index);
            wordEditField.setText("");
            if (!wordListModel.isEmpty()) {
                wordList.setSelectedIndex(Math.min(index, wordListModel.size() - 1));
            }
        });

        fieldList.addListSelectionListener(e -> {
            currentField = fieldList.getSelectedValue();
            fieldNameField.setText("");
            setupWordSelector();
        });

        newField.addActionListener(e -> {
            if (!fieldListModel.get(0).equals(FIELD_FIRST_LINE)) {
                // only add <field name> once
                fieldListModel.add(0, FIELD_FIRST_LINE);
            }
            fieldList.setSelectedIndex(0);
            fPane.getVerticalScrollBar().setValue(0);
            fieldNameField.setEnabled(true);
            fieldNameField.setText(currentField);
            fieldNameField.selectAll();
            fieldNameField.requestFocus();
        });

        fieldNameField.addActionListener(e -> fieldNameField.transferFocus());

        fieldNameField.addFocusListener(new FieldNameFocusAdapter());

        removeField.addActionListener(e -> {
            int index = fieldList.getSelectedIndex();
            if (index == -1) {
                return;
            }
            String fieldName = fieldListModel.get(index);
            removedFields.add(fieldName);
            fieldListModel.remove(index);
            wordListModels.remove(fieldName);
            fieldNameField.setText("");
            if (!fieldListModel.isEmpty()) {
                fieldList.setSelectedIndex(Math.min(index, wordListModel.size() - 1));
            }
        });

        ok.addActionListener(e -> {
            try {
                applyChanges();
                dispose();
            } catch (Exception ex) {
                LOGGER.info("Could not apply changes in \"Manage content selectors\"", ex);
                JOptionPane.showMessageDialog(frame, Localization.lang("Could not apply changes."));
            }
        });

        apply.addActionListener(e -> {
            // Store if an entry is currently being edited:
            if (!"".equals(wordEditField.getText())) {
                wordEditFieldListener.actionPerformed(null);
            }
            try {
                applyChanges();
            } catch (Exception ex) {
                LOGGER.info("Could not apply changes in \"Manage content selectors\"", ex);
                JOptionPane.showMessageDialog(frame, Localization.lang("Could not apply changes."));
            }
        });

        Action cancelAction = new AbstractAction() {

            @Override
            public void actionPerformed(ActionEvent e) {
                dispose();
            }
        };
        cancelAction.putValue(Action.NAME, Localization.lang("Cancel"));
        cancel.setAction(cancelAction);
    }

    private void actOnWordEdit() {
        String old = wordList.getSelectedValue();
        String newVal = wordEditField.getText();
        if ("".equals(newVal) || newVal.equals(old)) {
            return; // Empty string or no change.
        }
        int index = wordList.getSelectedIndex();
        if (wordListModel.contains(newVal)) {
            // ensure that word already in list is visible
            index = wordListModel.indexOf(newVal);
            wordList.ensureIndexIsVisible(index);
            return;
        }

        int newIndex = findPos(wordListModel, newVal);
        if (index >= 0) {
            // initiate replacement of selected word
            wordListModel.remove(index);
            if (newIndex > index) {
                // newIndex has to be adjusted after removal of previous entry
                newIndex--;
            }
        }
        wordListModel.add(newIndex, newVal);
        wordList.ensureIndexIsVisible(newIndex);
        wordEditField.selectAll();
    }

    private void newWordAction() {
        if (wordListModel.isEmpty() || !wordListModel.get(0).equals(WORD_FIRSTLINE_TEXT)) {
            wordListModel.add(0, WORD_FIRSTLINE_TEXT);
        }
        wordList.setSelectedIndex(0);
        wPane.getVerticalScrollBar().setValue(0);
    }

    private void applyChanges() {
        boolean changedFieldSet = false; // Watch if we need to rebuild entry editors
        boolean anythingChanged = false; // Watch if we should mark as there is data changed

        // First remove the mappings for fields that have been deleted.
        // If these were re-added, they will be added below, so it doesn't
        // cause any harm to remove them here.
        for (String fieldName : removedFields) {
            metaData.clearContentSelectors(fieldName);
            changedFieldSet = true;
            anythingChanged = true;
        }

        // Cycle through all fields that we have created listmodels for:
        for (Map.Entry<String, DefaultListModel<String>> entry : wordListModels.entrySet()) {
            // For each field name, store the values:
            if ((entry.getKey() == null) || FIELD_FIRST_LINE.equals(entry.getKey())) {
                continue;
            }
            DefaultListModel<String> lm = entry.getValue();
            int start = 0;
            // Avoid storing the <new word> marker if it is there:
            if (!lm.isEmpty()) {
                while ((start < lm.size()) && lm.get(start).equals(WORD_FIRSTLINE_TEXT)) {
                    start++;
                }
            }

            Set<String> data = new HashSet<>();
            for (int wrd = start; wrd < lm.size(); wrd++) {
                String word = lm.get(wrd);
                data.add(word);
            }

            // Check if any words have been added
            if (!data.equals(new HashSet<>(metaData.getContentSelectorValuesForField(entry.getKey())))) {
                anythingChanged = true;
            }

            // Check if there are words to be added and previously there were no content selector for the field
            if (!data.isEmpty() && metaData.getContentSelectorValuesForField(entry.getKey()).isEmpty()) {
                changedFieldSet = true;
            }

            metaData.addContentSelector(new ContentSelector(entry.getKey(), new ArrayList<>(data)));
        }

        // Update all selectors in the current BasePanel.
        if (changedFieldSet) {
            // We have added or removed content selectors, update the entry editor
            panel.rebuildAllEntryEditors();
        } else if (anythingChanged) {
            // Enough to update the content selectors, if anything changed
            panel.updateAllContentSelectors();
        }

        if (anythingChanged) {
            // Mark the database updated so changes are not lost
            panel.markNonUndoableBaseChanged();
        }

        panel.getAutoCompleters().addContentSelectorValuesToAutoCompleters(panel.getBibDatabaseContext().getMetaData());

    }

    /**
     * Set the contents of the field selector list.
     *
     */
    private void setupFieldSelector() {
        fieldListModel.clear();
        SortedSet<String> contents = new TreeSet<>();
        ContentSelectors selectors = metaData.getContentSelectors();
        for (String s : selectors.getFieldNamesWithSelectors()) {
            contents.add(s);
        }
        if (contents.isEmpty()) {
            // if nothing was added, put the default fields (as described in the help)
            fieldListModel.addElement(FieldName.AUTHOR);
            fieldListModel.addElement(FieldName.JOURNAL);
            fieldListModel.addElement(FieldName.KEYWORDS);
            fieldListModel.addElement(FieldName.PUBLISHER);
        } else {
            for (String s : contents) {
                fieldListModel.addElement(s);
            }
        }

        if (currentField == null) {
            // if dialog is created for the whole database,
            // select the first field to avoid confusions in GUI usage
            fieldList.setSelectedIndex(0);
        } else {
            // a specific field has been chosen at the constructor
            // select this field
            int i = fieldListModel.indexOf(currentField);
            if (i != -1) {
                // field has been found in list, select it
                fieldList.setSelectedIndex(i);
            }
        }
    }

    private void setupWordSelector() {

        // Have we already created a listmodel for this field?
        wordListModel = wordListModels.get(currentField);
        if (wordListModel == null) {
            wordListModel = new DefaultListModel<>();
            wordList.setModel(wordListModel);
            wordListModels.put(currentField, wordListModel);

            int index = 0;
            for (String s : metaData.getContentSelectorValuesForField(currentField)) {
                wordListModel.add(index, s);
                index++;
            }
        } else {
            wordList.setModel(wordListModel);
        }
    }

    private static int findPos(DefaultListModel<String> lm, String item) {
        for (int i = 0; i < lm.size(); i++) {
            String s = lm.get(i);
            if (item.compareToIgnoreCase(s) < 0) { // item precedes s
                return i;
            }
        }
        return lm.size();
    }

    private void initLayout() {
        fieldNameField.setEnabled(false);
        fieldList.setVisibleRowCount(4);
        wordList.setVisibleRowCount(10);
        final String VAL = "Uren luren himmelturen, ja Besseggen.";
        fieldList.setPrototypeCellValue(VAL);
        wordList.setPrototypeCellValue(VAL);

        fieldPan.setBorder(
                BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(), Localization.lang("Field name")));
        wordPan.setBorder(
                BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(), Localization.lang("Keyword")));
        fieldPan.setLayout(gbl);
        wordPan.setLayout(gbl);
        con.insets = new Insets(2, 2, 2, 2);
        con.fill = GridBagConstraints.BOTH;
        con.gridwidth = 2;
        con.weightx = 1;
        con.weighty = 1;
        con.gridx = 0;
        con.gridy = 0;
        gbl.setConstraints(fPane, con);
        fieldPan.add(fPane);
        gbl.setConstraints(wPane, con);
        wordPan.add(wPane);
        con.gridwidth = 1;
        con.gridx = 2;
        //con.weightx = 0.7;
        con.gridheight = 2;
        gbl.setConstraints(fieldNamePan, con);
        fieldPan.add(fieldNamePan);
        gbl.setConstraints(wordEditPan, con);
        wordPan.add(wordEditPan);
        con.gridx = 0;
        con.gridy = 1;
        con.weightx = 0;
        con.weighty = 0;
        con.gridwidth = 1;
        con.gridheight = 1;
        con.fill = GridBagConstraints.NONE;
        con.anchor = GridBagConstraints.WEST;
        gbl.setConstraints(newField, con);
        fieldPan.add(newField);
        gbl.setConstraints(newWord, con);
        wordPan.add(newWord);
        con.gridx = 1;
        //con.anchor = GridBagConstraints.EAST;
        gbl.setConstraints(removeField, con);
        fieldPan.add(removeField);
        gbl.setConstraints(removeWord, con);
        wordPan.add(removeWord);
        con.anchor = GridBagConstraints.WEST;
        con.gridx = 0;
        con.gridy = 0;
        gbl.setConstraints(fieldNameField, con);
        fieldNamePan.add(fieldNameField);
        gbl.setConstraints(wordEditField, con);
        wordEditPan.add(wordEditField);

        // Add buttons:
        ButtonBarBuilder bsb = new ButtonBarBuilder(buttonPan);
        bsb.addGlue();
        bsb.addButton(ok);
        bsb.addButton(apply);
        bsb.addButton(cancel);
        bsb.addRelatedGap();
        bsb.addButton(new HelpAction(HelpFile.CONTENT_SELECTOR).getHelpButton());
        bsb.addGlue();

        // Add panels to dialog:
        con.fill = GridBagConstraints.BOTH;
        getContentPane().setLayout(gbl);
        con.weightx = 1;
        con.weighty = 0.5;
        con.gridwidth = 1;
        con.gridheight = 1;
        con.gridx = 0;
        con.gridy = 0;
        gbl.setConstraints(fieldPan, con);
        getContentPane().add(fieldPan);
        con.gridy = 1;
        gbl.setConstraints(wordPan, con);
        getContentPane().add(wordPan);
        con.weighty = 0;
        con.gridy = 2;
        con.insets = new Insets(12, 2, 2, 2);
        gbl.setConstraints(buttonPan, con);
        getContentPane().add(buttonPan);

    }


    private class FieldNameFocusAdapter extends FocusAdapter {

        /**
         * Adds the text value to the list
         */
        @Override
        public void focusLost(FocusEvent e) {
            String s = fieldNameField.getText();
            fieldNameField.setText("");
            fieldNameField.setEnabled(false);
            if (!FIELD_FIRST_LINE.equals(s) && !"".equals(s)) {
                // user has typed something

                // remove "<first name>" from list
                fieldListModel.remove(0);

                int pos;
                if (fieldListModel.contains(s)) {
                    // field already exists, scroll to that field (below)
                    pos = fieldListModel.indexOf(s);
                } else {
                    // Add new field.
                    pos = findPos(fieldListModel, s);
                    fieldListModel.add(Math.max(0, pos), s);
                }
                fieldList.setSelectedIndex(pos);
                fieldList.ensureIndexIsVisible(pos);
                currentField = s;
                setupWordSelector();
                newWordAction();
            }
        }

    }

}
