/*
 * 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 thinstock;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.Iterator;
import java.util.List;

import javax.servlet.ServletConfig;
import javax.swing.AbstractAction;
import javax.swing.BorderFactory;

import com.ibm.icu.text.DecimalFormat;
import com.ibm.icu.text.NumberFormat;

import ilog.views.chart.IlvAxis;
import ilog.views.chart.IlvChart;
import ilog.views.chart.IlvChartLayout;
import ilog.views.chart.IlvChartRenderer;
import ilog.views.chart.IlvColor;
import ilog.views.chart.IlvDataInterval;
import ilog.views.chart.IlvDefaultStepsDefinition;
import ilog.views.chart.IlvLegend;
import ilog.views.chart.IlvStyle;
import ilog.views.chart.IlvTimeUnit;
import ilog.views.chart.data.IlvDataSet;
import ilog.views.chart.data.IlvDefaultDataSet;
import ilog.views.chart.data.IlvDefaultDataSource;
import ilog.views.chart.event.AxisChangeEvent;
import ilog.views.chart.event.AxisListener;
import ilog.views.chart.event.AxisRangeEvent;
import ilog.views.chart.renderer.IlvHiLoChartRenderer;
import ilog.views.chart.renderer.IlvPolylineChartRenderer;
import ilog.views.chart.renderer.IlvSingleAreaRenderer;
import ilog.views.chart.renderer.IlvSingleBarRenderer;
import ilog.views.chart.renderer.IlvSinglePolylineRenderer;
import ilog.views.chart.renderer.IlvSingleStairRenderer;
import ilog.views.chart.util.IlvDoubleArray;
import ilog.views.util.IlvLocaleUtil;
import ilog.views.util.IlvResourceUtil;
import ilog.views.util.time.IlvCalendarFactory;
import stock.CategoryTimeSteps;
import stock.SharedGrid;
import stock.StockColors;
import stock.StockDataSource;
import stock.StockUtil;
import stock.Stripes;
import stock.ThresholdLines;
import stock.indicator.BollingerBandsIndicator;
import stock.indicator.MACDIndicator;
import stock.indicator.MovingAverageIndicator;
import stock.indicator.PriceChannelIndicator;
import stock.indicator.RSIIndicator;
import stock.indicator.StochasticIndicator;
import stock.indicator.TechnicalIndicator;
import stock.indicator.VolumeIndicator;
import stock.indicator.WilliamsRIndicator;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class StockDemoApp implements StockColors

{
  private static final Logger LOGGER = LoggerFactory.getLogger(StockDemoApp.class);

  /** The number of lower charts. */
  private static final int LOWER_CHART_COUNT = 2;

  private static final int RIGHT_MARGIN = 6;

  private static final Font DEFAULT_FONT = new Font("Dialog", Font.PLAIN, 10);

  public static final int LINE = 0;
  public static final int AREA = 1;
  public static final int BAR = 2;
  public static final int STAIR = 3;
  public static final int HLOC = 4;
  public static final int CANDLE = 5;

  /**
   * The threshold beyond which quotes are displayed with an overview
   * representation mode. This constant is equal to the approximate duration of
   * 3 months for daily data and one year for weekly data.
   */
  public static final double OVERVIEW_THRESHOLD = 65;

  static {
    List<Color> colorList = IlvColor.getDefaultColors();
    colorList.clear();
    colorList.addAll(Arrays.asList(DEFAULT_COLORS));
  }

  // == Chart related data members
  StockDataSource stockDS;
  IlvChart mainChart;
  IlvChart[] lowerCharts;

  List<TechnicalIndicator> upperIndicators = new ArrayList<TechnicalIndicator>();
  TechnicalIndicator[] lowerIndicators;

  /** The overview renderer for the primary symbol. */
  IlvChartRenderer overviewRenderer;
  /** The detailed renderer for the primary symbol. */
  IlvChartRenderer detailedRenderer;
  /** The renderer for the seconary symbols. */
  IlvChartRenderer secondaryRenderer;

  boolean antiAliasing = false;

  // == UI related data members
  private List<UpperIndicatorAction> upperIndicatorActions;
  private List<LowerIndicatorAction>[] lowerIndicatorActions;

  private NumberFormat numFmt = new DecimalFormat(".###");
  private ThresholdLines threshold;

  /** Creates a new instance of StockDemoApp */
  public StockDemoApp(ServletConfig config) {
    initDemo(config);
  }

  private void initDemo(ServletConfig config) {

    numFmt = NumberFormat.getNumberInstance();
    numFmt.setMaximumFractionDigits(3);

    // Create the stock data source to read the result of the query.
    URL defaultDataURL = null;

    try {
      defaultDataURL = config.getServletContext().getResource("/data/default.csv");
    } catch (Exception x) {
      x.printStackTrace();
    }
    stockDS = new StockDataSource(defaultDataURL);

    // == Create the main chart.
    mainChart = createMainChart();
    setOverviewRepresentation(LINE);
    setDetailedRepresentation(CANDLE);
    secondaryRenderer = new IlvPolylineChartRenderer();
    mainChart.addRenderer(secondaryRenderer);

    // == Create indicator charts.
    lowerCharts = createLowerCharts();
    lowerIndicators = new TechnicalIndicator[lowerCharts.length];

    initIndicatorActions();
    setLowerIndicator(0, ((IndicatorAction) lowerIndicatorActions[0].get(0)).getIndicator());
    setLowerIndicator(1, ((IndicatorAction) lowerIndicatorActions[1].get(1)).getIndicator());
    setAntiAliasing(true);

    // Load the default data.
    loadDefaultData(config);
  }

  /**
   * Returns the data source that holds the stock data.
   */
  public StockDataSource getStockDataSource() {
    return stockDS;
  }

  /**
   * Returns the main chart.
   */
  public IlvChart getMainChart() {
    return mainChart;
  }

  /**
   * Returns the lower chart of the specified index.
   */
  public IlvChart getLowerChart(int idx) {
    return lowerCharts[idx];
  }

  public boolean isOverviewVisible() {
    return overviewRenderer.isVisible();
  }

  public IlvChartRenderer getOverviewRenderer() {
    return overviewRenderer;
  }

  public IlvDataSet getDetailedDataSet() {
    return detailedRenderer.getDataSource().getDataSet(2);
  }

  public IlvDataSet getLowerIndicatorDataSet(int chartIdx) {
    return lowerIndicators[chartIdx].getMainDataSet();
  }

  // --------------------------------------------------------------------------
  // - Indicators

  public String getLowerIndicatorName(int chartIdx) {
    return lowerIndicators[chartIdx].getName();
  }

  public void setLowerIndicator(int chartIdx, int indicatorIdx) {
    ((IndicatorAction) lowerIndicatorActions[chartIdx].get(indicatorIdx)).actionPerformed(null);
  }

  /** Sets a lower indicator. */
  protected void setLowerIndicator(int idx, TechnicalIndicator indicator) {
    if (lowerIndicators[idx] == indicator)
      return;
    if (lowerIndicators[idx] != null)
      lowerIndicators[idx].detach();
    if (indicator != null)
      indicator.attach(lowerCharts[idx]);
    lowerIndicators[idx] = indicator;
  }

  public void setUpperIndicators(int[] indicators) {
    for (int i = 0; i < upperIndicatorActions.size(); ++i) {
      UpperIndicatorAction action = (UpperIndicatorAction) upperIndicatorActions.get(i);
      removeUpperIndicator(action.getIndicator());
      action.reset();
    }
    for (int i = 0; i < indicators.length; ++i)
      ((IndicatorAction) upperIndicatorActions.get(indicators[i])).actionPerformed(null);
  }

  /** Adds an upper indicator. */
  protected void addUpperIndicator(TechnicalIndicator indicator) {
    if (indicator != null)
      indicator.attach(mainChart);
    upperIndicators.add(indicator);
  }

  /** Removes an upper indicator. */
  protected void removeUpperIndicator(TechnicalIndicator indicator) {
    if (!upperIndicators.contains(indicator))
      return;
    if (indicator != null)
      indicator.detach();
    upperIndicators.remove(indicator);
  }

  /**
   * Creates the main chart. This chart displays the stock data and the upper
   * indicators.
   */
  protected IlvChart createMainChart() {
    IlvChart chart = createChart();
    chart.setBorder(BorderFactory.createEmptyBorder(0, 0, 4, 0));
    setMargins(chart);
    chart.getXAxis().setVisibleRange(0, 10);
    chart.getYScale(0).setTitle("Share price ($)", 270);
    chart.getYScale(0).getTitleRenderer().setFont(new Font("Dialog", Font.PLAIN, 12));
    Stripes stripes = new Stripes(chart.getYScale(0), STRIPES_COLOR);
    stripes.setDrawOrder(-3);
    chart.addDecoration(stripes);
    chart.getYAxis(0).setVisibleRange(0, 100);

    chart.getXScale().setMajorTickVisible(false);
    chart.getXScale().setVisible(false);
    chart.getChartArea().setRightMargin(RIGHT_MARGIN);

    // Add an axis listener to toggle the visibility of the
    // quote renderers/quote overview.
    chart.getXAxis().addAxisListener(new AxisListener() {
      double oldLen = 0;
      boolean oldAntiAliasing;

      Override
      public void axisRangeChanged(AxisRangeEvent evt) {
        if (evt.isChangedEvent() && !evt.isAdjusting() && evt.isVisibleRangeEvent()) {
          updateDisplay(evt.getAxis());
        } else if (!evt.isChangedEvent()) {
          double min = evt.getNewMin();
          double max = evt.getNewMax();
          if (max - min < StockUtil.MIN_XLEN) {
            double mid = (max + min) / 2;
            evt.setNewMax(mid + StockUtil.MIN_XLEN / 2);
            evt.setNewMin(mid - StockUtil.MIN_XLEN / 2);
          }
        }
      }

      Override
      public void axisChanged(AxisChangeEvent evt) {
        if (evt.getType() == AxisChangeEvent.ADJUSTMENT_CHANGE) {
          if (evt.isAdjusting()) {
            if (StockDemoApp.this.antiAliasing) {
              StockDemoApp.this.setAntiAliasing(false);
              oldAntiAliasing = true;
            }
          } else {
            if (oldAntiAliasing) {
              StockDemoApp.this.setAntiAliasing(true);
              oldAntiAliasing = false;
            }
            updateDisplay(evt.getAxis());
          }
        } else if (evt.getType() == AxisChangeEvent.TRANSFORMER_CHANGE) {
          // LocalZoomHandler lzh = LocalZoomHandler.get(mainChart, -1);
          // if (lzh == null) return;
          // if (oldZoomfactor != lzh.getTransformer().getZoomFactor()) {
          // zoomInAction.computeEnabled();
          // zoomOutAction.computeEnabled();
          // oldZoomfactor = lzh.getTransformer().getZoomFactor();
          // }
        }
      }

      private void updateDisplay(IlvAxis axis) {
        // Check if the zoom level has changed and call the
        // xRangeChanged() method.
        IlvDataInterval xRange = mainChart.getXAxis().getVisibleRange();
        double newLen = xRange.getLength();
        boolean zoom = (oldLen == 0) || (Math.abs(newLen - oldLen) / oldLen > .01);
        StockDemoApp.this.xRangeChanged(zoom);
        oldLen = newLen;
      }
    });

    chart.getYAxis(0).addAxisListener(new AxisListener() {
      Override
      public void axisRangeChanged(AxisRangeEvent evt) {
        if (evt.isAboutToChangeEvent()) {
          IlvDataInterval range = StockUtil.getYDataRange(mainChart, 0, mainChart.getXAxis().getVisibleRange());
          if (!range.isEmpty()) {
            evt.setNewMin(range.min);
            evt.setNewMax(range.max);
          }
        }
      }

      Override
      public void axisChanged(AxisChangeEvent evt) {
      }
    });

    // == Add the quote display panel and the legend as a header.
    // JPanel header = new JPanel(new GridLayout(0,1,0,4));
    // header.setBorder(BorderFactory.createEmptyBorder(4, 0, 0, 8));
    // header.setBackground(HEADER_COLOR);
    IlvLegend legend = createLegend();
    // chart.setLegend(legend);
    // header.add(legend);
    // chart.setHeader(header);
    chart.addLegend(legend, IlvChartLayout.NORTH_BOTTOM);

    return chart;
  }

  /** Creates the charts displaying lower indicators. */
  protected IlvChart[] createLowerCharts() {
    IlvChart[] charts = new IlvChart[LOWER_CHART_COUNT];
    for (int i = 0; i < LOWER_CHART_COUNT; ++i) {
      charts[i] = createLowerChart();
    }
    return charts;
  }

  /** Creates a lower chart. */
  private IlvChart createLowerChart() {
    IlvChart chart = createChart();
    chart.synchronizeAxis(mainChart, IlvAxis.X_AXIS, false);
    chart.setXScale(null);
    chart.setXGrid(new SharedGrid(mainChart.getXScale(), GRID_COLOR));
    setMargins(chart);
    // JPanel header = new JPanel(new GridLayout(0,1,0,4));
    // header.setBorder(BorderFactory.createEmptyBorder(4, 0, 0, 8));
    // header.setBackground(HEADER_COLOR);
    IlvLegend legend = createLegend();
    // chart.setLegend(legend);
    // header.add(legend);
    // chart.setHeader(header);
    chart.addLegend(legend, IlvChartLayout.NORTH_BOTTOM);
    return chart;
  }

  private void setMargins(IlvChart chart) {
    chart.getChartArea().setHorizontalMargins(50, 3);
  }

  /** Creates the list of upper indicators. */
  protected List<TechnicalIndicator> createUpperIndicators() {
    List<TechnicalIndicator> list = new ArrayList<TechnicalIndicator>();
    list.add(new BollingerBandsIndicator(stockDS));
    list.add(new PriceChannelIndicator(stockDS));
    list.add(MovingAverageIndicator.createSMA(stockDS, 20));
    list.add(MovingAverageIndicator.createSMA(stockDS, 40));
    list.add(MovingAverageIndicator.createEMA(stockDS, 20));
    list.add(MovingAverageIndicator.createEMA(stockDS, 40));
    return list;
  }

  /** Creates the list of lower indicators. */
  protected List<TechnicalIndicator> createLowerIndicators() {
    List<TechnicalIndicator> list = new ArrayList<TechnicalIndicator>();
    list.add(new VolumeIndicator(stockDS));
    list.add(new RSIIndicator(stockDS, 14));
    list.add(MACDIndicator.createMACD(stockDS));
    list.add(new WilliamsRIndicator(stockDS, 14));
    list.add(StochasticIndicator.createFastStochastic(stockDS));
    list.add(StochasticIndicator.createSlowStochastic(stockDS));
    return list;

  }

  /** Initializes the actions associated with indicators. */
  SuppressWarnings("unchecked")
  private void initIndicatorActions() {
    List<TechnicalIndicator> list = createUpperIndicators();
    Iterator<TechnicalIndicator> ite = list.iterator();
    upperIndicatorActions = new ArrayList<StockDemoApp.UpperIndicatorAction>(list.size());
    while (ite.hasNext()) {
      TechnicalIndicator indicator = (TechnicalIndicator) ite.next();
      UpperIndicatorAction action = new UpperIndicatorAction(indicator);
      action.updateEnabled();
      upperIndicatorActions.add(action);
    }

    lowerIndicatorActions = new ArrayList[lowerCharts.length];
    for (int i = 0; i < lowerIndicatorActions.length; ++i) {
      lowerIndicatorActions[i] = new ArrayList<LowerIndicatorAction>();
      ite = createLowerIndicators().iterator();
      while (ite.hasNext()) {
        TechnicalIndicator indicator = (TechnicalIndicator) ite.next();
        LowerIndicatorAction action = new LowerIndicatorAction(indicator, i);
        action.updateEnabled();
        lowerIndicatorActions[i].add(action);
      }
    }
  }

  /** Toggles the price threshold. */
  public void setThreshold(boolean set) {
    if (set) {
      if (threshold == null) {
        Color upperColor = new Color(120, 220, 120);
        Color midColor = new Color(233, 150, 122);
        Color lowerColor = IlvColor.indianRed;
        threshold = new ThresholdLines(FOREGROUND, BACKGROUND, lowerColor, midColor, upperColor);
        ThresholdLines.set(threshold, mainChart, 0);
        threshold.setThresholds(mainChart.getYAxis(0).getDataRange());
      } else {
        ThresholdLines.set(threshold, mainChart, 0);
      }
      overviewRenderer.setRenderingHint(threshold);
      detailedRenderer.setRenderingHint(threshold);
    } else {
      ThresholdLines.remove(mainChart, 0);
      overviewRenderer.setRenderingHint(null);
      detailedRenderer.setRenderingHint(null);
    }
  }

  /** Toggles anti-aliasing for the charts. */
  public void setAntiAliasing(boolean b) {
    if (antiAliasing == b)
      return;
    mainChart.setAntiAliasing(b);
    mainChart.getChartArea().repaint();
    for (int i = 0; i < lowerCharts.length; ++i) {
      lowerCharts[i].setAntiAliasing(b);
      lowerCharts[i].getChartArea().repaint();
    }
    antiAliasing = b;
  }
  
  /**
   * Sets the input API key on the StockDataSource instance.
   */
  public void setAPIKey(String apiKey) {
          stockDS.setAPIKey(apiKey);
  }

  /**
   * Loads the quotes.
   */
  public boolean loadQuotes(final String symbol, Date startDate, Date endDate, int frequency, String[] secsymbols) {
        boolean retVal = false;
        if (symbol == null || symbol.length() == 0)
      LOGGER.warn("Missing ticker symbol!");
        else if (stockDS == null || stockDS.getAPIKey().isEmpty())
          LOGGER.warn("Missing API key!");
        else {
      // == Reset all data
      overviewRenderer.getDataSource().setDataSets(null);
      detailedRenderer.getDataSource().setDataSets(null);
      secondaryRenderer.getDataSource().setDataSets(null);

      // == Workaround BUG IlvHiLoChartRenderer (setDataSets(null))
      int mode = ((IlvHiLoChartRenderer) detailedRenderer).getMode();
      if (mode == IlvHiLoChartRenderer.CANDLE)
        setDetailedRepresentation(CANDLE);
      else
        setDetailedRepresentation(HLOC);
      // == End Workaround

      stockDS.reset();
      updateIndicators();
      stockDS.loadData(symbol, startDate, endDate, frequency, null);
      // primaryDataLoaded(symbol, secsymbols);
      retVal = true;
        }
        return retVal;
  }

  private void loadDefaultData(ServletConfig config) {
    URL dataFile = null;
    try {
      dataFile = config.getServletContext().getResource("/data/default.csv");
    } catch (MalformedURLException e) {
      e.printStackTrace();
    }
    stockDS.loadDefaultData(dataFile);
    primaryDataLoaded(StockDataSource.DEFAULT_SYMBOL, null);
    setThreshold(true);
    threshold.setThresholds(new IlvDataInterval(25, 45));
  }

  public void primaryDataLoaded(String expectedSymbol, String[] secsymbols) {
    if (stockDS.getDataSetCount() > 0) {
      if (threshold != null && ThresholdLines.get(mainChart, 0) == null) // i.e.
                                                                         // Threshold
                                                                         // has
                                                                         // been
                                                                         // hidden
        threshold = null;

      String symbol = stockDS.getSymbol().toUpperCase();
      if (!symbol.equalsIgnoreCase(expectedSymbol)) {
        String msg = IlvResourceUtil.getServerLocaleString(StockDemoApp.class, "PrimarySymbolNotFound");
        System.err.println(MessageFormat.format(msg, expectedSymbol));
      }

      // == Update overview Renderer
      overviewRenderer.getDataSource().addDataSet(stockDS.getCloseDataSet());
      overviewRenderer.setName(symbol);

      // == Update detailed renderer
      IlvDataSet[] quotes = { stockDS.getLowDataSet(), // low
          stockDS.getHighDataSet(), // high
          stockDS.getOpenDataSet(), // open
          stockDS.getCloseDataSet() // close
      };
      detailedRenderer.getDataSource().setDataSets(quotes);
      detailedRenderer.setName(symbol);
      Color c = PRIMARY_COLOR;
      IlvStyle style1 = IlvStyle.createStroked(c);
      IlvStyle style2 = new IlvStyle(c, IlvColor.brighter(c));
      IlvStyle style3 = new IlvStyle(c, IlvColor.darker(c));
      detailedRenderer.setStyles(new IlvStyle[] { style1, style1, style2, style3 });

      // == Load data for secondary symbols
      // final String[] secondarySymbols = queryPanel.getSecondarySymbols();
      if (secsymbols != null && secsymbols.length > 0) {
        IlvDoubleArray[] data = stockDS.loadData(secsymbols, null);
        secondaryDataLoaded(secsymbols, data);
      }
    }

    // == Update indicators
    updateIndicators();
    // == Reset visible range and history.
    resetVisibleRange();

    // == Update the x-scale
    updateXScale();

    // Adjust existing threshold
    if (threshold != null) {
      threshold.setThresholds(mainChart.getYAxis(0).getVisibleRange());
    }
  }

  private void secondaryDataLoaded(String[] symbols, IlvDoubleArray[] data) {
    IlvDefaultDataSource dataSource = new IlvDefaultDataSource();
    if (data != null) { // else loading was interrupted by another request
      // Update the renderer for the secondary symbols.
      String error = null;
      for (int i = 0; i < data.length; ++i) {
        if (data[i] == null) {
          if (error == null)
            error = symbols[i];
          else
            error += ", " + symbols[i];
          continue;
        }
        String name = symbols[i].toUpperCase();
        data[i].trim();
        dataSource.addDataSet(new IlvDefaultDataSet(name, data[i].data()));
      }
      if (error != null) {
        String msg = IlvResourceUtil.getServerLocaleString(StockDemoApp.class, "secondSymbolNotFound");
        System.out.println(msg + error);
      }
    }
    secondaryRenderer.setDataSource(dataSource);
  }

  /**
   * Invoked when quotes are loaded for the primary symbol. This method updates
   * the steps definition of the x-scale.
   */
  private void updateXScale() {
    Date[] dates = stockDS.getDates();
    if (dates == null) {
      mainChart.getXScale().setVisible(false);
      mainChart.getXScale().setStepsDefinition(new IlvDefaultStepsDefinition());
    } else {
      IlvTimeUnit unit = null;
      switch (stockDS.getFrequency()) {
      case StockDataSource.MONTHLY:
        unit = IlvTimeUnit.MONTH;
        break;
      case StockDataSource.WEEKLY:
        unit = IlvTimeUnit.WEEK;
        break;
      case StockDataSource.DAILY:
      default:
        unit = IlvTimeUnit.DAY;
        break;
      }
      CategoryTimeSteps def;
      def = new CategoryTimeSteps(dates, unit);
      mainChart.getXScale().setStepsDefinition(def);
      mainChart.getXScale().setVisible(true);
    }
  }

  /**
   * Invoked when quotes are loaded for the primary symbol. This method updates
   * the indicators and the enabled state of their associated actions
   */
  private void updateIndicators() {
    // Refresh indicators.
    for (int i = 0; i < lowerIndicators.length; ++i) {
      if (lowerIndicators[i] != null)
        lowerIndicators[i].refresh();
    }

    Iterator<TechnicalIndicator> ite = upperIndicators.iterator();
    while (ite.hasNext())
      ((TechnicalIndicator) ite.next()).refresh();

    // Update the enabled state of actions.
    Iterator<UpperIndicatorAction> ite2 = upperIndicatorActions.iterator();
    while (ite2.hasNext()) {
      ((UpperIndicatorAction) ite2.next()).updateEnabled();
    }

    Iterator<LowerIndicatorAction> ite3;
    for (int i = 0; i < lowerIndicatorActions.length; ++i) {
      ite3 = lowerIndicatorActions[i].iterator();
      while (ite3.hasNext()) {
        ((LowerIndicatorAction) ite3.next()).updateEnabled();
      }
    }
  }

  private void resetVisibleRange() {
    mainChart.getXAxis().setAutoVisibleRange(true);
    mainChart.getYAxis(0).setAutoVisibleRange(true);
  }

  /**
   * Creates a legend for the charts.
   */
  protected IlvLegend createLegend() {
    IlvLegend legend = new IlvLegend();
    legend.setLayout(new FlowLayout(FlowLayout.LEFT, 12, 2));
    legend.setPaintingBackground(false);
    legend.setBorder(null);
    legend.setForeground(FOREGROUND);
    legend.setMovable(false);
    legend.setFont(DEFAULT_FONT);
    legend.setSymbolSize(new Dimension(16, 12));
    return legend;
  }

  /**
   * Creates a new <code>IlvChart</code> and sets common properties.
   */
  protected IlvChart createChart() {
    IlvChart chart = new IlvChart();
    chart.getXGrid().setMajorPaint(GRID_COLOR);
    chart.getYGrid(0).setMajorPaint(GRID_COLOR);
    chart.getXGrid().setDrawOrder(-2);
    chart.getYGrid(0).setDrawOrder(-2);
    chart.setForeground(FOREGROUND);
    chart.setBackground(BACKGROUND);
    chart.setOpaque(true);
    chart.setFont(DEFAULT_FONT);
    IlvStyle style = chart.getChartArea().getPlotStyle();
    if (style != null)
      chart.getChartArea().setPlotStyle(style.setFillOn(false));
    return chart;
  }

  /**
   * Invoked when the visible time range changes.
   * <p>
   * This method toggles the visibility of the quote overview/quote renderers
   * according to the visible time range.
   */
  private void xRangeChanged(boolean zoom) {
    IlvDataInterval xRange = mainChart.getXAxis().getVisibleRange();
    if (xRange.isEmpty())
      return;

    IlvDataInterval yRange = StockUtil.getYDataRange(mainChart, 0, xRange);
    if (!yRange.isEmpty())
      mainChart.getYAxis(0).setVisibleRange(yRange);
    if (zoom) {
      boolean hideOverview = (xRange.getLength() < OVERVIEW_THRESHOLD);
      mainChart.getYAxis(0).setAutoDataRange(false);
      overviewRenderer.setVisible(!hideOverview);
      detailedRenderer.setVisible(hideOverview);
      mainChart.getYAxis(0).setAutoDataRange(true);
    }
  }

  /**
   * Specifies the stock overview representation.
   * 
   * @param representation
   *          The desired representation.
   * @see #BAR
   * @see #LINE
   * @see #AREA
   * @see #STAIR
   */
  public void setOverviewRepresentation(int representation) {
    int idx = mainChart.getRenderers().indexOf(overviewRenderer);
    IlvChartRenderer r = null;
    Color c = DEFAULT_COLORS[0];
    IlvStyle filledStyle = new IlvStyle(c, IlvColor.setAlpha(c, .45f));
    IlvStyle strokedStyle = new IlvStyle(2.f, c);
    switch (representation) {
    case BAR:
      r = new IlvSingleBarRenderer(filledStyle, 100.);
      break;
    case STAIR:
      r = new IlvSingleStairRenderer(filledStyle);
      break;
    case AREA:
      r = new IlvSingleAreaRenderer(filledStyle);
      break;
    case LINE:
    default:
      r = new IlvSinglePolylineRenderer(strokedStyle);
      break;
    }
    if (idx == -1) {
      mainChart.addRenderer(r);
    } else {
      r.setVisible(overviewRenderer.isVisible());
      r.setName(overviewRenderer.getName());
      mainChart.setRenderer(idx, r);
      // Update y-range explicitly since the setRenderer can modify
      // the x-range before the new renderer is added.
      xRangeChanged(false);
    }
    overviewRenderer = r;
  }

  /**
   * Specifies the stock overview representation.
   * 
   * @param representation
   *          The desired representation.
   * @see #HLOC
   * @see #CANDLE
   */
  public void setDetailedRepresentation(int representation) {
    int idx = mainChart.getRenderers().indexOf(detailedRenderer);
    IlvChartRenderer r = null;
    switch (representation) {
    case HLOC:
      r = new IlvHiLoChartRenderer(IlvHiLoChartRenderer.OPENCLOSE, 0, 80);
      break;
    case CANDLE:
    default:
      r = new IlvHiLoChartRenderer(IlvHiLoChartRenderer.CANDLE, 0, 80);
      break;
    }
    if (idx == -1)
      mainChart.addRenderer(r);
    else {
      r.setVisible(detailedRenderer.isVisible());
      r.setName(detailedRenderer.getName());
      IlvStyle[] styles = detailedRenderer.getStyles();
      mainChart.setRenderer(idx, r);
      r.setStyles(styles);
      xRangeChanged(false);
    }
    detailedRenderer = r;
  }

  /**
   * Returns the default start date for quote queries.
   * 
   * @return One year before the current date.
   */
  static final Date getDefaultStartDate() {
    Calendar cal = IlvCalendarFactory.createInstance(IlvLocaleUtil.getCurrentULocale());
    cal.setTime(new Date());
    cal.add(Calendar.YEAR, -1);
    return cal.getTime();
  }

  // --------------------------------------------------------------------------
  /**
   * The base action class for setting indicators.
   */
  abstract class IndicatorAction extends AbstractAction {
    private TechnicalIndicator indicator;

    /**
     * Creates a new <code>UpperIndicatorAction</code> that references the
     * specified indicator.
     */
    public IndicatorAction(TechnicalIndicator indicator) {
      super(indicator.getName());
      this.indicator = indicator;
    }

    /**
     * Computes the enable state of the action.
     */
    public void updateEnabled() {
      boolean enabled = (stockDS.getDataSetCount() > 0);
      setEnabled(enabled);
    }

    /**
     * Returns the indicator associated with this action.
     */
    public final TechnicalIndicator getIndicator() {
      return indicator;
    }
  }

  /**
   * An action class used to add upper indicators.
   */
  class UpperIndicatorAction extends IndicatorAction {
    private boolean added;

    /**
     * Creates a new <code>UpperIndicatorAction</code> that references the
     * specified indicator.
     */
    public UpperIndicatorAction(TechnicalIndicator indicator) {
      super(indicator);
    }

    /** Invoked when an action occurs. */
    Override
    public void actionPerformed(ActionEvent evt) {
      if (!isEnabled())
        return;
      if (added) {
        removeUpperIndicator(getIndicator());
        added = false;
      } else {
        addUpperIndicator(getIndicator());
        added = true;
      }
    }

    public void reset() {
      added = false;
    }
  }

  /**
   * An action class used to set lower indicators.
   */
  class LowerIndicatorAction extends IndicatorAction {
    private int chartIdx;

    /**
     * Creates a new <code>LowerIndicatorAction</code> that references the
     * specified indicator.
     */
    public LowerIndicatorAction(TechnicalIndicator indicator, int chartIdx) {
      super(indicator);
      this.chartIdx = chartIdx;
    }

    /** Invoked when an action occurs. */
    Override
    public void actionPerformed(ActionEvent evt) {
      if (!isEnabled())
        return;
      setLowerIndicator(chartIdx, getIndicator());
    }
  }

}