/*
 * File:     SyncManager.java
 * Project:  MPI Linguistic Application
 * Date:     03 April 2006
 *
 * Copyright (C) 2001-2006  Max Planck Institute for Psycholinguistics
 *
 * 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 2 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, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package mpi.eudico.client.annotator;

import mpi.eudico.client.annotator.player.*;

import mpi.eudico.client.annotator.util.*;

import mpi.eudico.client.util.TimeFormatter;

import java.awt.Component;
import java.awt.Container;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.GridLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;

import java.util.*;

import javax.swing.ButtonGroup;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.border.TitledBorder;


/**
 * A class for the synchronization of media players, i.e. setting relative or absolute
 * offsets for each media player.
 *
 */
public class SyncManager implements ActionListener, ItemListener {
    private ViewerManager2 viewerManager;
    private ElanLayoutManager layoutManager;
    private ElanMediaPlayer playerInFocus;
    private Hashtable nameForPlayer;
    private Hashtable playerForName;
    private Hashtable labelForPlayer;
    private Hashtable playerButtons;
    private ButtonGroup buttonGroup;
    private JRadioButton connectedButton;
    private JRadioButton absOffsetRB;
    private JRadioButton relOffsetRB;
    private JRadioButton allPlayersRB;
    private JPanel contentPanel;
    private JPanel playerPanel;
    private JPanel offsetPanel;
    private ButtonGroup playerGroup;
    private JButton applyButton;
    private boolean connected;
    private long minOffset;
    private boolean relative;

    /**
     * Creates a new SyncManager instance
     *
     * @param viewerManager the viewer manager
     * @param layoutManager the layout manager
     */
    public SyncManager(ViewerManager2 viewerManager,
        ElanLayoutManager layoutManager) {
        this.viewerManager = viewerManager;
        this.layoutManager = layoutManager;

        nameForPlayer = new Hashtable();
        playerForName = new Hashtable();
        labelForPlayer = new Hashtable();
        playerButtons = new Hashtable();

        buttonGroup = new ButtonGroup();
        contentPanel = new JPanel(new GridLayout(1, 2));
        playerPanel = new JPanel(new GridLayout(0, 1));
        offsetPanel = new JPanel(new GridBagLayout());

        GridBagConstraints gbc = new GridBagConstraints();
        gbc.gridx = 0;
        gbc.gridy = 0;
        gbc.anchor = GridBagConstraints.NORTHWEST;

        absOffsetRB = new JRadioButton();
        absOffsetRB.setFont(Constants.SMALLFONT);
        absOffsetRB.setSelected(true);
        absOffsetRB.addItemListener(this);
        buttonGroup.add(absOffsetRB);
        offsetPanel.add(absOffsetRB, gbc);
        connectedButton = absOffsetRB;

        gbc.gridy = 1;

        relOffsetRB = new JRadioButton();
        relOffsetRB.setFont(Constants.SMALLFONT);
        relOffsetRB.setSelected(false);
        relOffsetRB.addItemListener(this);
        buttonGroup.add(relOffsetRB);
        offsetPanel.add(relOffsetRB, gbc);
        applyButton = new JButton();
        applyButton.setFont(Constants.SMALLFONT);
        applyButton.addActionListener(this);

        gbc.gridy = 2;
        offsetPanel.add(applyButton, gbc);
        connected = true;

        playerGroup = new ButtonGroup();
        allPlayersRB = new JRadioButton();
        allPlayersRB.setFont(Constants.SMALLFONT);
        allPlayersRB.setSelected(true);
        allPlayersRB.addItemListener(this);
        playerGroup.add(allPlayersRB);
        playerPanel.add(allPlayersRB);
        contentPanel.add(offsetPanel);
        contentPanel.add(playerPanel);
        updateLocale();
    }

    /**
     * Adds a media player to the layout.
     *
     * @param player the media player
     */
    public void add(ElanMediaPlayer player) {
        int size = 1;

        while (playerForName.containsKey(String.valueOf(size))) {
            size++;
        }

        String name = String.valueOf(size);
        nameForPlayer.put(player, name);
        playerForName.put(name, player);
        labelForPlayer.put(player, createLabelPanel(player));

        JRadioButton button = new JRadioButton(ElanLocale.getString(
                    "SyncMode.Label.Player") + " " + name);
        button.setFont(Constants.SMALLFONT);
        button.addItemListener(this);
        playerButtons.put(player, button);
        playerGroup.add(button);
        playerPanel.add(button);
    }

    /**
     * Removes a media player.
     *
     * @param player media player
     */
    public void remove(ElanMediaPlayer player) {
        if (nameForPlayer.containsKey(player)) {
            playerForName.remove(nameForPlayer.get(player));
            nameForPlayer.remove(player);
            labelForPlayer.remove(player);
            ((JRadioButton) playerButtons.get(player)).removeItemListener(this);
            buttonGroup.remove((JRadioButton) playerButtons.get(player));
            playerPanel.remove((JRadioButton) playerButtons.get(player));
            playerButtons.remove(player);
        }
    }

    /**
     * Returns the player selection panel.
     *
     * @return the panel
     */
    public JPanel getPlayerSelectionPanel() {
        return contentPanel;

        //return playerPanel;
    }

    /**
     * Updates the localized Label for the player.
     *
     * @param player the player
     *
     * @return a localized JLabel
     */
    public JPanel getPlayerLabel(ElanMediaPlayer player) {
        if (!nameForPlayer.containsKey(player)) {
            return null;
        }

        JPanel panel = (JPanel) labelForPlayer.get(player);

        if (panel == null) {
            panel = createLabelPanel(player);
            labelForPlayer.put(player, panel);

            return panel;
        }

        Container playPanel = (Container) panel.getComponent(0);
        Component nameComp = playPanel.getComponent(0);

        if (nameComp instanceof JLabel) {
            JLabel nameLabel = (JLabel) nameComp;

            // the name (number) could have been changed
            String name = ElanLocale.getString("SyncMode.Label.Player") + " " +
                (String) nameForPlayer.get(player) + " " +
                ElanLocale.getString("SyncMode.Label.Offset") + ": ";
            nameLabel.setText(name);
        }

        GridBagConstraints gbc = new GridBagConstraints();
        gbc.gridx = 0;
        gbc.gridx = 1;
        gbc.anchor = GridBagConstraints.SOUTHWEST;
        gbc.insets = new Insets(1, 1, 1, 1);

        if (playPanel.getComponentCount() > 1) {
            playPanel.remove(1);
        }

        // the current master gets the master time label that is alive
        // the others get a static label that shows their offset
        if (player == playerInFocus) {
            playPanel.add(viewerManager.getTimePanel(), gbc);
        } else {
            JLabel offsetLabel = new JLabel();

            offsetLabel.setText(TimeFormatter.toString(player.getOffset()));
            playPanel.add(offsetLabel, gbc);
        }

        // update the combo box
        if (player instanceof MultiSourcePlayer) {
            JComboBox box = null;
            JLabel fileLabel = null;

            Component[] comps = panel.getComponents();

            for (int i = 0; i < comps.length; i++) {
                if (comps[i] instanceof JComboBox) {
                    box = (JComboBox) comps[i];
                }

                if (comps[i] instanceof JLabel) {
                    fileLabel = (JLabel) comps[i];
                }
            }

            String[] sources = ((MultiSourcePlayer) player).getDescriptorStrings();

            if (box != null) {
sourceloop: 
                for (int i = 0; i < sources.length; i++) {
                    for (int j = 0; j < box.getItemCount(); j++) {
                        if (box.getItemAt(j).equals(sources[i])) {
                            continue sourceloop;
                        }
                    }

                    box.addItem(sources[i]);
                }

boxloop: 
                for (int j = 0; j < box.getItemCount(); j++) {
                    for (int i = 0; i < sources.length; i++) {
                        if (box.getItemAt(j).equals(sources[i])) {
                            continue boxloop;
                        }
                    }

                    box.removeItemAt(j);
                }

                if (fileLabel != null) {
                    fileLabel.setText((String) box.getSelectedItem());
                }
            }
        }

        return panel;
    }

    /**
     * Creates the panel with name and time labels etc.
     * @param player the player
     * @return the label panel
     */
    private JPanel createLabelPanel(ElanMediaPlayer player) {
        JPanel panel = new JPanel(new GridBagLayout());

        if (!nameForPlayer.containsKey(player)) {
            return panel;
        }

        JLabel nameLabel = new JLabel();
        JPanel playPanel = new JPanel(new GridBagLayout());
        String name = ElanLocale.getString("SyncMode.Label.Player") + " " +
            (String) nameForPlayer.get(player) + " " +
            ElanLocale.getString("SyncMode.Label.Offset") + ": ";

        //nameLabel.setFont(Constants.SMALLFONT);
        //nameLabel.setText(nameForPlayer.get(player) + " offset: ");
        nameLabel.setText(name);

        GridBagConstraints gbc = new GridBagConstraints();
        gbc.gridx = 0;
        gbc.gridy = 0;
        gbc.anchor = GridBagConstraints.SOUTHEAST;
        gbc.insets = new Insets(1, 1, 1, 1);
        playPanel.add(nameLabel, gbc);

        gbc.gridx = 1;
        gbc.anchor = GridBagConstraints.SOUTHWEST;

        // the current master gets the master time label that is alive
        // the others get a static label that shows their offset
        if (player == playerInFocus) {
            playPanel.add(viewerManager.getTimePanel(), gbc);
        } else {
            JLabel offsetLabel = new JLabel();

            offsetLabel.setText(TimeFormatter.toString(player.getOffset()));
            playPanel.add(offsetLabel, gbc);
        }

        gbc.gridx = 0;
        gbc.gridy = 0;
        gbc.anchor = GridBagConstraints.CENTER;
        panel.add(playPanel, gbc);

        gbc.gridx = 0;
        gbc.gridy = 1;
        gbc.anchor = GridBagConstraints.CENTER;

        JLabel fileLabel = new JLabel();
        fileLabel.setFont(Constants.SMALLFONT);

        if (player.getMediaDescriptor() != null) {
            fileLabel.setText(FileUtility.fileNameFromPath(
                    player.getMediaDescriptor().mediaURL));
        } else {
            fileLabel.setText("-");
        }

        panel.add(fileLabel, gbc);

        gbc.gridx = 0;
        gbc.gridy = 2;
        gbc.anchor = GridBagConstraints.CENTER;

        if (player instanceof MultiSourcePlayer) {
            JComboBox box = new JComboBox();
            String[] sources = ((MultiSourcePlayer) player).getDescriptorStrings();

            for (int i = 0; i < sources.length; i++) {
                box.addItem(sources[i]);

                if (i == 0) {
                    fileLabel.setText(sources[i]);
                }
            }

            box.addItemListener(this);
            panel.add(box, gbc);
        }

        return panel;
    }

    /**
     * Returns whether or not the media players currently are connected
     * to the controlles.
     *
     * @return the connected state
     */
    public boolean connectedPlayers() {
        return connected;
    }

    /**
     * Update the offset for the current player in focus
     */
    private void setOffset() {
        if (playerInFocus != null) {
            viewerManager.setOffset(playerInFocus, playerInFocus.getMediaTime());
        }
    }

    /**
     * only keep the relative offsets
     *
     */
    private void setRelativeOffsets() {
        //   	if (relative) {
        //   		return;
        //   	}
        // find smallest offset
        minOffset = Long.MAX_VALUE;

        Enumeration en = playerForName.elements();

        while (en.hasMoreElements()) {
            ElanMediaPlayer player = (ElanMediaPlayer) en.nextElement();

            if (player.getOffset() < minOffset) {
                minOffset = player.getOffset();
            }
        }

        // correct all offsets
        en = playerForName.elements();

        while (en.hasMoreElements()) {
            ElanMediaPlayer player = (ElanMediaPlayer) en.nextElement();
            long offset = player.getOffset();
            viewerManager.setOffset(player, offset - minOffset);

            //    		player.setOffset(offset - minOffset);
        }

        // keep the image as it is
        viewerManager.getMasterMediaPlayer().setMediaTime(minOffset);

        relative = true;
    }

    /**
     * Use absolute offsets
     *
     */
    private void setAbsoluteOffsets() {
        //	   	if (!relative) {
        //	   		return;
        //	   	}
        // correct all offsets
        Enumeration en = playerForName.elements();

        while (en.hasMoreElements()) {
            ElanMediaPlayer player = (ElanMediaPlayer) en.nextElement();
            long offset = player.getOffset();

            // why should the minOffset be added here?
            viewerManager.setOffset(player, offset /*+ minOffset*/);

            //	   		player.setOffset(offset + minOffset);
        }

        // keep the image as it is
        viewerManager.getMasterMediaPlayer().setMediaTime(0);

        relative = false;
    }

    /**
     * Connects all players.
     */
    public void reconnect() {
        setOffset();
        playerInFocus = null;

        if (layoutManager.getMasterMediaPlayer() != null) {
            viewerManager.setMasterMediaPlayer(layoutManager.getMasterMediaPlayer());
            viewerManager.enableDisabledMediaPlayers();
        }

        connected = true;
        allPlayersRB.removeItemListener(this);
        allPlayersRB.setSelected(true);
        allPlayersRB.addItemListener(this);
    }

    /**
     * Sets the player that has the focus, i.e. the player of which the offset
     * can be manipulated with the player control buttons.
     * @param elanPlayer the focussed player
     */
    private void setFocussedPlayer(ElanMediaPlayer elanPlayer) {
        if (elanPlayer == null) {
            return;
        }

        setOffset();
        playerInFocus = elanPlayer;
        viewerManager.setMasterMediaPlayer(playerInFocus);
        viewerManager.disableSlaveMediaPlayers();

        long offset = playerInFocus.getOffset();
        playerInFocus.setOffset(0);
        playerInFocus.setMediaTime(offset);
        connected = false;
        layoutManager.doLayout();
    }

    /**
     * Sets the source for a MultiSourcePlayer, the offset for that player
     * and ui elements of the player labelpanel.
     *
     * @param sourceComponent the source selection ComboBox connected to the player
     */
    private void updateMultiPlayer(JComboBox sourceComponent) {
        Iterator playIt = labelForPlayer.keySet().iterator();
        Object playerObj = null;
        JPanel panel = null;

labelloop: 
        while (playIt.hasNext()) {
            playerObj = playIt.next();
            panel = (JPanel) labelForPlayer.get(playerObj);

            Component[] comps = panel.getComponents();

            for (int i = 0; i < comps.length; i++) {
                if (comps[i] == sourceComponent) {
                    break labelloop;
                }
            }

            panel = null;
            playerObj = null;
        }

        if ((panel != null) && playerObj instanceof MultiSourcePlayer) {
            MultiSourcePlayer player = (MultiSourcePlayer) playerObj;
            String source = (String) sourceComponent.getSelectedItem();

            if (source != null) {
                // this sets the offset of the player
                player.setCurrentSource(source);
                layoutManager.doLayout();
            }
        }
    }

    /**
     * Handles the action events
     *
     * @param event the action event
     */
    public void actionPerformed(ActionEvent event) {
        if (event.getSource() == applyButton) {
            // this will cause an item event on the "all players radio button" 
            allPlayersRB.setSelected(true);
        }
    }

    /**
     * Handling of changes in radio buttons selection states.
     *
     * @param e the item selection event
     */
    public void itemStateChanged(ItemEvent e) {
        // ignore deselect events
        if (e.getStateChange() == ItemEvent.SELECTED) {
            if (e.getSource() instanceof JComboBox) {
                updateMultiPlayer((JComboBox) e.getSource());
            } else if (e.getSource() == absOffsetRB) {
                relative = false;

                if (!allPlayersRB.isSelected()) {
                    allPlayersRB.setSelected(true);
                } else {
                    setAbsoluteOffsets();
                }
            } else if (e.getSource() == relOffsetRB) {
                relative = true;

                if (!allPlayersRB.isSelected()) {
                    allPlayersRB.setEnabled(true);
                } else {
                    setRelativeOffsets();
                }
            } else if (e.getSource() == allPlayersRB) {
                reconnect();

                if (relative) {
                    setRelativeOffsets();
                } else {
                    setAbsoluteOffsets();
                }

                layoutManager.doLayout();
            } else {
                // it is a player's radio button
                if (playerButtons.containsValue(e.getSource())) {
                    Enumeration en = playerButtons.keys();
                    Object player;

                    while (en.hasMoreElements()) {
                        player = en.nextElement();

                        if (playerButtons.get(player) == e.getSource()) {
                            setFocussedPlayer((ElanMediaPlayer) player);

                            break;
                        }
                    }
                }
            }
        }
    }

    /**
     * Update the labels, border titles and button texts.
     * Called by the ElanLayoutManager.
     */
    public void updateLocale() {
        offsetPanel.setBorder(new TitledBorder(ElanLocale.getString(
                    "SyncMode.Label.Offset")));
        absOffsetRB.setText(ElanLocale.getString("SyncMode.Label.Absolute"));
        relOffsetRB.setText(ElanLocale.getString("SyncMode.Label.Relative"));
        applyButton.setText(ElanLocale.getString("SyncMode.Button.Apply"));
        playerPanel.setBorder(new TitledBorder(ElanLocale.getString(
                    "SyncMode.Label.Player")));
        allPlayersRB.setText(ElanLocale.getString("SyncMode.Label.AllPlayers"));

        //update player radiobuttons and player labels
        Enumeration en = nameForPlayer.keys();
        Object player;
        JRadioButton b;
        JPanel p;
        JLabel l;

        while (en.hasMoreElements()) {
            player = en.nextElement();
            b = (JRadioButton) playerButtons.get(player);
            b.setText(ElanLocale.getString("SyncMode.Label.Player") + " " +
                nameForPlayer.get(player));
            p = (JPanel) labelForPlayer.get(player);

            try {
                l = (JLabel) p.getComponent(0);

                String name = ElanLocale.getString("SyncMode.Label.Player") +
                    " " + (String) nameForPlayer.get(player) + " " +
                    ElanLocale.getString("SyncMode.Label.Offset") + ": ";
                l.setText(name);
            } catch (Exception e) {
                // just catch any exception, NullPointer, ArrayIndexOutOfBounds or
                // ClassCast
            }
        }
    }
}
