/*
 * 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.
 */
import ilog.views.gantt.IlvDuration;
import ilog.views.gantt.IlvGanttChart;
import ilog.views.gantt.IlvTimeUtil;
import ilog.views.gantt.action.IlvZoomToFitAction;
import ilog.views.gantt.property.IlvActivityUserDefinedProperty;
import ilog.views.gantt.swing.IlvConfigurableTableColumn;
import ilog.views.gantt.swing.IlvJTable;
import ilog.views.gantt.text.IlvDurationFormat;
import ilog.views.util.IlvProductUtil;
import ilog.views.util.IlvResourceUtil;
import ilog.views.util.event.IlvEventListenerCollection;
import ilog.views.util.event.IlvEventListenerList;
import ilog.views.util.styling.IlvStylingException;
import ilog.views.util.text.IlvDateFormatFactory;

import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.text.DateFormat;
import java.util.*;
import javax.swing.*;
import javax.swing.event.CellEditorListener;
import javax.swing.event.ChangeEvent;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.TableCellEditor;


/**
 * Dynamic Column sample.
 * This sample code shows how to create table columns that display a
 * particular property of an <code>IlvGeneralActivity</code> without
 * requiring to code a custom subclass of <code>IlvStringColumn</code>.
 */
public class DynamicColumnSample extends JPanel {

  public IlvGanttChart chart;

  // The project configuration file that is used to load the general data model
  static final String projectURL = "data/data.igpr";

  // The zoom-to-fit action.
  private IlvZoomToFitAction zoomToFitAction;

  // The resource bundle
  private ResourceBundle bundle;

  public DynamicColumnSample(JFrame frame) {
    // Load the resource bundles containing the locale dependent table
    // header labels for the gantt chart.
    bundle = IlvResourceUtil.getBundle("dynamicColumn.gantt",
                                       getLocale(),
                                       getClass().getClassLoader());
    // Prepare the JPanel's appearance
    setLayout(new BorderLayout());
    JPanel panel = new JPanel();

    chart = new IlvGanttChart();
    try {
      // Load the sample project
      chart.setProject(getResourceURL(projectURL));
    } catch (IOException e1) {
      JOptionPane.showMessageDialog(frame,
                                    e1,
                                    "Error",
                                    JOptionPane.ERROR_MESSAGE);
    } catch (IlvStylingException e1) {
      JOptionPane.showMessageDialog(frame,
                                    e1,
                                    "Error",
                                    JOptionPane.ERROR_MESSAGE);
    }

    // A button to load the user-defined property columns.
    Button buttonGantt =
        new Button(bundle.getString("DynamicColumn.AddPropertyButton.Label"));
    buttonGantt.addActionListener(new ActionListener()
    {
      Override
      public void actionPerformed(ActionEvent e) {
        // Let's define the user-defined properties, and their types.
        // Note that this could be done by analysing the model.

        // The array of property names from the model that can be added.
        String[] propertyNames = {
          "department",
          "critical",
          "latestStart",
          "completion",
          "priority",
          "totalSlack"
        };

        //The array of property classes.
        Class<?>[] propertyClasses = {
          String.class,
          Boolean.class,
          Date.class,
          Float.class,
          Integer.class,
          IlvDuration.class
        };

        // Create and add a column to the table for each property.
        for (int i = 0; i < propertyNames.length; i++) {
          String property = propertyNames[i];
          Class<?> propertyClass = propertyClasses[i];

          IlvJTable table = chart.getTable();

          // Add the column to the chart, if not already there.
          try {
            if (table.getColumn(property) != null) {
              // The column exists
              return;
            }
          } catch (IllegalArgumentException ex) {
            // the column does not exist.
          }

          // Create and add customized column, by doing the following actions:
          // 1. Create the user-defined property adapter that can be accessed
          //    by the generic IlvStringProperty interface.
          // 2. Customize formatting for this property adapter based on the
          //    property class.
          // 3. Create the configurable table column for this property adapter.
          //    The table column can be customized with a table cell renderer,
          //    a cell editor, and a width size.
          // 4. Add the table column to the table.

          // Get the string for the header value
          String headerValue = bundle.getString("DynamicColumn.HeaderColumn."
                                                + property);
          // Create the user-defined property adapter that can be accessed via
          // the generic IlvStringProperty interface.
          IlvActivityUserDefinedProperty userDefinedProperty =
              new IlvActivityUserDefinedProperty(property, propertyClass);

          // Customize formatting based on the property class
          if (propertyClass.isAssignableFrom(Date.class)) {
            DateFormat dateFormat =
                IlvDateFormatFactory.getDateInstance(DateFormat.DEFAULT,
                                                     chart.getULocale());
            userDefinedProperty.setFormat(dateFormat);
          } else if (propertyClass.isAssignableFrom(IlvDuration.class)) {
            IlvDurationFormat durationFormat =
                new IlvDurationFormat(IlvDurationFormat.TIME_UNIT_MEDIUM);
            durationFormat.setLenientParseMode(true);
            userDefinedProperty.setFormat(durationFormat);
          }

          // Create the configurable table column.
          IlvConfigurableTableColumn propertyColumn;
          // For the "priority" property use a slider as cell editor, and
          // render the value with different colors.
          if ("priority".equals(property)) {
            propertyColumn =
                new IlvConfigurableTableColumn(headerValue,
                                               userDefinedProperty,
                                               property,
                                               new SliderEditor(0, 10, 0),
                                               new PriorityRenderer());
          } else if ("totalSlack".equals(property)) {
            // For the "totalSlack" property, use a larger column.
            propertyColumn =
                new IlvConfigurableTableColumn(headerValue,
                                               userDefinedProperty,
                                               150,
                                               property);
          } else {
            propertyColumn =
                new IlvConfigurableTableColumn(headerValue,
                                               userDefinedProperty,
                                               property);
          }
          // Add the column to the table.
          table.addColumn(propertyColumn);
        }
      }
    });
    panel.add(buttonGantt);

    add(BorderLayout.CENTER, chart);
    chart.revalidate();
    zoomToFitAction = new IlvZoomToFitAction(chart, "", null, null, "", "");
    zoomToFit();
    // Make sure the full chart is visible.
    chart.expandAllRows();
    // Set the initially displayed time span to start 1 day before the beginning
    // of the chart and extend for 3 weeks.
    Date visibleStartTime =
          IlvTimeUtil.subtract(chart.getGanttModel().getRootActivity().getStartTime(),
                               IlvDuration.ONE_DAY);
    chart.setVisibleTime(visibleStartTime);
    chart.setVisibleDuration(IlvDuration.ONE_WEEK.multiply(3));
    add(BorderLayout.SOUTH, panel);
  }

  public static void main(String[] args) {
    // This sample uses JViews Gantt features. When deploying an
    // application that includes this code, you need to be in possession
    // of a Perforce JViews Gantt Deployment license.
    IlvProductUtil.DeploymentLicenseRequired(
        IlvProductUtil.JViews_Gantt_Deployment);

    SwingUtilities.invokeLater(new Runnable() {
      Override
      public void run() {
        JFrame frame = new JFrame();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setBounds(10, 400, 700, 400);
        DynamicColumnSample example = new DynamicColumnSample(frame);
        frame.getContentPane().add(example);
        frame.setVisible(true);
      }
    });
  }

  // =========================================
  // Accessing Resources
  // =========================================

  /**
   * Returns the fully qualified URL of a resource file that is specified
   * relative to the working directory of the example. Note that this is
   * different than calling <code>Class.getResource()</code>, which performs
   * resource lookup relative to the example's classpath.
   *
   * @param relativePath The path of the resource file, relative to the working
   *                     directory of the example.
   * @return The resource URL.
   *
   * @throws IOException if the path cannot be resolved to a valid and existing
   *                     URL.
   */
  public URL getResourceURL(String relativePath)
      throws IOException {
    relativePath = relativePath.replace('\\', '/');
    URL url;

    // In an application context, we try to find an external file relative to
    // the current working directory. If an external file does not exist, then
    // the file may be bundled into the jar with the classes. In this case, we
    // prepend a '/' to relativePath so that we search relative to the
    // classloader's root and not relative to the packaging of the current
    // class.

    File file = new File(relativePath);
    // If the file exists in the external file system, we return its
    // corresponding URL.
    if (file.exists()) {
      url = file.toURI().toURL();
    }
    // Otherwise, we search for the file relative to the classloader's root.
    // This will find the file if it is packaged into a jar with the classes.
    else {
      // Prepend a '/' so that we search relative to the classloader's root and
      // not relative to the packaging of the current class.
      if (relativePath.charAt(0) != '/') {
        relativePath = '/' + relativePath;
      }
      url = getClass().getResource(relativePath);
    }


    // Verify that we have a valid URL by trying to open its associated stream.
    if (url == null) {
      throw new FileNotFoundException(relativePath);
    }
    InputStream stream = url.openStream();
    stream.close();
    return url;
  }

  /**
   * Zooms the chart to fit the data model's time interval.
   */
  protected void zoomToFit() {
    zoomToFitAction.perform();
  }


  // =========================================
  // PriorityColumn.SliderEditor
  // =========================================

  /**
   * <code>SliderEditor</code> is a JSlider table cell editor that can be used
   * to edit an Integer of an <code>IlvGeneralActivity</code> within a table.
   */
  static class SliderEditor extends JSlider implements TableCellEditor {
    /**
     * There is currently a bug in JSlider that causes refresh problems if we
     * add the slider directly to the table. Instead, the workaround is to put
     * the slider inside of a container and add the container to the table.
     */
    private JPanel sliderContainer;

    /**
     * The list of cell editor listeners.
     */
    private IlvEventListenerCollection<CellEditorListener> listeners
        = new IlvEventListenerList<CellEditorListener>();

    /**
     * Creates a new horizontal <code>SliderEditor</code> .
     */
    SliderEditor(int min, int max, int ini) {
      super(HORIZONTAL, min, max, ini);
      setPaintTicks(true);
      setMajorTickSpacing(1);
      setPaintLabels(true);
      setSnapToTicks(true);
      // Create custom tick labels for even values.
      Font labelFont = new Font("Dialog", Font.PLAIN, 10);
      Hashtable<Integer, JLabel> labels = new Hashtable<Integer, JLabel>();
      for (int i = 0; i <= max; i += 2) {
        JLabel label = new JLabel("" + i, JLabel.CENTER);
        label.setFont(labelFont);
        labels.put(i, label);
      }
      setLabelTable(labels);
      // Put the slider inside a simple panel container. The container will
      // be added to the table when editing starts, instead of the slider being
      // added directly. This works around a current Swing bug in JSlider.
      sliderContainer = new JPanel(new BorderLayout());
      sliderContainer.add(this, BorderLayout.CENTER);
    }

    /**
     * Returns the component that should be added to the table's component
     * hierarchy to perform editing. Once installed in the table hierarchy this
     * component will be able to draw and receive user input.
     *
     * <p>Instead of returning the slider, we return the lightweight container that
     * the slider is in. This is a workaround for a current bug in JSlider.</p>
     *
     * @param table      The JTable that is asking the editor to edit. This
     *                   parameter can be <code>null</code>.
     * @param value      The value of the cell to be edited. This should either
     *                   be a String for a valid priority value or
     *                   <code>null</code>.
     * @param isSelected The cell is to be rendered with selection
     *                   highlighting.
     * @param row        The row of the cell being edited.
     * @param column     The column of the cell being edited.
     * @return The component to edit.
     */
    Override
    public Component getTableCellEditorComponent(JTable table,
                                                 Object value,
                                                 boolean isSelected,
                                                 int row, int column) {
      setValue(Integer.parseInt((String) value));
      // Remove the comment from the following line for the slider to have the
      // same color as the table.
      // this.setBackground(table.getBackground());
      //return sliderContainer;
      return this;
    }

    /**
     * Returns the value contained in the editor. This will be the slider's
     * value as a String.
     */
    Override
    public Object getCellEditorValue() {
      return String.valueOf(getValue());
    }

    /**
     * Asks the editor whether it can start editing using
     * <code>anEvent</code>.
     * <code>anEvent</code> is in the invoking component coordinate system.
     * The editor cannot assume the component returned by
     * <code>getCellEditorComponent()</code> is installed. This method is
     * intended for the use of the client to avoid the cost of setting up
     * and installing the editor component if editing is not possible.
     * If editing can be started this method returns <code>true</code>.
     *
     * @param anEvent The event the editor should use to consider whether to
     *                begin editing.
     * @return <code>true</code> if editing can be started.
     *
     * @see #shouldSelectCell
     */
    Override
    public boolean isCellEditable(EventObject anEvent) {
      if (anEvent instanceof MouseEvent) {
        if (((MouseEvent) anEvent).getClickCount() >= 2) {
          return true;
        }
      }
      return false;
    }

    /**
     * Specifies to the editor to start editing using <code>anEvent</code>.
     * The editor itself then decides whether it wants to start editing in
     * different states depending on the exact type of <code>anEvent</code>.
     * For example, with a text field editor, if the event is a mouse event
     * the editor might start editing with the cursor at the clicked point. If
     * the event is a keyboard event, it might want to replace the value
     * of the text field with that first key, and so on. <code>anEvent</code>
     * is in the invoking component's coordinate system. A <code>null</code>
     * value is a valid parameter for <code>anEvent</code>, and it is up to the
     * editor to determine what is the default starting state. For example,
     * a text field editor might want to select all the text and start
     * editing if <code>anEvent</code> is <code>null</code>. The editor can
     * assume the component returned by <code>getCellEditorComponent()</code>
     * is properly installed in the client component hierarchy before this
     * method is called.
     * <p>
     * The return value of <code>shouldSelectCell()</code> is a boolean
     * indicating whether the editing cell should be selected. Typically,
     * the return value is <code>true</code>, because in most cases the editing
     * cell should be selected. However, it is useful to return
     * <code>false</code> to keep the selection from changing for some types
     * of edits. For example, in a table that
     * contains a column of check boxes, the user might want to change
     * these check boxes without altering the selection. (See Netscape
     * Communicator for such an example.) Of course, it is up to
     * the client of the editor to use the return value, but it does not
     * need to if it does not want to.
     *
     * @param anEvent The event the editor should use to start editing.
     * @return <code>true</code> if the editor would like the editing cell to
     * be selected.
     * @see #isCellEditable
     */
    Override
    public boolean shouldSelectCell(EventObject anEvent) {
      return true;
    }

    /**
     * Specifies to the editor to stop editing and accept any partially edited
     * value as the value of the editor. The editor returns <code>false</code>
     * if editing was not stopped. This is useful for editors that validate and
     * cannot accept invalid entries.
     *
     * @return <code>true</code> if editing was stopped.
     */
    Override
    public boolean stopCellEditing() {
      fireEditingStopped();
      return true;
    }

    /**
     * Specifies to the editor to cancel editing and not accept any partially
     * edited value.
     */
    Override
    public void cancelCellEditing() {
      fireEditingCanceled();
    }

    /**
     * Adds a listener to the list that is notified when the editor starts,
     * stops, or cancels editing.
     *
     * @param l The <code>CellEditorListener</code>.
     */
    Override
    public void addCellEditorListener(CellEditorListener l) {
      listeners.addListener(l);
    }

    /**
     * Removes a listener from the list that is notified.
     *
     * @param l The <code>CellEditorListener</code>.
     */
    Override
    public void removeCellEditorListener(CellEditorListener l) {
      listeners.removeListener(l);
    }

    /**
     * Notifies all registered listeners that editing has stopped.
     */
    protected void fireEditingStopped() {
      Iterator<CellEditorListener> i = listeners.getListeners();
      if (i.hasNext()) {
        ChangeEvent e = new ChangeEvent(this);
        while (i.hasNext()) {
          i.next().editingStopped(e);
        }
      }
    }

    /**
     * Notifies all registered listeners that editing has been cancelled.
     */
    protected void fireEditingCanceled() {
      Iterator<CellEditorListener> i = listeners.getListeners();
      if (i.hasNext()) {
        ChangeEvent e = new ChangeEvent(this);
        while (i.hasNext()) {
          i.next().editingCanceled(e);
        }
      }
    }

  }

  // =========================================
  // Priority Column TableCellRenderer
  // =========================================

  /**
   * Creates a renderer that will be used to render the cells in this column. For each
   * cell, the renderer will be passed the <code>String</code> <em>value</em> returned by
   * the column's {@link IlvConfigurableTableColumn#getValue} method. We start with a
   * basic Swing <code>DefaultTableCellRenderer</code> and override the
   * <code>getTableCellRendererComponent</code> method to set the text color based on the
   * priority level.
   */
  static class PriorityRenderer extends DefaultTableCellRenderer {

    private Color darkGreen = Color.green.darker();

    PriorityRenderer() {
      setHorizontalAlignment(JLabel.CENTER);
    }

    Override
    public Component getTableCellRendererComponent(JTable table, Object value,
                                                   boolean isSelected,
                                                   boolean hasFocus,
                                                   int row, int column) {
      Component comp =
          super.getTableCellRendererComponent(table, value, isSelected,
                                              hasFocus, row, column);
      if (value instanceof String
          && ((String) (value)).length() != 0) {
        int intVal = Integer.parseInt((String) value);
        Color color;
        if (intVal <= 2) {
          color = Color.red;
        } else if (intVal <= 4) {
          color = Color.orange;
        } else if (intVal <= 7) {
          color = darkGreen;
        } else {
          color = Color.blue;
        }
        comp.setForeground(color);
      }
      return comp;
    }

  }

}