/*
 * IzPack - Copyright 2001-2008 Julien Ponge, All Rights Reserved.
 *
 * http://izpack.org/
 * http://izpack.codehaus.org/
 *
 * Copyright 2007 Vladimir Ralev
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.izforge.izpack.panels;

import com.izforge.izpack.LocaleDatabase;
import com.izforge.izpack.Pack;
import com.izforge.izpack.gui.LabelFactory;
import com.izforge.izpack.installer.*;
import com.izforge.izpack.util.Debug;
import com.izforge.izpack.util.IoHelper;
import com.izforge.izpack.util.VariableSubstitutor;
import com.izforge.izpack.adaptator.IXMLElement;

import javax.swing.*;
import javax.swing.plaf.metal.MetalLookAndFeel;
import javax.swing.tree.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.File;
import java.io.InputStream;
import java.util.*;
import java.util.List;

public class TreePacksPanel extends IzPanel implements PacksPanelInterface
{
    /**
     * Required (serializable)
     */
    private static final long serialVersionUID = 5684716698930628262L;

    // Common used Swing fields
    /**
     * The free space label.
     */
    protected JLabel freeSpaceLabel;

    /**
     * The space label.
     */
    protected JLabel spaceLabel;

    /**
     * The tip label.
     */
    protected JTextArea descriptionArea;

    /**
     * The dependencies label.
     */
    protected JTextArea dependencyArea;

    /**
     * The packs tree.
     */
    protected JTree packsTree;

    /**
     * The packs model.
     */
    protected PacksModel packsModel;

    /**
     * The tablescroll.
     */
    protected JScrollPane tableScroller;

    // Non-GUI fields
    /**
     * Map that connects names with pack objects
     */
    private Map<String, Pack> names;

    /**
     * The bytes of the current pack.
     */
    protected long bytes = 0;

    /**
     * The free bytes of the current selected disk.
     */
    protected long freeBytes = 0;

    /**
     * Are there dependencies in the packs
     */
    protected boolean dependenciesExist = false;

    /**
     * The packs locale database.
     */
    private LocaleDatabase langpack = null;

    /**
     * The name of the XML file that specifies the panel langpack
     */
    private static final String LANG_FILE_NAME = "packsLang.xml";

    private HashMap<String, Pack> idToPack;
    private HashMap<String, ArrayList<String>> treeData;
    private HashMap<Pack, Integer> packToRowNumber;

    private HashMap<String, CheckBoxNode> idToCheckBoxNode = new HashMap<String, CheckBoxNode>();
    //private boolean created = false;   // UNUSED

    private CheckTreeController checkTreeController;

    /**
     * The constructor.
     *
     * @param parent The parent window.
     * @param idata  The installation data.
     */
    public TreePacksPanel(InstallerFrame parent, InstallData idata)
    {
        super(parent, idata);
        // Load langpack.
        try
        {
            this.langpack = parent.langpack;
            InputStream langPackStream = null;
            String webdir = idata.info.getWebDirURL();
            if (webdir != null)
            {
                try
                {
                    java.net.URL url = new java.net.URL(webdir + "/langpacks/" + LANG_FILE_NAME + idata.localeISO3);
                    langPackStream = new WebAccessor(null).openInputStream(url);
                }
                catch (Exception e)
                {
                    // just ignore this. we use the fallback below
                }
            }
            
            if(langPackStream == null)
            {
                langPackStream = ResourceManager.getInstance().getInputStream(LANG_FILE_NAME);
            }

            this.langpack.add(langPackStream);
            langPackStream.close();
        }
        catch (Throwable exception)
        {
            Debug.trace(exception);
        }

        // init the map
        computePacks(idata.availablePacks);

    }

    /**
     * The Implementation of this method should create the layout for the current class.
     */

    protected void createNormalLayout()
    {
        this.removeAll();
        setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
        createLabel("PacksPanel.info", "preferences", null, null);
        add(Box.createRigidArea(new Dimension(0, 3)));
        createLabel("PacksPanel.tip", "tip", null, null);
        add(Box.createRigidArea(new Dimension(0, 5)));
        tableScroller = new JScrollPane();
        packsTree = createPacksTree(300, tableScroller, null, null);
        if (dependenciesExist)
        {
            dependencyArea = createTextArea("PacksPanel.dependencyList", null, null, null);
        }
        descriptionArea = createTextArea("PacksPanel.description", null, null, null);
        spaceLabel = createPanelWithLabel("PacksPanel.space", null, null);
        if (IoHelper.supported("getFreeSpace"))
        {
            add(Box.createRigidArea(new Dimension(0, 3)));
            freeSpaceLabel = createPanelWithLabel("PacksPanel.freespace", null, null);
        }
    }

    /*
    * (non-Javadoc)
    * 
    * @see com.izforge.izpack.panels.PacksPanelInterface#getLangpack()
    */
    public LocaleDatabase getLangpack()
    {
        return (langpack);
    }

    /*
    * (non-Javadoc)
    * 
    * @see com.izforge.izpack.panels.PacksPanelInterface#getBytes()
    */
    public long getBytes()
    {
        return (bytes);
    }

    /*
    * (non-Javadoc)
    * 
    * @see com.izforge.izpack.panels.PacksPanelInterface#setBytes(int)
    */
    public void setBytes(long bytes)
    {
        this.bytes = bytes;
    }

    /*
    * (non-Javadoc)
    * 
    * @see com.izforge.izpack.panels.PacksPanelInterface#showSpaceRequired()
    */
    public void showSpaceRequired()
    {
        if (spaceLabel != null)
        {
            spaceLabel.setText(Pack.toByteUnitsString(bytes));
        }
    }

    /*
    * (non-Javadoc)
    * 
    * @see com.izforge.izpack.panels.PacksPanelInterface#showFreeSpace()
    */
    public void showFreeSpace()
    {
        if (IoHelper.supported("getFreeSpace") && freeSpaceLabel != null)
        {
            String msg = null;
            freeBytes = IoHelper.getFreeSpace(IoHelper.existingParent(
                    new File(idata.getInstallPath())).getAbsolutePath());
            if (freeBytes < 0)
            {
                msg = parent.langpack.getString("PacksPanel.notAscertainable");
            }
            else
            {
                msg = Pack.toByteUnitsString(freeBytes);
            }
            freeSpaceLabel.setText(msg);
        }
    }

    public Debugger getDebugger()
    {
        return null;
    }

    /**
     * Indicates wether the panel has been validated or not.
     *
     * @return true if the needed space is less than the free space, else false
     */
    public boolean isValidated()
    {
        refreshPacksToInstall();
        if (IoHelper.supported("getFreeSpace") && freeBytes >= 0 && freeBytes <= bytes)
        {
            JOptionPane.showMessageDialog(this, parent.langpack
                    .getString("PacksPanel.notEnoughSpace"), parent.langpack
                    .getString("installer.error"), JOptionPane.ERROR_MESSAGE);
            return (false);
        }
        return (true);
    }

    /**
     * Asks to make the XML panel data.
     *
     * @param panelRoot The XML tree to write the data in.
     */
    public void makeXMLData(IXMLElement panelRoot)
    {
        new ImgPacksPanelAutomationHelper().makeXMLData(idata, panelRoot);
    }


    /**
     * This method tries to resolve the localized name of the given pack. If this is not possible,
     * the name given in the installation description file in ELEMENT <pack> will be used.
     *
     * @param pack for which the name should be resolved
     * @return localized name of the pack
     */
    private String getI18NPackName(Pack pack)
    {
        // Internationalization code
        String packName = pack.name;
        String key = pack.id;
        if (langpack != null && pack.id != null && !"".equals(pack.id))
        {
            packName = langpack.getString(key);
        }
        if ("".equals(packName) || key == null || key.equals(packName))
        {
            packName = pack.name;
        }
        return (packName);
    }

    public String getI18NPackName(String packId)
    {
        Pack pack = idToPack.get(packId);
        if (pack == null)
        {
            return packId;
        }
        return getI18NPackName(pack);
    }

    /**
     * Layout helper method:<br>
     * Creates an label with a message given by msgId and an icon given by the iconId. If layout and
     * constraints are not null, the label will be added to layout with the given constraints. The
     * label will be added to this object.
     *
     * @param msgId       identifier for the IzPack langpack
     * @param iconId      identifier for the IzPack icons
     * @param layout      layout to be used
     * @param constraints constraints to be used
     * @return the created label
     */
    protected JLabel createLabel(String msgId, String iconId, GridBagLayout layout,
                                 GridBagConstraints constraints)
    {
        JLabel label = LabelFactory.create(parent.langpack.getString(msgId), parent.icons
                .getImageIcon(iconId), TRAILING);
        if (layout != null && constraints != null)
        {
            layout.addLayoutComponent(label, constraints);
        }
        add(label);
        return (label);
    }

    /**
     * Creates a panel containing a anonymous label on the left with the message for the given msgId
     * and a label on the right side with initial no text. The right label will be returned. If
     * layout and constraints are not null, the label will be added to layout with the given
     * constraints. The panel will be added to this object.
     *
     * @param msgId       identifier for the IzPack langpack
     * @param layout      layout to be used
     * @param constraints constraints to be used
     * @return the created (right) label
     */
    protected JLabel createPanelWithLabel(String msgId, GridBagLayout layout,
                                          GridBagConstraints constraints)
    {
        JPanel panel = new JPanel();
        JLabel label = new JLabel();
        if (label == null)
        {
            label = new JLabel("");
        }
        panel.setAlignmentX(LEFT_ALIGNMENT);
        panel.setLayout(new BoxLayout(panel, BoxLayout.X_AXIS));
        panel.add(LabelFactory.create(parent.langpack.getString(msgId)));
        panel.add(Box.createHorizontalGlue());
        panel.add(label);
        if (layout != null && constraints != null)
        {
            layout.addLayoutComponent(panel, constraints);
        }
        add(panel);
        return (label);
    }

    private void refreshPacksToInstall()
    {
        idata.selectedPacks.clear();
        CheckBoxNode cbn = (CheckBoxNode) getTree().getModel().getRoot();
        Enumeration e = cbn.depthFirstEnumeration();
        while (e.hasMoreElements())
        {
            CheckBoxNode c = (CheckBoxNode) e.nextElement();
            if (c.isSelected() || c.isPartial())
            {
                idata.selectedPacks.add(c.getPack());
            }
        }
    }

    /**
     * Creates a text area with standard settings and the title given by the msgId. If scroller is
     * not null, the create text area will be added to the scroller and the scroller to this object,
     * else the text area will be added directly to this object. If layout and constraints are not
     * null, the text area or scroller will be added to layout with the given constraints. The text
     * area will be returned.
     *
     * @param msgId       identifier for the IzPack langpack
     * @param scroller    the scroller to be used
     * @param layout      layout to be used
     * @param constraints constraints to be used
     * @return the created text area
     */
    protected JTextArea createTextArea(String msgId, JScrollPane scroller, GridBagLayout layout,
                                       GridBagConstraints constraints)
    {
        JTextArea area = new JTextArea();
        // area.setMargin(new Insets(2, 2, 2, 2));
        area.setAlignmentX(LEFT_ALIGNMENT);
        area.setCaretPosition(0);
        area.setEditable(false);
        area.setEditable(false);
        area.setOpaque(false);
        area.setLineWrap(true);
        area.setWrapStyleWord(true);
        area.setBorder(BorderFactory.createTitledBorder(parent.langpack.getString(msgId)));
        area.setFont(getControlTextFont());

        if (layout != null && constraints != null)
        {
            if (scroller != null)
            {
                layout.addLayoutComponent(scroller, constraints);
            }
            else
            {
                layout.addLayoutComponent(area, constraints);
            }
        }
        if (scroller != null)
        {
            scroller.setViewportView(area);
            add(scroller);
        }
        else
        {
            add(area);
        }
        return (area);

    }

    /**
     * FIXME Creates the JTree component and calls all initialization tasks
     *
     * @param width
     * @param scroller
     * @param layout
     * @param constraints
     * @return
     */
    protected JTree createPacksTree(int width, JScrollPane scroller, GridBagLayout layout,
                                    GridBagConstraints constraints)
    {
        JTree tree = new JTree((CheckBoxNode) populateTreePacks(null));
        packsTree = tree;
        tree.setCellRenderer(new CheckBoxNodeRenderer(this));
        tree.setEditable(false);
        tree.setShowsRootHandles(true);
        tree.setRootVisible(false);
        checkTreeController = new CheckTreeController(this);
        tree.addMouseListener(checkTreeController);
        tree.setBorder(BorderFactory.createEmptyBorder(0, 2, 0, 2));
        tree.setBackground(Color.white);
        tree.setToggleClickCount(0);
        //tree.setRowHeight(0);

        //table.getSelectionModel().addTreeSelectionListener(this);
        scroller.setViewportView(tree);
        scroller.setAlignmentX(LEFT_ALIGNMENT);
        scroller.getViewport().setBackground(Color.white);
        scroller.setPreferredSize(new Dimension(width, (idata.guiPrefs.height / 3 + 30)));

        if (layout != null && constraints != null)
        {
            layout.addLayoutComponent(scroller, constraints);
        }
        add(scroller);
        return (tree);
    }

    /**
     * Computes pack related data like the names or the dependencies state.
     *
     * @param packs
     */
    private void computePacks(List packs)
    {
        names = new HashMap<String, Pack>();
        dependenciesExist = false;
        for (Object pack1 : packs)
        {
            Pack pack = (Pack) pack1;
            names.put(pack.name, pack);
            if (pack.dependencies != null || pack.excludeGroup != null)
            {
                dependenciesExist = true;
            }
        }
    }

    /**
     * Refresh tree data from the PacksModel. This functions serves as a bridge
     * between the flat PacksModel and the tree data model.
     */
    public void fromModel()
    {
        TreeModel model = this.packsTree.getModel();
        CheckBoxNode root = (CheckBoxNode) model.getRoot();
        updateModel(root);
    }

    private int getRowIndex(Pack pack)
    {
        Object o = packToRowNumber.get(pack);
        if (o == null)
        {
            return -1;
        }
        Integer ret = (Integer) o;
        return ret;
    }

    /**
     * Helper function for fromModel() - runs the recursion
     *
     * @param rnode
     */
    private void updateModel(CheckBoxNode rnode)
    {
        int rowIndex = getRowIndex(rnode.getPack());
        if (rowIndex > 0)
        {
            Integer state = (Integer) packsModel.getValueAt(rowIndex, 0);
            if ((state == -2) && rnode.getChildCount() > 0)
            {
                boolean dirty = false;
                Enumeration toBeDeselected = rnode.depthFirstEnumeration();
                while (toBeDeselected.hasMoreElements())
                {
                    CheckBoxNode cbn = (CheckBoxNode) toBeDeselected.nextElement();
                    boolean chDirty = cbn.isSelected() || cbn.isPartial() || cbn.isEnabled();
                    dirty = dirty || chDirty;
                    if (chDirty)
                    {
                        cbn.setPartial(false);
                        cbn.setSelected(false);
                        cbn.setEnabled(false);
                        setModelValue(cbn);
                    }
                }
                if (dirty)
                {
                    fromModel();
                }
                return;
            }
        }

        Enumeration e = rnode.children();
        while (e.hasMoreElements())
        {
            Object next = e.nextElement();
            CheckBoxNode cbnode = (CheckBoxNode) next;
            String nodeText = cbnode.getId();
            Object nodePack = idToPack.get(nodeText);
            if (!cbnode.isPartial())
            {
                int childRowIndex = getRowIndex((Pack) nodePack);
                if (childRowIndex > 0)
                {
                    Integer state = (Integer) packsModel.getValueAt(childRowIndex, 0);
                    cbnode.setEnabled(state >= 0);
                    cbnode.setSelected(Math.abs(state.intValue()) == 1);
                }
            }
            updateModel(cbnode);
        }
    }

    /**
     * Updates a value for pack in PacksModel with data from a checkbox node
     *
     * @param cbnode This is the checkbox node which contains model values
     */
    public void setModelValue(CheckBoxNode cbnode)
    {
        String id = cbnode.getId();
        Object nodePack = idToPack.get(id);
        int value = 0;
        if (cbnode.isEnabled() && cbnode.isSelected())
        {
            value = 1;
        }
        if (!cbnode.isEnabled() && cbnode.isSelected())
        {
            value = -1;
        }
        if (!cbnode.isEnabled() && !cbnode.isSelected())
        {
            value = -2;
        }
        int rowIndex = getRowIndex((Pack) nodePack);
        if (rowIndex > 0)
        {
            Integer newValue = value;
            Integer modelValue = (Integer) packsModel.getValueAt(rowIndex, 0);
            if (!newValue.equals(modelValue))
            {
                packsModel.setValueAt(newValue, rowIndex, 0);
            }
        }
    }

    /**
     * Initialize tree model sructures
     */
    private void createTreeData()
    {
        treeData = new HashMap<String, ArrayList<String>>();
        idToPack = new HashMap<String, Pack>();

        java.util.Iterator iter = idata.availablePacks.iterator();
        while (iter.hasNext())
        {
            Pack p = (Pack) iter.next();
            idToPack.put(p.id, p);
            if (p.parent != null)
            {
                ArrayList<String> kids = null;
                if (treeData.containsKey(p.parent))
                {
                    kids = treeData.get(p.parent);
                }
                else
                {
                    kids = new ArrayList<String>();
                }
                kids.add(p.id);
                treeData.put(p.parent, kids);
            }
        }
    }

    /**
     * Shows and updates the description text in the panel
     *
     * @param id
     */
    public void setDescription(String id)
    {
        VariableSubstitutor vs = new VariableSubstitutor(idata.getVariables());
        if (descriptionArea != null)
        {
            Pack pack = idToPack.get(id);
            String desc = "";
            String key = pack.id + ".description";
            if (langpack != null && pack.id != null && !"".equals(pack.id))
            {
                desc = langpack.getString(key);
            }
            if ("".equals(desc) || key.equals(desc))
            {
                desc = pack.description;
            }
            desc = vs.substitute(desc, null);
            descriptionArea.setText(desc);
        }
    }

    /**
     * Shows and updates the dependencies text in the panel
     *
     * @param id
     */
    public void setDependencies(String id)
    {
        if (dependencyArea != null)
        {
            Pack pack = idToPack.get(id);
            List<String> dep = pack.dependencies;
            String list = "";
            if (dep != null)
            {
                list += (langpack == null) ? "Dependencies: " : langpack
                        .getString("PacksPanel.dependencies");
            }
            for (int j = 0; dep != null && j < dep.size(); j++)
            {
                String name = dep.get(j);
                list += getI18NPackName(names.get(name));
                if (j != dep.size() - 1)
                {
                    list += ", ";
                }
            }

            // add the list of the packs to be excluded
            String excludeslist = (langpack == null) ? "Excludes: " : langpack
                    .getString("PacksPanel.excludes");
            int numexcludes = 0;
            int i = getRowIndex(pack);
            if (pack.excludeGroup != null)
            {
                for (int q = 0; q < idata.availablePacks.size(); q++)
                {
                    Pack otherpack = (Pack) idata.availablePacks.get(q);
                    String exgroup = otherpack.excludeGroup;
                    if (exgroup != null)
                    {
                        if (q != i && pack.excludeGroup.equals(exgroup))
                        {

                            excludeslist += getI18NPackName(otherpack) + ", ";
                            numexcludes++;
                        }
                    }
                }
            }
            // concatenate
            if (dep != null)
            {
                excludeslist = "    " + excludeslist;
            }
            if (numexcludes > 0)
            {
                list += excludeslist;
            }
            if (list.endsWith(", "))
            {
                list = list.substring(0, list.length() - 2);
            }

            // and display the result
            dependencyArea.setText(list);
        }
    }

    /**
     * Gives a CheckBoxNode instance from the id
     *
     * @param id
     * @return
     */
    public CheckBoxNode getCbnById(String id)
    {
        return this.idToCheckBoxNode.get(id);
    }

    /**
     * Reads the available packs and creates the JTree structure based on
     * the parent definitions.
     *
     * @param parent
     * @return
     */
    private Object populateTreePacks(String parent)
    {
        if (parent == null) // the root node
        {
            java.util.Iterator iter = idata.availablePacks.iterator();
            ArrayList rootNodes = new ArrayList();
            while (iter.hasNext())
            {
                Pack p = (Pack) iter.next();
                if (p.parent == null)
                {
                    rootNodes.add(populateTreePacks(p.id));
                }
            }
            TreeNode nv = new CheckBoxNode("Root", "Root", rootNodes.toArray(), true);
            return nv;
        }
        else
        {
            ArrayList links = new ArrayList();
            Object kidsObject = treeData.get(parent);
            Pack p = idToPack.get(parent);
            String translated = getI18NPackName(parent);

            if (kidsObject != null)
            {
                ArrayList kids = (ArrayList) kidsObject;
                for (Object kid : kids)
                {
                    String kidId = (String) kid;
                    links.add(populateTreePacks(kidId));
                }

                CheckBoxNode cbn = new CheckBoxNode(parent, translated, links.toArray(), true);
                idToCheckBoxNode.put(cbn.getId(), cbn);
                cbn.setPack(p);
                cbn.setTotalSize(p.nbytes);
                return cbn;
            }
            else
            {
                CheckBoxNode cbn = new CheckBoxNode(parent, translated, true);
                idToCheckBoxNode.put(cbn.getId(), cbn);
                cbn.setPack(p);
                cbn.setTotalSize(p.nbytes);
                return cbn;
            }
        }
    }

    /**
     * Called when the panel becomes active. If a derived class implements this method also, it is
     * recomanded to call this method with the super operator first.
     */
    public void panelActivate()
    {
        try
        {

            // TODO the PacksModel could be patched such that isCellEditable
            // allows returns false. In that case the PacksModel must not be
            // adapted here.
            packsModel = new PacksModel(this, idata, this.parent.getRules())
            {
                /**
                 * Required (serializable)
                 */
                private static final long serialVersionUID = 697462278279845304L;

                public boolean isCellEditable(int rowIndex, int columnIndex)
                {
                    return false;
                }
            };

            //initialize helper map to increa performance
            packToRowNumber = new HashMap<Pack, Integer>();
            java.util.Iterator rowpack = idata.availablePacks.iterator();
            while (rowpack.hasNext())
            {
                Pack p = (Pack) rowpack.next();
                packToRowNumber.put(p, idata.availablePacks.indexOf(p));
            }

            // Init tree structures
            createTreeData();

            // Create panel GUI (and populate the TJtree)
            createNormalLayout();

            // Reload the data from the PacksModel into the tree in order the initial
            // dependencies to be resolved and effective
            fromModel();

            // Init the pack sizes (individual and cumulative)
            CheckBoxNode root = (CheckBoxNode) packsTree.getModel().getRoot();
            checkTreeController.updateAllParents(root);
            CheckTreeController.initTotalSize(root, false);

            // Ugly repaint because of a bug in tree.treeDidChange
            packsTree.revalidate();
            packsTree.repaint();

            tableScroller.setColumnHeaderView(null);
            tableScroller.setColumnHeader(null);

            // set the JCheckBoxes to the currently selected panels. The
            // selection might have changed in another panel
            java.util.Iterator iter = idata.availablePacks.iterator();
            bytes = 0;
            while (iter.hasNext())
            {
                Pack p = (Pack) iter.next();
                if (p.required)
                {
                    bytes += p.nbytes;
                    continue;
                }
                if (idata.selectedPacks.contains(p))
                {
                    bytes += p.nbytes;
                }
            }
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
        showSpaceRequired();
        showFreeSpace();
    }

    /*
    * (non-Javadoc)
    * 
    * @see com.izforge.izpack.installer.IzPanel#getSummaryBody()
    */
    public String getSummaryBody()
    {
        StringBuffer retval = new StringBuffer(256);
        Iterator iter = idata.selectedPacks.iterator();
        boolean first = true;
        while (iter.hasNext())
        {
            if (!first)
            {
                retval.append("<br>");
            }
            first = false;
            Pack pack = (Pack) iter.next();
            retval.append(getI18NPackName(pack));
        }
        return retval.toString();
    }


    public JTree getTree()
    {
        return packsTree;
    }

}

/**
 * The renderer model for individual checkbox nodes in a JTree. It renders the
 * checkbox and a label for the pack size.
 *
 * @author <a href="vralev@redhat.com">Vladimir Ralev</a>
 * @version $Revision: 1.1 $
 */
class CheckBoxNodeRenderer implements TreeCellRenderer
{
    private static final JPanel rendererPanel = new JPanel();
    private static final JLabel packSizeLabel = new JLabel();
    private static final JCheckBox checkbox = new JCheckBox();
    private static final JCheckBox normalCheckBox = new JCheckBox();
    private static final java.awt.Font normalFont = new JCheckBox().getFont();
    private static final java.awt.Font boldFont = new java.awt.Font(normalFont.getFontName(),
            java.awt.Font.BOLD,
            normalFont.getSize());
    private static final java.awt.Font plainFont = new java.awt.Font(normalFont.getFontName(),
            java.awt.Font.PLAIN,
            normalFont.getSize());
    private static final Color annotationColor = new Color(0, 0, 120); // red
    private static final Color changedColor = new Color(200, 0, 0);

    private static Color selectionForeground, selectionBackground,
            textForeground, textBackground;

    TreePacksPanel treePacksPanel;

    public CheckBoxNodeRenderer(TreePacksPanel t)
    {
        selectionForeground = UIManager.getColor("Tree.selectionForeground");
        selectionBackground = UIManager.getColor("Tree.selectionBackground");
        textForeground = UIManager.getColor("Tree.textForeground");
        textBackground = UIManager.getColor("Tree.textBackground");
        treePacksPanel = t;

        int treeWidth = t.getTree().getPreferredSize().width;
        int height = checkbox.getPreferredSize().height;
        int cellWidth = treeWidth - treeWidth / 4;

        //Don't touch, it fixes various layout bugs in swing/awt
        rendererPanel.setLayout(new java.awt.BorderLayout(0, 0));
        rendererPanel.setBackground(textBackground);
        rendererPanel.add(java.awt.BorderLayout.WEST, checkbox);

        rendererPanel.setAlignmentX((float) 0);
        rendererPanel.setAlignmentY((float) 0);
        rendererPanel.add(java.awt.BorderLayout.EAST, packSizeLabel);

        rendererPanel.setMinimumSize(new Dimension(cellWidth, height));
        rendererPanel.setPreferredSize(new Dimension(cellWidth, height));
        rendererPanel.setSize(new Dimension(cellWidth, height));

        rendererPanel.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0));
    }

    public Component getTreeCellRendererComponent(JTree tree, Object value,
                                                  boolean selected, boolean expanded, boolean leaf, int row,
                                                  boolean hasFocus)
    {
        treePacksPanel.fromModel();

        if (selected)
        {
            checkbox.setForeground(selectionForeground);
            checkbox.setBackground(selectionBackground);
            rendererPanel.setForeground(selectionForeground);
            rendererPanel.setBackground(selectionBackground);
            packSizeLabel.setBackground(selectionBackground);
        }
        else
        {
            checkbox.setForeground(textForeground);
            checkbox.setBackground(textBackground);
            rendererPanel.setForeground(textForeground);
            rendererPanel.setBackground(textBackground);
            packSizeLabel.setBackground(textBackground);
        }

        if ((value != null) && (value instanceof CheckBoxNode))
        {
            CheckBoxNode node = (CheckBoxNode) value;

            if (node.isTotalSizeChanged())
            {
                packSizeLabel.setForeground(changedColor);
            }
            else
            {
                if (selected)
                {
                    packSizeLabel.setForeground(selectionForeground);
                }
                else
                {
                    packSizeLabel.setForeground(annotationColor);
                }
            }

            checkbox.setText(node.getTranslatedText());

            packSizeLabel.setText(Pack.toByteUnitsString(node.getTotalSize()));

            if (node.isPartial())
            {
                checkbox.setSelected(false);
            }
            else
            {
                checkbox.setSelected(node.isSelected());
            }

            checkbox.setEnabled(node.isEnabled());
            packSizeLabel.setEnabled(node.isEnabled());

            if (node.getChildCount() > 0)
            {
                checkbox.setFont(boldFont);
                packSizeLabel.setFont(boldFont);
            }
            else
            {
                checkbox.setFont(normalFont);
                packSizeLabel.setFont(plainFont);
            }

            if (node.isPartial())
            {
                checkbox.setIcon(new PartialIcon());
            }
            else
            {
                checkbox.setIcon(normalCheckBox.getIcon());
            }
        }
        return rendererPanel;
    }

    public Component getCheckRenderer()
    {
        return rendererPanel;
    }

}

/**
 * The model structure for a JTree node.
 *
 * @author <a href="vralev@redhat.com">Vladimir Ralev</a>
 * @version $Revision: 1.1 $
 */
class CheckBoxNode extends DefaultMutableTreeNode
{

    /**
     * Required (serializable)
     */
    private static final long serialVersionUID = 8743154051564336973L;
    String id;
    boolean selected;
    boolean partial;
    boolean enabled;
    boolean totalSizeChanged;
    String translatedText;
    Pack pack;
    long totalSize;

    public CheckBoxNode(String id, String translated, boolean selected)
    {
        this.id = id;
        this.selected = selected;
        this.translatedText = translated;
    }

    public CheckBoxNode(String id, String translated, Object elements[], boolean selected)
    {
        this.id = id;
        this.translatedText = translated;
        for (int i = 0, n = elements.length; i < n; i++)
        {
            CheckBoxNode tn = (CheckBoxNode) elements[i];
            add(tn);
        }
    }

    public boolean isLeaf()
    {
        return this.getChildCount() == 0;
    }

    public boolean isSelected()
    {
        return selected;
    }

    public void setSelected(boolean newValue)
    {
        selected = newValue;
    }

    public String getId()
    {
        return id;
    }

    public void setId(String newValue)
    {
        id = newValue;
    }

    public String toString()
    {
        return getClass().getName() + "[" + id + "/" + selected + "]";
    }

    public boolean isPartial()
    {
        return partial;
    }

    public void setPartial(boolean partial)
    {
        this.partial = partial;
        if (partial)
        {
            setSelected(true);
        }
    }

    public boolean isEnabled()
    {
        return enabled;
    }

    public void setEnabled(boolean enabled)
    {
        this.enabled = enabled;
    }

    public String getTranslatedText()
    {
        return translatedText;
    }

    public void setTranslatedText(String translatedText)
    {
        this.translatedText = translatedText;
    }

    public Pack getPack()
    {
        return pack;
    }

    public void setPack(Pack pack)
    {
        this.pack = pack;
    }

    public long getTotalSize()
    {
        return totalSize;
    }

    public void setTotalSize(long totalSize)
    {
        this.totalSize = totalSize;
    }

    public boolean isTotalSizeChanged()
    {
        return totalSizeChanged;
    }

    public void setTotalSizeChanged(boolean totalSizeChanged)
    {
        this.totalSizeChanged = totalSizeChanged;
    }
}

/**
 * Special checkbox icon which shows partially selected nodes.
 *
 * @author <a href="vralev@redhat.com">Vladimir Ralev</a>
 * @version $Revision: 1.1 $
 */
class PartialIcon implements Icon
{
    protected int getControlSize()
    {
        return 13;
    }

    public void paintIcon(Component c, Graphics g, int x, int y)
    {
        int controlSize = getControlSize();
        g.setColor(MetalLookAndFeel.getControlShadow());
        g.fillRect(x, y, controlSize - 1, controlSize - 1);
        drawBorder(g, x, y, controlSize, controlSize);

        g.setColor(Color.green);
        drawCheck(c, g, x, y);
    }

    private void drawBorder(Graphics g, int x, int y, int w, int h)
    {
        g.translate(x, y);

        // outer frame rectangle
        g.setColor(MetalLookAndFeel.getControlDarkShadow());
        g.setColor(new Color(0.4f, 0.4f, 0.4f));
        g.drawRect(0, 0, w - 2, h - 2);

        // middle frame
        g.setColor(MetalLookAndFeel.getControlHighlight());
        g.setColor(new Color(0.6f, 0.6f, 0.6f));
        g.drawRect(1, 1, w - 2, h - 2);

        // background
        g.setColor(new Color(0.99f, 0.99f, 0.99f));
        g.fillRect(2, 2, w - 3, h - 3);

        //some extra lines for FX
        g.setColor(MetalLookAndFeel.getControl());
        g.drawLine(0, h - 1, 1, h - 2);
        g.drawLine(w - 1, 0, w - 2, 1);
        g.translate(-x, -y);
    }

    protected void drawCheck(Component c, Graphics g, int x, int y)
    {
        int controlSize = getControlSize();
        g.setColor(new Color(0.0f, 0.7f, 0.0f));

        g.fillOval(x + controlSize / 2 - 2, y + controlSize / 2 - 2, 6, 6);
    }

    public int getIconWidth()
    {
        return getControlSize();
    }

    public int getIconHeight()
    {
        return getControlSize();
    }
}

/**
 * Controller class which handles the mouse clicks on checkbox nodes. Also
 * contains utility methods to update the sizes and the states of the nodes.
 *
 * @author <a href="vralev@redhat.com">Vladimir Ralev</a>
 * @version $Revision: 1.1 $
 */
class CheckTreeController extends MouseAdapter
{
    JTree tree;
    TreePacksPanel treePacksPanel;
    int checkWidth = new JCheckBox().getPreferredSize().width;

    public CheckTreeController(TreePacksPanel p)
    {
        this.tree = p.getTree();
        this.treePacksPanel = p;
    }

    private void selectNode(CheckBoxNode current)
    {
        current.setPartial(false);
        treePacksPanel.setModelValue(current);
        Enumeration e = current.depthFirstEnumeration();
        while (e.hasMoreElements())
        {
            CheckBoxNode child = (CheckBoxNode) e.nextElement();
            child.setSelected(current.isSelected() || child.getPack().required);
            if (!child.isSelected())
            {
                child.setPartial(false);
            }
            treePacksPanel.setModelValue(child);
        }
        treePacksPanel.fromModel();
    }

    private boolean hasExcludes(CheckBoxNode node)
    {
        Enumeration e = node.depthFirstEnumeration();
        while (e.hasMoreElements())
        {
            CheckBoxNode cbn = (CheckBoxNode) e.nextElement();
            if (cbn.getPack().excludeGroup != null)
            {
                return true;
            }
        }
        return false;
    }

    public void mouseReleased(MouseEvent me)
    {
        TreePath path = tree.getPathForLocation(me.getX(), me.getY());
        if (path == null)
        {
            return;
        }
        CheckBoxNode current = (CheckBoxNode) path.getLastPathComponent();
        treePacksPanel.setDescription(current.getId());
        treePacksPanel.setDependencies(current.getId());
        if (me.getX() > tree.getPathBounds(path).x + checkWidth)
        {
            return;
        }

        // If this pack is required, leave it alone
        if (current.getPack().required)
        {
            return;
        }

        boolean currIsSelected = current.isSelected() & !current.isPartial();
        boolean currIsPartial = current.isPartial();
        boolean currHasExcludes = hasExcludes(current);
        CheckBoxNode root = (CheckBoxNode) current.getRoot();

        if (currIsPartial && currHasExcludes)
        {
            current.setSelected(false);
            selectNode(current); // deselect actually
            updateAllParents(root);
        }
        else
        {
            if (!currIsSelected)
            {
                selectAllChildNodes(current);
            }
            current.setSelected(!currIsSelected);
            selectNode(current);
            updateAllParents(root);
        }

        initTotalSize(root, true);

        // must override the bytes being computed at packsModel
        treePacksPanel.setBytes((int) root.getTotalSize());
        treePacksPanel.showSpaceRequired();
        tree.treeDidChange();
    }

    public void selectAllChildNodes(CheckBoxNode cbn)
    {
        Enumeration e = cbn.children();
        while (e.hasMoreElements())
        {
            CheckBoxNode subCbn = (CheckBoxNode) e.nextElement();
            selectAllDependencies(subCbn);
            if (subCbn.getChildCount() > 0)
            {
                selectAllChildNodes(subCbn);
            }

            subCbn.setSelected(true);
            // we need this, because the setModel ignored disabled values
            subCbn.setEnabled(true);
            treePacksPanel.setModelValue(subCbn);
            subCbn.setEnabled(!subCbn.getPack().required);
        }
    }

    public void selectAllDependencies(CheckBoxNode cbn)
    {
        Pack pack = cbn.getPack();
        List<String> deps = pack.getDependencies();
        if (deps == null)
        {
            return;
        }
        Iterator<String> e = deps.iterator();
        while (e.hasNext())
        {
            String depId = e.next();
            CheckBoxNode depCbn = treePacksPanel.getCbnById(depId);
            selectAllDependencies(depCbn);
            if (depCbn.getChildCount() > 0)
            {
                if (!depCbn.isSelected() || depCbn.isPartial())
                {
                    selectAllChildNodes(depCbn);
                }
            }
            depCbn.setSelected(true);
            // we need this, because the setModel ignored disabled values
            depCbn.setEnabled(true);
            treePacksPanel.setModelValue(depCbn);
            depCbn.setEnabled(!depCbn.getPack().required);
        }
    }

    /**
     * Updates partial/deselected/selected state of all parent nodes.
     * This is needed and is a patch to allow unrelated nodes (in terms of the tree)
     * to fire updates for each other.
     *
     * @param root
     */
    public void updateAllParents(CheckBoxNode root)
    {
        Enumeration rootEnum = root.depthFirstEnumeration();
        while (rootEnum.hasMoreElements())
        {
            CheckBoxNode child = (CheckBoxNode) rootEnum.nextElement();
            if (child.getParent() != null && !child.getParent().equals(root))
            {
                updateParents(child);
            }
        }
    }

    /**
     * Updates the parents of this particular node
     *
     * @param node
     */
    private void updateParents(CheckBoxNode node)
    {
        CheckBoxNode parent = (CheckBoxNode) node.getParent();
        if (parent != null && !parent.equals(parent.getRoot()))
        {
            Enumeration ne = parent.children();
            boolean allSelected = true;
            boolean allDeselected = true;
            while (ne.hasMoreElements())
            {
                CheckBoxNode child = (CheckBoxNode) ne.nextElement();
                if (child.isSelected())
                {
                    allDeselected = false;
                }
                else
                {
                    allSelected = false;
                }
                if (child.isPartial())
                {
                    allSelected = allDeselected = false;
                }
                if (!allSelected && !allDeselected)
                {
                    break;
                }
            }
            if (parent.getChildCount() > 0)
            {
                if (!allSelected && !allDeselected)
                {
                    setPartialParent(parent);
                }
                else
                {
                    parent.setPartial(false);
                }
                if (allSelected)
                {
                    parent.setSelected(true);
                }
                if (allDeselected)
                {
                    parent.setSelected(false);
                }
                treePacksPanel.setModelValue(parent);
                if (allSelected || allDeselected)
                {
                    updateParents(parent);
                }
            }
            //updateTotalSize(node);
        }
    }

    public static void setPartialParent(CheckBoxNode node)
    {
        node.setPartial(true);
        CheckBoxNode parent = (CheckBoxNode) node.getParent();
        if (parent != null && !parent.equals(parent.getRoot()))
        {
            setPartialParent(parent);
        }
    }

    public static long initTotalSize(CheckBoxNode node, boolean markChanged)
    {
        if (node.isLeaf())
        {
            return node.getPack().nbytes;
        }
        Enumeration e = node.children();
        Pack nodePack = node.getPack();
        long bytes = 0;
        if (nodePack != null)
        {
            bytes = nodePack.nbytes;
        }
        while (e.hasMoreElements())
        {
            CheckBoxNode c = (CheckBoxNode) e.nextElement();
            long size = initTotalSize(c, markChanged);
            if (c.isSelected() || c.isPartial())
            {
                bytes += size;
            }
        }
        if (markChanged)
        {
            long old = node.getTotalSize();
            if (old != bytes)
            {
                node.setTotalSizeChanged(true);
            }
            else
            {
                node.setTotalSizeChanged(false);
            }
        }
        node.setTotalSize(bytes);
        return bytes;
    }
}
