package net.sf.jabref.gui;

import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import javax.swing.SwingUtilities;

import net.sf.jabref.Globals;
import net.sf.jabref.gui.maintable.MainTable;
import net.sf.jabref.preferences.JabRefPreferences;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * Manages visibility of SideShowComponents in a given newly constructed
 * sidePane.
 */
public class SidePaneManager {
    private static final Log LOGGER = LogFactory.getLog(SidePaneManager.class);

    private final JabRefFrame frame;

    private final SidePane sidep;

    private final Map<Class<? extends SidePaneComponent>, SidePaneComponent> components = new LinkedHashMap<>();

    private final List<SidePaneComponent> visible = new LinkedList<>();

    public SidePaneManager(JabRefFrame frame) {
        this.frame = frame;
        /*
         * Change by Morten Alver 2005.12.04: By postponing the updating of the
         * side pane components, we get rid of the annoying latency when
         * switching tabs:
         */
        frame.getTabbedPane().addChangeListener(event -> SwingUtilities.invokeLater(
                () -> setActiveBasePanel(SidePaneManager.this.frame.getCurrentBasePanel())));
        sidep = new SidePane();
        sidep.setVisible(false);
    }

    public SidePane getPanel() {
        return sidep;
    }

    public synchronized <T extends SidePaneComponent> boolean hasComponent(Class<T> sidePaneComponent) {
        return components.containsKey(sidePaneComponent);
    }

    public synchronized <T extends SidePaneComponent> boolean isComponentVisible(Class<T> sidePaneComponent) {
        SidePaneComponent component = components.get(sidePaneComponent);
        if (component == null) {
            return false;
        } else {
            return visible.contains(component);
        }
    }

    /**
     * If panel is visible it will be hidden and the other way around
     */
    public synchronized <T extends SidePaneComponent> void toggle(Class<T> sidePaneComponent) {
        if (isComponentVisible(sidePaneComponent)) {
            hide(sidePaneComponent);
        } else {
            show(sidePaneComponent);
        }
    }

    /**
     * If panel is hidden it will be shown and focused
     * If panel is visible but not focused it will be focused
     * If panel is visible and focused it will be hidden
     */
    public synchronized <T extends SidePaneComponent> void toggleThreeWay(Class<T> sidePaneComponent) {
        boolean isPanelFocused = Globals.getFocusListener().getFocused() == components.get(sidePaneComponent);
        if (isComponentVisible(sidePaneComponent) && isPanelFocused) {
            hide(sidePaneComponent);
        } else {
            show(sidePaneComponent);
        }
    }

    public synchronized <T extends SidePaneComponent> void show(Class<T> sidePaneComponent) {
        SidePaneComponent component = components.get(sidePaneComponent);
        if (component == null) {
            LOGGER.warn("Side pane component '" + sidePaneComponent + "' unknown.");
        } else {
            show(component);
        }
    }

    public synchronized <T extends SidePaneComponent> void hide(Class<T> sidePaneComponent) {
        SidePaneComponent component = components.get(sidePaneComponent);
        if (component == null) {
            LOGGER.warn("Side pane component '" + sidePaneComponent + "' unknown.");
        } else {
            hideComponent(component);
            if (frame.getCurrentBasePanel() != null) {
                MainTable mainTable = frame.getCurrentBasePanel().getMainTable();
                mainTable.setSelected(mainTable.getSelectedRow());
                mainTable.requestFocus();
            }
        }
    }

    public synchronized void register(SidePaneComponent comp) {
        components.put(comp.getClass(), comp);
    }

    private synchronized void show(SidePaneComponent component) {
        if (!visible.contains(component)) {
            // Put the new component at the top of the group
            visible.add(0, component);

            // Sort the visible components by their preferred position
            Collections.sort(visible, new PreferredIndexSort());

            updateView();
            component.componentOpening();
        }
        Globals.getFocusListener().setFocused(component);
        component.grabFocus();
    }

    public synchronized <T extends SidePaneComponent> SidePaneComponent getComponent(Class<T> sidePaneComponent) {
        return components.get(sidePaneComponent);
    }

    public synchronized void hideComponent(SidePaneComponent comp) {
        if (visible.contains(comp)) {
            comp.componentClosing();
            visible.remove(comp);
            updateView();
        }
    }

    public synchronized <T extends SidePaneComponent> void hideComponent(Class<T> sidePaneComponent) {
        SidePaneComponent component = components.get(sidePaneComponent);
        if (component == null) {
            return;
        }
        if (visible.contains(component)) {
            component.componentClosing();
            visible.remove(component);
            updateView();
        }
    }

    private static Map<Class<? extends SidePaneComponent>, Integer> getPreferredPositions() {
        Map<Class<? extends SidePaneComponent>, Integer> preferredPositions = new HashMap<>();

        List<String> componentNames = Globals.prefs.getStringList(JabRefPreferences.SIDE_PANE_COMPONENT_NAMES);
        List<String> componentPositions = Globals.prefs
                .getStringList(JabRefPreferences.SIDE_PANE_COMPONENT_PREFERRED_POSITIONS);

        for (int i = 0; i < componentNames.size(); ++i) {
            String componentName = componentNames.get(i);
            try {
                Class<? extends SidePaneComponent> componentClass = (Class<? extends SidePaneComponent>) Class.forName(componentName);
                preferredPositions.put(componentClass, Integer.parseInt(componentPositions.get(i)));
            } catch (ClassNotFoundException e) {
                LOGGER.error("Following side pane could not be found: " + componentName, e);
            } catch (ClassCastException e) {
                LOGGER.error("Following Class is no side pane: '" + componentName, e);
            } catch (NumberFormatException e) {
                LOGGER.info("Invalid number format for side pane component '" + componentName + "'.", e);
            }
        }

        return preferredPositions;
    }

    private void updatePreferredPositions() {
        Map<Class<? extends SidePaneComponent>, Integer> preferredPositions = getPreferredPositions();

        // Update the preferred positions of all visible components
        int index = 0;
        for (SidePaneComponent comp : visible) {
            preferredPositions.put(comp.getClass(), index);
            index++;
        }

        // Split the map into a pair of parallel String lists suitable for storage
        List<String> tmpComponentNames = preferredPositions.keySet().parallelStream()
                .map(Class::getName)
                .collect(Collectors.toList());

        List<String> componentPositions = preferredPositions.values().stream().map(Object::toString)
                .collect(Collectors.toList());

        Globals.prefs.putStringList(JabRefPreferences.SIDE_PANE_COMPONENT_NAMES, tmpComponentNames);
        Globals.prefs.putStringList(JabRefPreferences.SIDE_PANE_COMPONENT_PREFERRED_POSITIONS, componentPositions);
    }


    /**
     * Helper class for sorting visible components based on their preferred position
     */
    private class PreferredIndexSort implements Comparator<SidePaneComponent> {

        private final Map<Class<? extends SidePaneComponent>, Integer> preferredPositions;


        public PreferredIndexSort() {
            preferredPositions = getPreferredPositions();
        }

        @Override
        public int compare(SidePaneComponent comp1, SidePaneComponent comp2) {
            int pos1 = preferredPositions.getOrDefault(comp1.getClass(), 0);
            int pos2 = preferredPositions.getOrDefault(comp2.getClass(), 0);
            return Integer.valueOf(pos1).compareTo(pos2);
        }
    }


    public synchronized void moveUp(SidePaneComponent comp) {
        if (visible.contains(comp)) {
            int currIndex = visible.indexOf(comp);
            if (currIndex > 0) {
                int newIndex = currIndex - 1;
                visible.remove(currIndex);
                visible.add(newIndex, comp);

                updatePreferredPositions();
                updateView();
            }
        }
    }

    public synchronized void moveDown(SidePaneComponent comp) {
        if (visible.contains(comp)) {
            int currIndex = visible.indexOf(comp);
            if (currIndex < (visible.size() - 1)) {
                int newIndex = currIndex + 1;
                visible.remove(currIndex);
                visible.add(newIndex, comp);

                updatePreferredPositions();
                updateView();
            }
        }
    }

    public synchronized <T extends SidePaneComponent> void unregisterComponent(Class<T> sidePaneComponent) {
        components.remove(sidePaneComponent);
    }

    /**
     * Update all side pane components to show information from the given
     * BasePanel.
     *
     * @param panel
     */
    private synchronized void setActiveBasePanel(BasePanel panel) {
        for (SidePaneComponent component : components.values()) {
            component.setActiveBasePanel(panel);
        }
    }

    public synchronized void updateView() {
        sidep.setComponents(visible);
        if (visible.isEmpty()) {
            if (sidep.isVisible()) {
                Globals.prefs.putInt(JabRefPreferences.SIDE_PANE_WIDTH, frame.getSplitPane().getDividerLocation());
            }
            sidep.setVisible(false);
        } else {
            boolean wasVisible = sidep.isVisible();
            sidep.setVisible(true);
            if (!wasVisible) {
                int width = Globals.prefs.getInt(JabRefPreferences.SIDE_PANE_WIDTH);
                if (width > 0) {
                    frame.getSplitPane().setDividerLocation(width);
                } else {
                    frame.getSplitPane().setDividerLocation(getPanel().getPreferredSize().width);
                }
            }
        }
    }
}
