/*
 * Licensed Materials - Property of Perforce Software, Inc. 
 * © Copyright Perforce Software, Inc. 2014, 2021 
 * © Copyright IBM Corp. 2009, 2014
 * © Copyright ILOG 1996, 2009
 * All Rights Reserved.
 *
 * Note to U.S. Government Users Restricted Rights:
 * The Software and Documentation were developed at private expense and
 * are "Commercial Items" as that term is defined at 48 CFR 2.101,
 * consisting of "Commercial Computer Software" and
 * "Commercial Computer Software Documentation", as such terms are
 * used in 48 CFR 12.212 or 48 CFR 227.7202-1 through 227.7202-4,
 * as applicable.
 */
package treemap;

import java.awt.Font;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.Serializable;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.faces.context.FacesContext;
import javax.faces.event.ValueChangeEvent;
import javax.faces.model.DataModel;
import javax.faces.model.SelectItem;
import javax.swing.table.TableModel;
import javax.swing.tree.TreePath;

import ilog.views.chart.IlvChart;
import ilog.views.chart.IlvChartLayout;
import ilog.views.chart.data.IlvTreeTableDataSource;
import ilog.views.chart.datax.IlvColumnUtilities;
import ilog.views.chart.datax.IlvDataColumnInfo;
import ilog.views.chart.datax.IlvDefaultDataColumnInfo;
import ilog.views.chart.datax.adapter.IlvClusterNode;
import ilog.views.chart.datax.adapter.partition.IlvClusterId;
import ilog.views.chart.datax.adapter.partition.IlvColumnValueClusterId;
import ilog.views.chart.datax.adapter.partition.IlvPartitionerFactory;
import ilog.views.chart.datax.adapter.partition.IlvStringPartitionerFactory;
import ilog.views.chart.datax.adapter.partition.IlvUniformScalePartitionerFactory;
import ilog.views.chart.datax.flat.table.IlvDefaultFlatTableModel;
import ilog.views.chart.datax.flat.table.IlvFlatTableModel;
import ilog.views.chart.datax.tree.list.IlvTreeListModel;
import ilog.views.chart.graphic.IlvDataLabelAnnotation;
import ilog.views.chart.renderer.IlvColorScheme;
import ilog.views.chart.renderer.IlvTreemapChartRenderer;
import ilog.views.util.IlvResourceUtil;
import ilog.views.util.data.IlvCSVReader;
import treemap.table.FilteredFlatTableModel;

/**
 * The application bean
 */
public class TreemapBean implements Serializable {

  // The datasource
  private IlvTreeTableDataSource dataSource;

  // The file containing the data.
  private final String dataFile = "/data/energy.csv";

  // Information about the columns in the Excel data.
  private final String[] columnNames = { "Country", "Region", "Population", "GDP", "GDP per Capita", "Ranking", "Year",
      "Energy Type", "Production", "Consumption" };

  // Information about the columns type in the Excel data.
  private final Class<?>[] columnTypes = { String.class, String.class, Double.class, Double.class, Double.class,
      Integer.class, String.class, String.class, Double.class, Double.class };

  // The current color model
  private String colorColumn = columnNames[5];

  // The current area
  private String areaColumn = columnNames[4];

  // The color chooser model
  private List<SelectItem> colorColumnModel = null;

  // The area chooser model
  private List<SelectItem> areaColumnModel = null;

  // The current partition
  private String partitionLabel;

  // The partition names.
  private String partitionNames[] = { "Country", "Region", "Population", "GDP", "GDP per Capita", "Ranking", "Year",
      "Energy Type", "Production", "Consumption" };

  // The current partition separator
  private String partitionValue = partitionNames[1];

  private final static String NONE = "<none>";

  // The first partitionning criteria
  private String partitionCriterion1 = partitionNames[0];

  // The second partitionning criteria
  private String partitionCriterion2 = NONE;

  // The treemap chart
  private TreemapWithHistory treemap;

  // The treepath finder
  private TreemapPathFinder treepathFinder = new TreemapPathFinder();

  // The table model
  private FilteredFlatTableModel tableModel;

  // The current selection path
  private String selectionPath;

  /**
   * Creates a new <code>TreemapBean</code> instance.
   */
  public TreemapBean() {
    initDataSource();
    initTableModel();
    initTreemap("Country");
  }

  /**
   * Loads the data into memory and makes it accessible through a data source.
   */
  private void initDataSource() {
    TableModel table;
    try {
      InputStream input = FacesContext.getCurrentInstance().getExternalContext().getResourceAsStream(dataFile);
      Reader reader = new InputStreamReader(new BufferedInputStream(input), "UTF-8");
      try {
        IlvCSVReader parser = IlvCSVReader.getInstance('\t', 0);
        table = parser.read(reader);
      } finally {
        reader.close();
      }
    } catch (IOException e) {
      e.printStackTrace();
      return;
    }

    // Copy the data into an IlvFlatTableModel, with appropriate
    // conversion of values.
    int numColumns = table.getColumnCount();
    if (numColumns != columnNames.length) {
      String msg = IlvResourceUtil.getCurrentLocaleString(TreemapBean.class, "columnNamesNotUpToDate");
      throw new RuntimeException(msg);
    }
    IlvDataColumnInfo[] columns = new IlvDataColumnInfo[numColumns];
    for (int col = 0; col < numColumns; col++) {
      columns[col] = new IlvDefaultDataColumnInfo(columnNames[col], columnTypes[col]);
    }
    int numRows = table.getRowCount();
    IlvDefaultFlatTableModel tableData = new IlvDefaultFlatTableModel(numRows, columns);
    for (int col = 0; col < numColumns; col++) {
      if (columnTypes[col] == Double.class) {
        for (int row = 0; row < numRows; row++) {
          double value = Double.parseDouble(table.getValueAt(row, col).toString());
          tableData.setDoubleAt(value, row, col);
        }
      } else if (columnTypes[col] == Integer.class) {
        for (int row = 0; row < numRows; row++) {
          int value = (int) Double.parseDouble(table.getValueAt(row, col).toString());
          tableData.setValueAt(Integer.valueOf(value), row, col);
        }
      } else if (columnTypes[col] == String.class) {
        for (int row = 0; row < numRows; row++) {
          String value = table.getValueAt(row, col).toString();
          tableData.setValueAt(value, row, col);
        }
      } else {
        for (int row = 0; row < numRows; row++) {
          Object value = table.getValueAt(row, col);
          tableData.setValueAt(value, row, col);
        }
      }
    }

    // Set the types of some columns to enumerated. This is a hint to the
    // QUALITATIVE color scheme how to distribute the colors.
    ((IlvDefaultDataColumnInfo) IlvColumnUtilities.getColumnByName(tableData, "Country")).setEnumerated(true);
    ((IlvDefaultDataColumnInfo) IlvColumnUtilities.getColumnByName(tableData, "Region")).setEnumerated(true);

    // Create a data source taking the worksheet as input.
    dataSource = new IlvTreeTableDataSource(tableData);
  }

  /**
   * Initializes the table model
   */
  private void initTableModel() {
    IlvFlatTableModel model = dataSource.getFlatTableModel();
    tableModel = new FilteredFlatTableModel(model);
  }

  /**
   * Initializes the treemap display.
   */
  private void initTreemap(String nameColumn) {

    // Start customizing it.
    treemap = new TreemapWithHistory(dataSource);

    treemap.addSelectionListener(new TreemapWithHistory.SelectionListener() {
      Override
      public void selectionChanged(TreePath treepath) {
        Map<String, String> criteria = new HashMap<String, String>();
        if (treepath != null) {
          int nb = treepath.getPathCount() - 1; // -1 = root

          if (nb == 0)
            selectionPath = "";

          Object object = treepath.getLastPathComponent();

          if (object instanceof IlvClusterNode) {
            StringBuffer tmpSelectionPath = new StringBuffer();
            Object[] path = treepath.getPath();
            for (int i = 1; i <= nb; i++) {
              IlvClusterNode node = (IlvClusterNode) path[i];
              IlvColumnValueClusterId clusterId = (IlvColumnValueClusterId) node.getId();
              String columnName = clusterId.getColumn().getName();
              criteria.put(columnName, path[i].toString());

              tmpSelectionPath.append(path[i].toString());
              if (i != nb)
                tmpSelectionPath.append("/");
            }
            selectionPath = tmpSelectionPath.toString();
            tableModel.setCriteria(criteria);

          } else {
            // A leaf = a single record.
            int row = dataSource.getFlatModel().getObjects().indexOf(object);
            Object companyName = tableModel.getValueAt(row, "Country");
            selectionPath = companyName.toString();
            tableModel.setRowVisible(row);
          }
        } else {
          tableModel.setCriteria(new HashMap<String, String>());
          selectionPath = null;
        }
      }
    });

    IlvChart chart = treemap.getChart();

    IlvTreemapChartRenderer renderer = (IlvTreemapChartRenderer) chart.getRenderer(0);

    // Set the packing algorithm.
    renderer.setPacking(IlvTreemapChartRenderer.SQUARIFIED_CORNER_PACKING);

    // Add labels.
    // The label visibility is set in class TreemapDemo,
    // method updateLabeling().
    renderer.setLabelColumnName(nameColumn);
    renderer.setAnnotation(new IlvDataLabelAnnotation());

    // Add tooltips.
    renderer.setLabelColumnName(nameColumn);

    // init colors
    updateColorColumn();
    updateAreaColumn();
    updateLabeling();
    updatePartitioners();
  }

  /**
   * Returns <code>true</code> if the path specified is valid against the
   * specified tree list model, <code>false</code> otherwise.
   * 
   * @param path
   *          The tree path to test.
   * @param tmodel
   *          The tree list model.
   * @return <code>true</code> if the path is valid, <code>false</code>
   *         otherwise.
   */
  public boolean isValidPath(TreePath path, IlvTreeListModel tmodel) {
    TreePath tp = path;
    Object[] chain = tp.getPath();
    if (chain[0] != tmodel.getRoot())
      return false;
    for (int i = 1; i < chain.length; i++)
      if (!tmodel.getChildren(chain[i - 1]).contains(chain[i]))
        return false;
    return true;
  }

  /**
   * Updates the treemap when a particular color column has been chosen.
   */
  private void updateColorColumn() {
    String columnName = colorColumn;

    IlvChart chart = treemap.getChart();

    IlvTreemapChartRenderer renderer = (IlvTreemapChartRenderer) chart.getRenderer(0);
    // Reset the LegendLabelFormat, since the previous LegendLabelFormat
    // may be a DecimalFormat and therefore not suitable for non-numeric
    // columns.
    renderer.setLegendLabelFormat(null);
    // Set the color column and color scheme.
    // Also make a legend appear when suitable.
    IlvDataColumnInfo column;
    if (columnName != null
        && (column = IlvColumnUtilities.getColumnByName(dataSource.getTreeModel(), columnName)) != null) {
      renderer.setColorColumnName(columnName);
      if (column.getType() == String.class)
        // For non-numeric columns, use "qualitative" colors.
        renderer.setColorScheme(IlvTreemapChartRenderer.COLORSCHEME_QUALITATIVE);
      else if (column.getName().equals("Profits"))
        // For numeric columns with positive and negative values, use
        // red for negative and green for positive values. Take care
        // that frontier between red and green is exactly at 0.
        renderer.setColorScheme(
            IlvColorScheme.createAdjustedColorScheme(IlvTreemapChartRenderer.COLORSCHEME_DIVERGING_RED_GREEN, 0.0));
      else
        // For other numeric columns, use red for low and green for
        // high values.
        renderer.setColorScheme(
            IlvColorScheme.createAdjustedColorScheme(IlvTreemapChartRenderer.COLORSCHEME_DIVERGING_RED_GREEN));
      if (Number.class.isAssignableFrom(column.getType())) {
        // Ensure there is a legend.
        renderer.setLegendLabelFormat(new DecimalFormat("#.##"));
        chart.setLegendPosition(IlvChartLayout.EAST);
        chart.setLegendVisible(true);
      } else {
        // Ensure there is no legend.
        chart.setLegendVisible(true);
      }
    } else {
      renderer.setColorColumnName(null);
      renderer.setColorScheme(IlvTreemapChartRenderer.COLORSCHEME_DEPTH);
      // Ensure there is no legend.
      chart.setLegendVisible(true);
    }
  }

  /**
   * Updates the treemap when a particular area column has been chosen.
   */
  private void updateAreaColumn() {
    String columnName = areaColumn;
    IlvChart chart = treemap.getChart();
    IlvTreemapChartRenderer renderer = (IlvTreemapChartRenderer) chart.getRenderer(0);
    if (columnName != null && IlvColumnUtilities.getColumnByName(dataSource.getTreeModel(), columnName) != null)
      renderer.setAreaColumnName(columnName);
    else
      renderer.setAreaColumnName(null);
  }

  /**
   * Updates the treemap when a particular labeling mode has been chosen.
   */
  private void updateLabeling() {
    IlvChart chart = treemap.getChart();
    IlvTreemapChartRenderer renderer = (IlvTreemapChartRenderer) chart.getRenderer(0);
    renderer.setAnnotationVisibility(IlvTreemapChartRenderer.VISIBILITY_SMART);
    renderer.setClippedAnnotationVisibility(false);
    IlvDataLabelAnnotation annotation = new IlvDataLabelAnnotation();
    annotation.getLabelRenderer().setFont(new Font("SansSerif", Font.PLAIN, 12));
    renderer.setAnnotation(annotation);
  }

  /**
   * Updates the data source and with it also the treemap and tree-table when a
   * particular partitioning has been chosen.
   */
  private void updatePartitioners() {
    IlvFlatTableModel model = dataSource.getFlatTableModel();
    boolean criteria1Set = !NONE.equals(partitionCriterion1);
    boolean criteria2Set = !NONE.equals(partitionCriterion2);

    int count = criteria1Set ? 1 : 0;
    count += criteria2Set ? 1 : 0;

    SuppressWarnings("unchecked")
    IlvPartitionerFactory<? extends IlvClusterId>[] newPartitionerFactories = new IlvPartitionerFactory[count];

    if (criteria1Set)
      newPartitionerFactories[0] = createPartitioner(model, partitionCriterion1);

    if (criteria2Set)
      newPartitionerFactories[count - 1] = createPartitioner(model, partitionCriterion2);

    if (!Arrays.equals(dataSource.getPartitionerFactories(), newPartitionerFactories)) {
      dataSource.setPartitionerFactories(newPartitionerFactories);
      tableModel.setCriteria(new HashMap<String, String>());
      TreePath selection = treemap.getSelection();
      if (selection != null && !isValidPath(selection, dataSource.getTreeModel()))
        treemap.setSelection(null);
      treemap.clearHistory();
      StringBuffer headerLabel = new StringBuffer();
      if (count == 0) {
        String msg = IlvResourceUtil.getCurrentLocaleString(TreemapBean.class, "noPartitioning");
        headerLabel.append(msg);
      } else {

        if (criteria1Set) {
          headerLabel.append(partitionCriterion1);
          if (criteria2Set) {
            headerLabel.append("/");
            headerLabel.append(partitionCriterion2);
          }
        } else if (criteria2Set)
          headerLabel.append(partitionCriterion2);
      }
      partitionLabel = headerLabel.toString();
    }
  }

  private IlvPartitionerFactory<? extends IlvClusterId> createPartitioner(IlvFlatTableModel model, String columnName) {
    IlvDataColumnInfo column = IlvColumnUtilities.getColumnByName(model, columnName);
    // Set the corresponding partitioner.
    IlvPartitionerFactory<? extends IlvClusterId> partitioner = (Number.class.isAssignableFrom(column.getType())
        ? new IlvUniformScalePartitionerFactory(column, 4, 5)
        : new IlvStringPartitionerFactory(column));
    return partitioner;
  }

  /**
   * JSF action to change the color model.
   * 
   * @param event
   *          The value change event.
   */
  public void colorColumnChanged(ValueChangeEvent event) {
    colorColumn = event.getNewValue().toString();
    updateColorColumn();
  }

  /**
   * JSF action to change the area.
   * 
   * @param event
   *          The value change event.
   */
  public void areaColumnChanged(ValueChangeEvent event) {
    areaColumn = event.getNewValue().toString();
    updateAreaColumn();
  }

  /**
   * JSF action to change the partitionners.
   * 
   * @param event
   *          The value change event.
   */
  public void partitionCriterion1Changed(ValueChangeEvent event) {
    partitionCriterion1 = event.getNewValue().toString();
    updatePartitioners();
  }

  /**
   * JSF action to change the partitionners.
   * 
   * @param event
   *          The value change event.
   */
  public void partitionCriterion2Changed(ValueChangeEvent event) {
    partitionCriterion2 = event.getNewValue().toString();
    updatePartitioners();
  }

  /**
   * JSF action to change the partitionners.
   * 
   * @param event
   *          The value change event.
   */
  public void partitionColumnChanged(ValueChangeEvent event) {
    partitionValue = event.getNewValue().toString();
    updatePartitioners();
  }

  /**
   * Returns the color select one chooser model.
   * 
   * @return The color chooser model.
   */
  public List<SelectItem> getColorColumnModel() {
    if (colorColumnModel == null) {
      colorColumnModel = new ArrayList<SelectItem>();
      String msg = IlvResourceUtil.getCurrentLocaleString(TreemapBean.class, "allSameColorLabel");
      colorColumnModel.add(new SelectItem("(all same color)", msg));
      for (int col = 0; col < columnNames.length; col++)
        if (!columnNames[col].equals("Country"))
          colorColumnModel.add(new SelectItem(columnNames[col]));
    }
    return colorColumnModel;

  }

  /**
   * Returns the area select one chooser model.
   * 
   * @return The area chooser model.
   */
  public List<SelectItem> getAreaColumnModel() {
    if (areaColumnModel == null) {
      areaColumnModel = new ArrayList<SelectItem>();
      String msg = IlvResourceUtil.getCurrentLocaleString(TreemapBean.class, "allEqualAreaLabel");
      areaColumnModel.add(new SelectItem("(all equal area)", msg));
      for (int col = 0; col < columnNames.length; col++)
        if (columnTypes[col] == Double.class)
          areaColumnModel.add(new SelectItem(columnNames[col]));
    }
    return areaColumnModel;
  }

  private List<SelectItem> partitionModel;

  /**
   * Returns the area select one chooser model.
   * 
   * @return The area chooser model.
   */
  public List<SelectItem> getPartitionColumnModel() {
    if (partitionModel == null) {
      partitionModel = new ArrayList<SelectItem>();
      partitionModel.add(new SelectItem(NONE));
      for (int col = 0; col < partitionNames.length; col++)
        partitionModel.add(new SelectItem(partitionNames[col]));
    }
    return partitionModel;
  }

  /**
   * Returns the current area.
   * 
   * @return The current area.
   */
  public String getAreaColumn() {
    return areaColumn;
  }

  /**
   * Sets the current area
   * 
   * @param areaColumn
   *          The area to set.
   */
  public void setAreaColumn(String areaColumn) {
    this.areaColumn = areaColumn;
  }

  /**
   * Returns the current color scheme.
   * 
   * @return The color scheme.
   */
  public String getColorColumn() {
    return colorColumn;
  }

  /**
   * Sets the current color scheme.
   * 
   * @param colorColumn
   *          The color scheme to set.
   */
  public void setColorColumn(String colorColumn) {
    this.colorColumn = colorColumn;
  }

  /**
   * Returns the current partition.
   * 
   * @return The current partition.
   */
  public String getPartitionLabel() {
    return partitionLabel;
  }

  /**
   * Sets the current partition value separator
   * 
   * @return The partition separator.
   */
  public String getPartitionValue() {
    return partitionValue;
  }

  /**
   * Sets the partition separator
   * 
   * @param partitionValue
   *          The paration separator to set.
   */
  public void setPartitionValue(String partitionValue) {
    this.partitionValue = partitionValue;
  }

  /**
   * Returns the first criterion used to partition the treemap.
   * 
   * @return The first partition criterion.
   */
  public String getPartitionCriterion1() {
    return partitionCriterion1;
  }

  /**
   * Sets the first criterion used to partition the treemap.
   * 
   * @param partitionCriteria1
   *          The criterion to set.
   */
  public void setPartitionCriterion1(String partitionCriteria1) {
    this.partitionCriterion1 = partitionCriteria1;
  }

  /**
   * Returns the second criterion used to partition the treemap.
   * 
   * @return The second partition criterion.
   */
  public String getPartitionCriterion2() {
    return partitionCriterion2;
  }

  /**
   * Sets the second criterion used to partition the treemap.
   * 
   * @param partitionCriteria2
   *          The criterion to set.
   */
  public void setPartitionCriterion2(String partitionCriteria2) {
    this.partitionCriterion2 = partitionCriteria2;
  }

  /**
   * Returns the treemap object (the graphic component + the selection and
   * history of selection)
   * 
   * @return The treemap.
   */
  public TreemapWithHistory getTreemap() {
    return treemap;
  }

  /**
   * Returns the object finder that returns the tree path of a picked element of
   * the treemap chart.
   * 
   * @return The tree path finder.
   */
  public TreemapPathFinder getTreepathFinder() {
    return treepathFinder;
  }

  /**
   * Returns the table model.
   * 
   * @return The table model
   */
  public DataModel getTableModel() {
    return tableModel;
  }

  /**
   * Returns <code>true</code> if the table should be rendered
   * 
   * @return <code>true</code> if the table should be rendered,
   *         <code>false</code> otherwise.
   */
  public boolean getTableRendered() {
    return getTableModel().getRowCount() > 0;
  }

  /**
   * Returns the table column names.
   * 
   * @return The table column names.
   */
  public List<String> getColumnNames() {
    return Arrays.asList(columnNames);
  }

  /**
   * 
   * @return Return the column CSSs classes.
   */
  public String getTableColumnCssClasses() {
    String res = "";
    // hack: in the page the 2 first column are inverted
    res += "column-left,column-center,";
    for (int i = 2; i < columnTypes.length; i++) {
      Class<?> clazz = columnTypes[i];
      if (String.class.equals(clazz))
        res += "column-left,";
      else
        res += "column-right,";
    }
    res = res.substring(0, res.length() - 1);
    return res;
  }

  /**
   * Returns the current path selected.
   * 
   * @return The selection path.
   */
  public String getSelectionPath() {
    return selectionPath;
  }

  /**
   * Return <code>true</code> if the selection path should be displayed,
   * <code>false</code> otherwise.
   * 
   * @return The rendered state of the selection path display.
   */
  public boolean isSelectionPathRendered() {
    return selectionPath != null && !"".equals(selectionPath);
  }

}