/* Copyright (c) 2005 Stanford University and Christopher Bruns
 * 
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including 
 * without limitation the rights to use, copy, modify, merge, publish, 
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject
 * to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included 
 * in all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

/*
 * Created on May 15, 2006
 * Original author: Christopher Bruns
 */
package org.simtk.isimsu;

import java.util.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import java.text.DecimalFormat;

public class IonSelectionDialog 
extends JDialog 
implements ActionListener, SaltSelectionObservable
{
    protected String programName = "SimTK ISIM"; // TODO get from ISIMWrapper

    protected JButton addIonButton = new JButton("Add New Ion");
    protected JButton finishedButton = new JButton("Finish");
    protected JButton cancelButton = new JButton("Cancel");
    protected JButton helpButton = new JButton("Help ?");
    protected Collection<IonSelectionPanel> ionPanelList = new LinkedHashSet<IonSelectionPanel>();
    protected JPanel ionsPanel = new JPanel();
    protected SaltRangeSet ionConditionSet;
    protected JLabel netChargeLabel = new JLabel();
    protected SaltSelectionObservableClass saltSelectionObservable
        = new SaltSelectionObservableClass();
    // protected Set<IonSpecies> currentlyDisplayedIons = new HashSet<IonSpecies>();

    // Create indexes to speed up consideration of what ions are possible
    protected Map<IonSpecies, Set<SaltConditionRange> >
        ionRangeMap = new HashMap<IonSpecies, Set<SaltConditionRange> >();
    protected Map<SaltConditionRange, Set<IonSpecies> >
        rangeIonMap = new HashMap<SaltConditionRange, Set<IonSpecies> >();
    
    IonSelectionDialog(Frame frame, SaltRangeSet ionSet) {
        super(frame);
        ionConditionSet = ionSet;

        setTitle("Choose Ions (" + programName + ")");

        setLocationRelativeTo(frame);
        
        // Insert a JPanel so I can use setBorder()
        Container rootPane = getContentPane();
        JPanel rootJPanel = new JPanel();
        rootJPanel.setLayout(new BoxLayout(rootJPanel, BoxLayout.Y_AXIS));
        rootJPanel.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
        rootPane.add(rootJPanel, BorderLayout.CENTER);

        // Place the buttons at the bottom of the dialog
        JPanel buttonPanel = new JPanel();
        buttonPanel.setLayout(new BoxLayout(buttonPanel, BoxLayout.X_AXIS));
        buttonPanel.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
        buttonPanel.add(Box.createHorizontalGlue()); // right justify
        // buttonPanel.add(backButton);
        buttonPanel.add(addIonButton);
        buttonPanel.add(Box.createRigidArea(new Dimension(10, 0)));
        buttonPanel.add(finishedButton);
        buttonPanel.add(Box.createRigidArea(new Dimension(10, 0)));
        buttonPanel.add(cancelButton);
        buttonPanel.add(Box.createRigidArea(new Dimension(10, 0)));
        buttonPanel.add(helpButton);
        
        cancelButton.setToolTipText("Close this window without saving these ions.");
        helpButton.setToolTipText("View helpful information.");
        
        // backButton.addActionListener(this);
        addIonButton.addActionListener(this);
        finishedButton.addActionListener(this);
        cancelButton.addActionListener(this);
        helpButton.addActionListener(this);

        ionsPanel.setLayout(new BoxLayout(ionsPanel, BoxLayout.Y_AXIS));
        rootJPanel.add(ionsPanel);

        // Information between the ion panels and the buttons
        JPanel infoPanel = new JPanel();
        infoPanel.setLayout(new BoxLayout(infoPanel, BoxLayout.X_AXIS));
        infoPanel.add(Box.createHorizontalStrut(10));
        infoPanel.add(netChargeLabel);
        netChargeLabel.setToolTipText("The net charge must be zero (0.0) for a consistent set of ions"); 
        infoPanel.add(Box.createHorizontalGlue());
        rootJPanel.add(infoPanel);
        
        rootJPanel.add(buttonPanel);
        
        Set<IonSpecies> availableIons = availableIons();
        if (availableIons.isEmpty()) {
            addIonButton.setEnabled(false);
        }
        else {
            addIonSelectionPanel(
                    new IonSelectionPanel(
                            this, 
                            firstAvailableIon(),
                            ionConditionSet));
        }
        
        updateIonChoices();
        updateConcentrationRanges(getAvailableRanges());
        
        pack();
    }

    public double netCharge() {
        double charge = 0.0;
        for (IonSelectionPanel panel : ionPanelList) {
            if (panel == null) continue;
            charge += panel.netCharge();
        }
        return charge;
    }
    
    public void updateButtons() {

        // Determine if more ions are possible for current set
        // then set addIonButton appropriately
        if (availableIons().isEmpty()) {
            addIonButton.setEnabled(false);
            addIonButton.setToolTipText("No more ions are possible in this condition.  Try changing ion types and/or concentrations.");
        }
        else {
            addIonButton.setEnabled(true);
            addIonButton.setToolTipText("Display one more ion type.");
        }

        // Determine if current ion set is consistent with an ion condition
        if (! (netCharge() == 0.0)) {
            finishedButton.setEnabled(false);
            finishedButton.setToolTipText("The net charge must be zero (0.0).  Change ions and/or concentrations.");
        }
        else if (completeableRanges().isEmpty()) {
            finishedButton.setEnabled(false);
            finishedButton.setToolTipText("There are no precomputed parameters for this condition.  Change the ions and/or concentrations.");
        }
        else {
            finishedButton.setEnabled(true);
            finishedButton.setToolTipText("Use this set of ions for the simulation.");
        }

        cancelButton.setEnabled(true);

        if (finishedButton.isEnabled())
            getRootPane().setDefaultButton(finishedButton);
        else if (addIonButton.isEnabled())
            getRootPane().setDefaultButton(addIonButton);
   }
    
    public void actionPerformed(ActionEvent event) {
        if      (event.getSource() == addIonButton)            
            addIonButtonPressed();
        else if (event.getSource() == finishedButton)   
            finishedButtonPressed();
        else if (event.getSource() == cancelButton)     
            cancelButtonPressed();
        else if (event.getSource() == helpButton)       
            helpButtonPressed();
        
        else if (event.getSource() instanceof JComboBox) {
            // Ion type changed
            updateNetCharge();
        }
    }
    
    protected void updateNetCharge() {
        double charge = netCharge();
        
        if (charge == 0.0) netChargeLabel.setForeground(Color.BLACK); // Nice color
        else if (charge > 0.0) netChargeLabel.setForeground(Color.RED); // Warning color
        else netChargeLabel.setForeground(Color.MAGENTA);
        
        // Positive values should have a plus sign
        String plusSign = "+";
        if (charge <= 0.0) plusSign = "";
        
        String valueJudgement = " (just right)";
        if (charge < 0) {
            valueJudgement = " (too low)";
        }
        if (charge > 0) valueJudgement = " (too high)";

        if ( (charge != 0) && (ionPanelList.size() == 1) ) {
            valueJudgement = valueJudgement + " [click 'Add New Ion']";
            getRootPane().setDefaultButton(addIonButton);
        }
        
        netChargeLabel.setText(
                "Net charge = "+
                plusSign+
                new DisplayableIonConcentration(charge)+
                valueJudgement);
        updateButtons();
    }
    
    public void updateConcentrationRanges(Set<SaltConditionRange> ranges) {

        // Hash panels by ion
        Map<IonSpecies, IonSelectionPanel> ionPanels = new HashMap<IonSpecies, IonSelectionPanel>();
        // Hash extreme concentrations by ion
        Map<IonSpecies, Double> ionMaxConcentrations = new HashMap<IonSpecies, Double>();
        Map<IonSpecies, Double> ionMinConcentrations = new HashMap<IonSpecies, Double>();
        for (IonSelectionPanel panel : ionPanelList) {
            IonSpecies ion = panel.displayedIon();

            ionPanels.put(ion, panel);
            
            // Set concentration extremes to universally overwritable values
            ionMaxConcentrations.put(ion, 0.0);
            ionMinConcentrations.put(ion, Double.MAX_VALUE);
        }
        
        Map<IonSpecies, Set<Double> > ionConcentrations = new HashMap<IonSpecies, Set<Double> >();
        
        for (SaltConditionRange range : ranges) {
            for (SaltCondition condition : range) {
                ION_CONCENTRATION: 
                for (IonConcentration ionConcentration : condition) {
                    IonSpecies ion = ionConcentration.getIonSpecies();

                    if (! (ionPanels.containsKey(ion))) continue ION_CONCENTRATION;
                    
                    double concentration = ionConcentration.getConcentration();

                    if (! (ionConcentrations.containsKey(ion)) )
                        ionConcentrations.put(ion, new TreeSet<Double>() );
                    ionConcentrations.get(ion).add(concentration);
                    
                    if (concentration > ionMaxConcentrations.get(ion))
                        ionMaxConcentrations.put(ion, concentration);
                    if (concentration < ionMinConcentrations.get(ion))
                        ionMinConcentrations.put(ion, concentration);
                }
            }
        }
        
        for (IonSpecies ion : ionPanels.keySet()) {
            IonSelectionPanel panel = ionPanels.get(ion);
            
            double min = ionMinConcentrations.get(ion);
            double max = ionMaxConcentrations.get(ion);
            panel.setConcentrationRange(min, max);
            double c = panel.concentration();
            if (c > max) panel.setConcentration(max);            
            if (c < min) panel.setConcentration(min);
            
            panel.setConcentrationValues(ionConcentrations.get(ion));
        }
    }
    
    // SaltSelectionObservable interface -- delegate to saltSelectionObservable
    public void addSaltSelectionListener(SaltSelectionListener listener) {
        saltSelectionObservable.addSaltSelectionListener(listener);
    }
    public void removeSaltSelectionListener(SaltSelectionListener listener) {
        saltSelectionObservable.removeSaltSelectionListener(listener);        
    }
    public void selectSalt(
            SaltSelectionObservable source, 
            SaltCondition salt) {
        saltSelectionObservable.selectSalt(source, salt);              
    }

    /**
     * Ensure that each ion panel has an appropriate choice of
     * other ions to change to.
     *
     */
    protected void updateIonChoices() {
        
        // What ions could replace the one shown in each panel?
        for (IonSelectionPanel panel : ionPanelList) {

            IonSpecies displayedIon = panel.displayedIon();

            // Compute list of ions on panels other than this one
            Set<IonSpecies> otherDisplayedIons = new HashSet<IonSpecies>();
            for (IonSelectionPanel otherPanel : ionPanelList) {
                if (otherPanel.equals(panel)) continue;
                otherDisplayedIons.add(otherPanel.displayedIon());
            }

            // What ranges are consistent with those other ions?
            Set<IonSpecies> newIonChoices = new HashSet<IonSpecies>();
            for (SaltConditionRange range : getAvailableRanges(otherDisplayedIons)) {
                ION: for (IonSpecies ion : range.getIonSpecies()) {
                    if ( ion.equals(displayedIon) ) continue ION; // already shown
                    if ( otherDisplayedIons.contains(ion) ) continue ION; // already on another panel
                    newIonChoices.add(ion);
                }
            }
            panel.setUnselectedIons(newIonChoices);
        }
        
        if (availableIons().isEmpty()) addIonButton.setEnabled(false);
        else addIonButton.setEnabled(true);
        
        updateButtons();
    }
    
    protected Set<IonSpecies> currentlyDisplayedIons() {
        Set<IonSpecies> ions = new HashSet<IonSpecies>();
        
        for (IonSelectionPanel panel : ionPanelList) {
            ions.add(panel.displayedIon());
        }
        
        return ions;
    }
    
    /**
     * @return the set of ion ranges that are consistent 
     * with the currently displayed ions
     */
    protected Set<SaltConditionRange> getAvailableRanges() {
        return getAvailableRanges(currentlyDisplayedIons());
    }
    
    // Ion condition ranges compatible with a particular set of ions
    protected Set<SaltConditionRange> getAvailableRanges(Set<IonSpecies> ions) {
        Set<SaltConditionRange> availableRanges = new HashSet<SaltConditionRange>();

        RANGE: for (SaltConditionRange range : ionConditionSet) {
            boolean isConsistent = true;
            ION: for (IonSpecies ion : ions) {
                if (! (range.getIonSpecies().contains(ion)) ) {
                    isConsistent = false;
                    continue RANGE;
                }
            }
            if (isConsistent) availableRanges.add(range);
        }
        
        return availableRanges;        
    }
    
    protected Set<SaltConditionRange> completeableRanges() {
        Set<SaltConditionRange> completeableRanges = new HashSet<SaltConditionRange>();

        Set<IonSpecies> displayedIons = currentlyDisplayedIons();
        RANGE: for (SaltConditionRange range : getAvailableRanges()) {
            boolean rangeIsCompleteable = true;
            for (IonSpecies rangeIon : range.getIonSpecies()) {
                if (! (displayedIons.contains(rangeIon))) {
                    rangeIsCompleteable = false;
                    continue RANGE;
                }
            }

            if (rangeIsCompleteable) completeableRanges.add(range);            
        }
        
        return completeableRanges;
    }
    
    /**
     * 
     * @return the set of all ion types that could be added to the current list
     */
    protected Set<IonSpecies> availableIons() {
        Set<IonSpecies> availableIons = new HashSet<IonSpecies>();
        Set<IonSpecies> displayedIons = currentlyDisplayedIons();
        
        // New ions must be in the set of usable salt condition ranges
        for (SaltConditionRange range : getAvailableRanges()) {
            for (IonSpecies ion : range.getIonSpecies()) {
                // New ions must not be currently displayed
                if (! (displayedIons.contains(ion)) ) {
                    availableIons.add(ion);
                }
            }
        }

        return availableIons;
    }
    
    protected void addIonButtonPressed() {
        Set<IonSpecies> availableIons = availableIons();
        if (availableIons.isEmpty()) {
            addIonButton.setEnabled(false);
            return;
        }
        
        addIonSelectionPanel(
                new IonSelectionPanel(
                        this, 
                        firstAvailableIon(),
                        ionConditionSet));
        
    }

    protected void finishedButtonPressed() {
        Set<SaltConditionRange> ranges = completeableRanges();
        if (ranges.isEmpty()) {
            updateButtons();
            return;
        }
        
        SaltConditionRange range = ranges.iterator().next();
        if (range == null) {
            updateButtons();
            return;
        }

        SaltCondition saltCondition = new SaltCondition(range);
        String conditionName = "";
        
        // Populate salt condition, one ion at a time
        for (IonSelectionPanel panel : ionPanelList) {
            IonSpecies ion = panel.displayedIon();
            IonConcentration concentration = new IonConcentration(ion, saltCondition);
            concentration.setConcentration((float)panel.concentration());
         
            String concentrationName = ""+concentration.getConcentration()+ion.getIonId();
            if (conditionName.length() > 0) conditionName = conditionName + " " + concentrationName;
            else conditionName = concentrationName;
            
            saltCondition.add(concentration);
        }

        saltCondition.setName(conditionName);
        
        // Fire salt selection event and close the window
        selectSalt(this, saltCondition);
        setVisible(false);
    }

    protected void cancelButtonPressed() {
        setVisible(false);
    }

    protected void helpButtonPressed() {
        JOptionPane.showMessageDialog(this, 
                
                "Choose ions one at a time.\n"+
                "Both positive ions and negative ions are required to create \n"+
                "a neutral (zero) net charge.\n"+
                "A consistent set of ions must have a neutral net charge.\n"+
                "Ion conditions lacking precomputed parameters may not be used.\n"+
                "The \"Finish\" button will become enabled once a consistent\n"+
                "and precomputed set of ions is chosen.\n"
                ,
                
                "Help: Choose Ions (" + programName + ")",
                JOptionPane.INFORMATION_MESSAGE);
    }
    
    protected void deleteIonSelectionPanel(IonSelectionPanel panel) {
        ionsPanel.remove(panel);
        ionPanelList.remove(panel);
        
        // Don't let user delete the last panel
        if (ionPanelList.size() == 1) {
            IonSelectionPanel lonePanel = (IonSelectionPanel) ionPanelList.iterator().next();
            lonePanel.disableDeletion();
            // lonePanel.deleteButton.setEnabled(false);
        }

        updateIonChoices();
        updateConcentrationRanges(getAvailableRanges());
        
        // Clean up space occupied by panel
        Dimension currentDimension = getSize();
        Dimension preferredDimension = getPreferredSize();
        setSize(new Dimension(currentDimension.width, preferredDimension.height));
    }
    
    protected IonSpecies firstAvailableIon() {
        
        // Get ion with alphabetically first ID
        java.util.List<IonSpecies> sortedIons = new Vector<IonSpecies>();
        for (IonSpecies ion : availableIons())
            sortedIons.add(ion);
        Collections.sort(sortedIons);

        if (sortedIons.size() < 1) return null;
        return sortedIons.iterator().next();
    }
    
    protected void addIonSelectionPanel(IonSelectionPanel panel) {
        ionsPanel.add(panel);
        ionPanelList.add(panel);

        // Permit deletion of ion panels when there are two or more
        if (ionPanelList.size() == 1)
            // panel.deleteButton.setEnabled(false);
            panel.disableDeletion();
        else if (ionPanelList.size() == 2) {
            Iterator ionIterator = ionPanelList.iterator();
            while (ionIterator.hasNext()) {
                IonSelectionPanel p = (IonSelectionPanel) ionIterator.next();
                // p.deleteButton.setEnabled(true);
                p.enableDeletion();
            }
        }
        else 
            // panel.deleteButton.setEnabled(true);
            panel.enableDeletion();
        
        updateIonChoices();
        updateConcentrationRanges(getAvailableRanges());

        // Clean up space occupied by panel
        Dimension currentDimension = getSize();
        Dimension preferredDimension = getPreferredSize();
        setSize(new Dimension(currentDimension.width, preferredDimension.height));

        // Adjust concentration in new panel to minimize net charge
        panel.adjustConcentrationToNeutralizeCharge();
    }
    
}

class DisplayableIonConcentration extends Number {
    protected Double number;
    DisplayableIonConcentration(double d) {
        number = d;
    }
    
    // Number interface
    public double doubleValue(){return number.doubleValue();}
    public float floatValue(){return number.floatValue();}
    public long longValue(){return number.longValue();}
    public int intValue(){return number.intValue();}
    
    @Override
    public String toString() {
        return stringForConcentration(number);
    }

    // Make sure it hashes like a Double
    @Override
    public boolean equals(Object o) {
        return number.equals(o);
    }
    @Override
    public int hashCode() {
        return number.hashCode();
    }
    
    protected String stringForConcentration(double conc) {
        double concentration = Math.abs(conc);
        double sign = (conc < 0) ? -1.0 : 1.0;
        
        // Don't round here
        // double rounded = roundedConcentration(concentration);
        double rounded = concentration;
        
        String units = "mM";
        if (rounded >= 1000) {
            units = "M";
            rounded = rounded / 1000.0;
        }
        else if (rounded == 0.0) {units = "mM";} // Zero can be any units: use default
        else if (rounded < 1.0) {
            units = "\u03BC"+"M"; // micro symbol
            // units = "u"+"M";
            rounded = rounded * 1000.0;
        }
        
        DecimalFormat numberFormat = new DecimalFormat("##0.0####");

        return ""+numberFormat.format(sign * rounded)+" "+units;
    }

}
