/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 ro.nextreports.designer;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.JComponent;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JToggleButton;
import javax.swing.JToolBar;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreePath;

import org.jdesktop.swingx.JXTree;
import org.jdesktop.swingx.decorator.ColorHighlighter;
import org.jdesktop.swingx.decorator.HighlightPredicate;

import ro.nextreports.designer.action.undo.LayoutEdit;
import ro.nextreports.designer.grid.DefaultGridModel;
import ro.nextreports.designer.grid.event.GridModelEvent;
import ro.nextreports.designer.grid.event.GridModelListener;
import ro.nextreports.designer.ui.BaseDialog;
import ro.nextreports.designer.util.I18NSupport;
import ro.nextreports.designer.util.ImageUtil;
import ro.nextreports.designer.util.Show;
import ro.nextreports.designer.util.TreeUtil;

import ro.nextreports.engine.util.ObjectCloner;
import ro.nextreports.engine.ReportLayout;
import ro.nextreports.engine.band.Band;
import ro.nextreports.engine.band.BandElement;
import com.jgoodies.looks.HeaderStyle;
import com.jgoodies.looks.Options;

/**
 * @author Decebal Suiu
 */
public class StructurePanel extends JPanel implements GridModelListener, ActionListener {

    private JToggleButton showEmptyButton;
    private StructureTreeNode rootNode;
    private JXTree structureTree;
    private StructureTreeModel structureTreeModel;
    private JPopupMenu popup;
    // when layout is cleared entirely, we must create a ReportGridCell for the first row inserted and
    // firstReportCell variable becomes true
    // on columns inserted event we must test this variable and if it is true we must return
    private boolean firstReportCell = false;

    public StructurePanel() {
        super();
        initComponents();
    }

    public JXTree getStructureTree() {
        return structureTree;
    }

    public StructureTreeModel getStructureTreeModel() {
        return structureTreeModel;
    }

    public void refresh() {
        rootNode = new StructureTreeNode();
        for (Band band : LayoutHelper.getReportLayout().getBands()) {
            rootNode.add(new StructureTreeNode(band));
        }
        structureTreeModel.setRoot(rootNode);
    }
    
    public StructureTreeNode getBandElementTreeNode(int row, int column) {
        Enumeration en = rootNode.breadthFirstEnumeration();
        while (en.hasMoreElements()) {
            while (en.hasMoreElements()) {
                StructureTreeNode node = (StructureTreeNode) en.nextElement();
                Object userObject = node.getUserObject();
                if (userObject instanceof ReportGridCell) {
                    ReportGridCell reportGridCell = (ReportGridCell) userObject;
                    if ((row == reportGridCell.getRow()) && (column == reportGridCell.getColumn())) {
                        return node;
                    }
                }
            }
        }

        return null;
    }
    
    public StructureTreeNode getRowElementTreeNode(int row) {
        Enumeration en = rootNode.breadthFirstEnumeration();
        while (en.hasMoreElements()) {
            while (en.hasMoreElements()) {
                StructureTreeNode node = (StructureTreeNode) en.nextElement();
                Object userObject = node.getUserObject();
                if (userObject instanceof Band) {
                	Band band = (Band) userObject;
                	for (int i=0, size=node.getChildCount(); i<size; i++) {
                		StructureTreeNode child = (StructureTreeNode)node.getChildAt(i);
                		int bandRow = (Integer) child.getUserObject();
                		int gridRow = LayoutHelper.getReportLayout().getGridRow(band.getName(), bandRow);
                		if (gridRow == row) {
                			return child;
                		}
                	}                   
                }
            }
        }

        return null;
    }

    public void gridChanged(GridModelEvent event) {
//        System.out.println("StructurePanel.gridChanged()");

        int eventType = event.getType();
        if (eventType == GridModelEvent.ROWS_INSERTED) {
//            System.out.println("Row inserted ..............");
            int row = event.getFirstRow();
//            System.out.println("row = " + row);
            String bandName = Globals.getReportGrid().getBandName(row);
//            System.out.println("bandName = " + bandName);
            int columnCount = Globals.getReportGrid().getColumnCount();
            BandLocation bandLocation = Globals.getReportGrid().getBandLocation(bandName);
            // after a clear all
            if (bandLocation == null) {
                return;
            }
            int bandRow = bandLocation.getRow(row);
            DefaultMutableTreeNode bandTreeNode = getBandRowInsertedTreeNode(bandName, bandRow, row);
            if ((row == 0) && (columnCount == 0)) {
                StructureTreeNode elementNode = new StructureTreeNode(new ReportGridCell(null, row, 0));
                elementNode.setVisible(false);
                bandTreeNode.add(elementNode);
                firstReportCell = true;
            }
            for (int column = 0; column < columnCount; column++) {
                //System.out.println("column = " + column);
                StructureTreeNode elementNode = new StructureTreeNode(new ReportGridCell(null, row, column));
                elementNode.setVisible(false);
                bandTreeNode.add(elementNode);
            }

            structureTreeModel.reload();
            //((DefaultTreeModel) structureTree.getModel()).nodeStructureChanged(bandTreeNode.getParent());
//            System.out.println("...............................");

        } else if (eventType == GridModelEvent.ROWS_DELETED) {
//            System.out.println("Row deleted ..............");
            int row = event.getFirstRow();
            int lastRow = event.getLastRow();
            for (int i = lastRow; i >= row; i--) {
//                System.out.println("row = " + i);
                String bandName = Globals.getReportGrid().getBandName(i);
//                System.out.println("bandName = " + bandName);
                int bandRow = Globals.getReportGrid().getBandLocation(bandName).getRow(i);
                deleteBandRowTreeNode(bandName, bandRow, i);
            }
//            System.out.println("...............................");

        } else if (eventType == GridModelEvent.CELLS_UPDATED) {
//            System.out.println("Cells updated ............");
            int row = event.getFirstRow();
//            System.out.println("row = " + row);
            int column = event.getFirstColumn();
//            System.out.println("column = " + column);
            String bandName = Globals.getReportGrid().getBandName(row);
//            System.out.println("bandName = " + bandName);
//            System.out.println(Globals.getReportGrid().getBandLocations());
            DefaultGridModel gridModel = (DefaultGridModel) event.getSource();
            BandElement element = (BandElement) gridModel.getValueAt(row, column);
            int bandRow = Globals.getReportGrid().getBandLocation(bandName).getRow(row);
            StructureTreeNode elementNode = getBandElementTreeNode(bandName, bandRow, column);
            elementNode.setVisible(element != null);
            elementNode.setUserObject(new ReportGridCell(element, row, column));
            TreePath[] selectionPath = structureTree.getSelectionPaths();
            // must use reload to take visble/invisible null cells into account
            structureTreeModel.reload(elementNode.getParent());

            // reselect the nodes
            if ((selectionPath != null) && ((element != null) || !structureTreeModel.isActivatedFilter())) {
                structureTree.setSelectionPaths(selectionPath);
            }
            if (element == null) {
                Globals.getReportDesignerPanel().getPropertiesPanel().refresh();
            }

        } else if (eventType == GridModelEvent.COLUMNS_INSERTED) {
//            System.out.println("Columns inserted ..............");
            int column = event.getFirstColumn();
//            System.out.println("column = " + column);
            if (firstReportCell) {
                firstReportCell = false;
            } else {
                insertColumnNodes(column);
                structureTreeModel.nodeStructureChanged(rootNode);
            }
//            System.out.println("...............................");

        } else if (eventType == GridModelEvent.COLUMNS_DELETED) {
//            System.out.println("Columns deleted ..............");
            int column = event.getFirstColumn();
            int lastColumn = event.getLastColumn();
//            System.out.println("column = " + column);
            for (int i = lastColumn; i >= column; i--) {
                deleteColumnNodes(i);
            }
//            System.out.println("...............................");
        }
    }

    private void initComponents() {
        setLayout(new BorderLayout());

        rootNode = new StructureTreeNode();
        rootNode.add(new StructureTreeNode(new Band(ReportLayout.HEADER_BAND_NAME)));
        rootNode.add(new StructureTreeNode(new Band(ReportLayout.DETAIL_BAND_NAME)));
        rootNode.add(new StructureTreeNode(new Band(ReportLayout.FOOTER_BAND_NAME)));
        structureTreeModel = new StructureTreeModel(rootNode);

        structureTree = new JXTree(structureTreeModel);
        structureTree.setShowsRootHandles(true);
        structureTree.setCellRenderer(new StructureTreeCellRenderer());
        structureTree.setRolloverEnabled(true);
        structureTree.addHighlighter(new ColorHighlighter(HighlightPredicate.ROLLOVER_ROW, null, Color.RED)); 

        createPopup();

        // by default do not show empty cells
        Action showEmptyAction = new AbstractAction() {

            public void actionPerformed(ActionEvent event) {
                TreePath lastPath = structureTree.getSelectionPath();
                structureTreeModel.activateFilter(!showEmptyButton.isSelected());
                structureTreeModel.reload();
                if (lastPath != null) {
                    structureTree.expandPath(lastPath.getParentPath());
                    structureTree.setSelectionPath(lastPath);
                }
            }

        };
        showEmptyAction.putValue(Action.SMALL_ICON, ImageUtil.getImageIcon("empty_cell"));
        showEmptyAction.putValue(Action.SHORT_DESCRIPTION, I18NSupport.getString("report.structure.show.empty"));
        showEmptyButton = new JToggleButton(showEmptyAction);
        structureTreeModel.activateFilter(true);

        JToolBar toolBar = new JToolBar();
        toolBar.putClientProperty("JToolBar.isRollover", Boolean.TRUE); // hide buttons borders
        toolBar.putClientProperty(Options.HEADER_STYLE_KEY, HeaderStyle.BOTH);
        toolBar.setBorderPainted(false);

        // add expand action
        toolBar.add(new AbstractAction() {

            public Object getValue(String key) {
                if (AbstractAction.SMALL_ICON.equals(key)) {
                    return ImageUtil.getImageIcon("expandall");
                } else if (AbstractAction.SHORT_DESCRIPTION.equals(key)) {
                    return I18NSupport.getString("querybuilder.expand.all");
                }

                return super.getValue(key);
            }

            public void actionPerformed(ActionEvent e) {
                TreeUtil.expandAll(structureTree);
            }

        });

        // add collapse action
        toolBar.add(new AbstractAction() {

            public Object getValue(String key) {
                if (AbstractAction.SMALL_ICON.equals(key)) {
                    return ImageUtil.getImageIcon("collapseall");
                } else if (AbstractAction.SHORT_DESCRIPTION.equals(key)) {
                    return I18NSupport.getString("querybuilder.collapse.all");
                }

                return super.getValue(key);
            }

            public void actionPerformed(ActionEvent e) {
                TreeUtil.collapseAll(structureTree);
            }

        });

        toolBar.add(showEmptyButton);

        add(toolBar, BorderLayout.NORTH);
        add(new JScrollPane(structureTree), BorderLayout.CENTER);
    }

    public void addGroup(String groupName, int groupMask) {
        int children = rootNode.getChildCount();

        List<Band> headerBands = LayoutHelper.getReportLayout().getGroupHeaderBands();
        int headers = 0;
        if (headerBands != null) {
            headers = headerBands.size();
        }
        List<Band> footerBands = LayoutHelper.getReportLayout().getGroupFooterBands();
        int footers = 0;
        if (footerBands != null) {
            footers = footerBands.size();
        }
        rootNode.insert(new StructureTreeNode(new Band(ReportLayout.GROUP_FOOTER_BAND_NAME_PREFIX + groupName)), children - footers);
        rootNode.insert(new StructureTreeNode(new Band(ReportLayout.GROUP_HEADER_BAND_NAME_PREFIX + groupName)), headers);
        ((DefaultTreeModel) structureTree.getModel()).nodeStructureChanged(rootNode);
    }

    public void deleteGroup(String groupName) {
        DefaultMutableTreeNode footerBandNode = getBandTreeNode(ReportLayout.GROUP_FOOTER_BAND_NAME_PREFIX + groupName);
        if (footerBandNode != null) {
            structureTreeModel.removeNodeFromParent(footerBandNode);
        }
        DefaultMutableTreeNode headerBandNode = getBandTreeNode(ReportLayout.GROUP_HEADER_BAND_NAME_PREFIX + groupName);
        if (headerBandNode != null) {
            structureTreeModel.removeNodeFromParent(headerBandNode);
        }
    }

    private void createPopup() {
        popup = new JPopupMenu();
        JMenuItem mi = new JMenuItem(I18NSupport.getString("insert.row"));
        mi.addActionListener(this);
        mi.setActionCommand("insert");
        popup.add(mi);
        popup.setOpaque(true);
        popup.setLightWeightPopupEnabled(true);

        structureTree.addMouseListener(new MouseAdapter() {
            public void mousePressed(MouseEvent e) {
                show(e);
            }

            public void mouseReleased(MouseEvent e) {
                show(e);
            }

            private void show(MouseEvent e) {
                if (e.isPopupTrigger()) {
                    TreePath path = structureTree.getSelectionPath();
                    if (path != null) {
                        DefaultMutableTreeNode dmtn = (DefaultMutableTreeNode) path.getLastPathComponent();
                        if (dmtn.getUserObject() instanceof Band) {
                            popup.show((JComponent) e.getSource(), e.getX(), e.getY());
                        }
                    }
                }
            }
        });
    }

    @SuppressWarnings("unchecked")
    private DefaultMutableTreeNode getBandTreeNode(String bandName) {
        Enumeration en = rootNode.children();
        while (en.hasMoreElements()) {
            DefaultMutableTreeNode node = (DefaultMutableTreeNode) en.nextElement();
            Object userObject = node.getUserObject();
            if (userObject instanceof Band) {
                Band band = (Band) userObject;
                if (bandName.equals(band.getName())) {
                    return node;
                }
            }
        }

        return null;
    }

    @SuppressWarnings("unchecked")
    private DefaultMutableTreeNode getBandRowTreeNode(String bandName, int row) {
        DefaultMutableTreeNode bandNode = getBandTreeNode(bandName);
        Enumeration rows = bandNode.children();
        while (rows.hasMoreElements()) {
            DefaultMutableTreeNode node = (DefaultMutableTreeNode) rows.nextElement();
            Object userObject = node.getUserObject();
            if ((Integer) userObject == row) {
                return node;
            }
        }

        StructureTreeNode rowNode = new StructureTreeNode(row);
        bandNode.insert(rowNode, row);

        return rowNode;
    }

    private StructureTreeNode getBandElementTreeNode(String bandName, int row, int column) {
        DefaultMutableTreeNode bandNode = getBandRowTreeNode(bandName, row);
        StructureTreeNode child;
        if (bandNode.getChildCount() <= column) {
            child = new StructureTreeNode(new ReportGridCell(null, row, column));
            child.setVisible(false);
            bandNode.insert(child, column);
        } else {
            child = (StructureTreeNode) bandNode.getChildAt(column);
        }

        return child;
    }

    @SuppressWarnings("unchecked")
    private DefaultMutableTreeNode getBandRowInsertedTreeNode(String bandName, int bandRow, int row) {

        DefaultMutableTreeNode bandNode = getBandTreeNode(bandName);

        // tree must be traversed in preorder (band by band)        
        Enumeration en = rootNode.preorderEnumeration();
        String currentBandName = ReportLayout.HEADER_BAND_NAME;
        while (en.hasMoreElements()) {
            DefaultMutableTreeNode node = (DefaultMutableTreeNode) en.nextElement();
            Object userObject = node.getUserObject();
            if (userObject instanceof Band) {
                currentBandName = ((Band) userObject).getName();
            } else if (userObject instanceof ReportGridCell) {
                // increment rows for all report grid cells which are in the rows
                // following the inserted row
                ReportGridCell reportGridCell = (ReportGridCell) userObject;
                if (row <= reportGridCell.getRow()) {
                    reportGridCell.setRow(reportGridCell.getRow() + 1);
                }
            } else if (userObject instanceof Integer) {
                // modify 'row' nodes in the tree (for the current band)
                if (bandName.equals(currentBandName)) {
                    if ((Integer) userObject == bandRow) {
                        DefaultMutableTreeNode sibling = node.getNextSibling();
                        node.setUserObject(((Integer) node.getUserObject()).intValue() + 1);
                        while (sibling != null) {
                            sibling.setUserObject(((Integer) sibling.getUserObject()).intValue() + 1);
                            sibling = sibling.getNextSibling();
                        }
                    }
                }
            }
        }

        DefaultMutableTreeNode rowNode = new StructureTreeNode(bandRow);
        bandNode.insert(rowNode, bandRow);

        return rowNode;
    }

    private void deleteBandRowTreeNode(String bandName, int bandRow, int row) {

        // tree must be traversed in preorder (band by band)
        Enumeration en = rootNode.preorderEnumeration();
        String currentBandName = ReportLayout.HEADER_BAND_NAME;
        DefaultMutableTreeNode nodeToDelete = null;
        while (en.hasMoreElements()) {
            DefaultMutableTreeNode node = (DefaultMutableTreeNode) en.nextElement();
            Object userObject = node.getUserObject();
            if (userObject instanceof Band) {
                currentBandName = ((Band) userObject).getName();
            } else if (userObject instanceof ReportGridCell) {
                // decrement rows for all report grid cells which are in the rows
                // following the inserted row
                ReportGridCell reportGridCell = (ReportGridCell) userObject;
                if (row <= reportGridCell.getRow()) {
                    reportGridCell.setRow(reportGridCell.getRow() - 1);
                }
            } else if (userObject instanceof Integer) {
                // modify 'row' nodes in the tree (for the current band)
                if (bandName.equals(currentBandName)) {
                    if ((Integer) userObject == bandRow) {
                        if (nodeToDelete == null) {
                            nodeToDelete = node;
                            DefaultMutableTreeNode sibling = node.getNextSibling();
                            while (sibling != null) {
                                sibling.setUserObject(((Integer) sibling.getUserObject()).intValue() - 1);
                                sibling = sibling.getNextSibling();
                            }
                        }

                    }
                }
            }
        }
        if (nodeToDelete != null) {
            structureTreeModel.removeNodeFromParent(nodeToDelete);
        }
    }

    private void insertColumnNodes(int column) {
        Enumeration en = rootNode.preorderEnumeration();        
        List<DefaultMutableTreeNode> nodes = new ArrayList<DefaultMutableTreeNode>();
        List<DefaultMutableTreeNode> nodesToAdd = new ArrayList<DefaultMutableTreeNode>();
        int columnCount = Globals.getReportGrid().getColumnCount();
        while (en.hasMoreElements()) {
            DefaultMutableTreeNode node = (DefaultMutableTreeNode) en.nextElement();
            Object userObject = node.getUserObject();
            if (userObject instanceof ReportGridCell) {
                ReportGridCell reportGridCell = (ReportGridCell) userObject;
                if ((column == reportGridCell.getColumn()) ||
                        ((column == reportGridCell.getColumn() + 1) && (column + 1 == columnCount))) {
                    StructureTreeNode colNode = new StructureTreeNode(
                            new ReportGridCell(null, reportGridCell.getRow(), column));
                    colNode.setVisible(false);
                    nodes.add((DefaultMutableTreeNode) node.getParent());
                    nodesToAdd.add(colNode);
                }
                // increment column for all report grid cells which are in the columns
                // following the inserted column
                if (column <= reportGridCell.getColumn()) {
                    reportGridCell.setColumn(reportGridCell.getColumn() + 1);
                }
            }
        }
        for (int i = 0, size = nodes.size(); i < size; i++) {
            nodes.get(i).insert(nodesToAdd.get(i), column);
        }
    }

    private void deleteColumnNodes(int column) {
        Enumeration en = rootNode.preorderEnumeration();
        List<DefaultMutableTreeNode> nodesToDelete = new ArrayList<DefaultMutableTreeNode>();
        while (en.hasMoreElements()) {
            DefaultMutableTreeNode node = (DefaultMutableTreeNode) en.nextElement();
            Object userObject = node.getUserObject();
            if (userObject instanceof ReportGridCell) {
                ReportGridCell reportGridCell = (ReportGridCell) userObject;
                if (column == reportGridCell.getColumn()) {
                    nodesToDelete.add(node);
                }
                // decrement column for all report grid cells which are in the columns
                // following the deleted column
                if (column <= reportGridCell.getColumn()) {
                    reportGridCell.setColumn(reportGridCell.getColumn() - 1);
                }
            }
        }
        for (DefaultMutableTreeNode node : nodesToDelete) {
            structureTreeModel.removeNodeFromParent(node);
        }
    }


    public void actionPerformed(ActionEvent e) {
        DefaultMutableTreeNode node;
        TreePath path = structureTree.getSelectionPath();
        node = (DefaultMutableTreeNode) path.getLastPathComponent();
        if (e.getActionCommand().equals("insert")) {

            final NumberSelectionPanel panel = new NumberSelectionPanel(I18NSupport.getString("insert.row.number"));
            final BaseDialog dialog = new BaseDialog(panel, I18NSupport.getString("insert.row.after.action.name"), true) {
                public boolean okPressed() {
                    if ((panel.getNumber() < 1) || (panel.getNumber() > BandUtil.MAX)) {
                        Show.info(this, I18NSupport.getString("rowCol.max", BandUtil.MAX));
                        return false;
                    }
                    return true;
                }
            };
            dialog.pack();
            dialog.setLocationRelativeTo(Globals.getMainFrame());
            dialog.setVisible(true);
            if (!dialog.okPressed()) {
                return;
            }

            ReportLayout oldLayout = ObjectCloner.silenceDeepCopy(LayoutHelper.getReportLayout());

            int rows = panel.getNumber();

            String bandName = ((Band) node.getUserObject()).getName();
            Band band = LayoutHelper.getReportLayout().getBand(bandName);            
            int row = Globals.getReportGrid().getBandLocation(bandName).getLastGridRow();
            int cols = Globals.getReportGrid().getColumnCount();
            if (cols == 0) {
            	// empty report : we will add one column
            	cols = 1;
            }
            for (int i = 0; i < rows; i++) {
                Globals.getReportLayoutPanel().getReportGridPanel().insertRow(band);                
                for (int j=0; j<cols; j++) {
                	BandUtil.insertElement(new BandElement(""), row+i, j);
                }
            }
            Globals.getReportGrid().getSelectionModel().clearSelection();

            ReportLayout newLayout = ObjectCloner.silenceDeepCopy(LayoutHelper.getReportLayout());
            Globals.getReportUndoManager().addEdit(new LayoutEdit(oldLayout, newLayout, I18NSupport.getString("edit.row.insert.before")));
        }
    }

}