/*
* MktView.java (Market View Java Applet)
* Copyright (C) 1996 Softbear Inc. (info@softbear.com)
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*
*/

/*
* The MktView class is used to view a stock's
* performance (e.g. each day's high/low, open/close,
* and volume) over a period of time (e.g. 90 days).
* This data can be graphed using a bar graph, a
* candlesquote graph, a line graph, etc.
* This class merely does the screen painting, the
* actual data is stored in the MktModel class.
* @see #MktModel
*/

public class MktView extends java.applet.Applet {

static boolean debug = false;
static int BAR_GRAPH = 0;
static int CANDLESTICK_GRAPH = 1;
static int LINE_GRAPH = 2;

/*
* In the FUTURE, the line graph can be supplemented
* with a moving average, momentum, relative strength
* index, stochastic oscillator, line graph w/ moving average, etc.
*/
static String gnameTab[] = {
"Bar Graph", /* 0 */
"Candlesquote Graph", /* 1 */
"Line Graph", /* 2 */
"Line Graph with Moving Average" /* 2 */
};


// model
MktModel mktModel;
int styleNr;

// graph view
java.awt.Panel westPanel; // displays the quote info
java.awt.Component eastMenu; // contains the graph style
java.awt.TextField textField; // contains the stock name

// clickable region
int imouse;
int xmouse;
int ymouse0;
int ymouse1;

// quote view
int selectedQuoteNr;
QuoteView quoteView;

public boolean action(java.awt.Event evt, java.lang.Object arg) {
if (evt.target instanceof java.awt.Button) {
this.setSymbol();
} else if (evt.target instanceof java.awt.Choice) {
String str = (String) arg;
if (str.equals(this.gnameTab[this.BAR_GRAPH])) {
this.styleNr = this.BAR_GRAPH;
} else if (str.equals(this.gnameTab[this.CANDLESTICK_GRAPH])) {
this.styleNr = this.CANDLESTICK_GRAPH;
} else if (str.equals(this.gnameTab[this.LINE_GRAPH])) {
this.styleNr = this.LINE_GRAPH;
}
}
this.repaint();
return true;
}

public void init() {
int nrDays;
String stockName;

stockName = this.getParameter("stockName");
try {
nrDays = Integer.parseInt(this.getParameter("nrDays"), 10);
} catch (NumberFormatException exception) {
nrDays = 60;
}

if (stockName == null) {
stockName = "FAKE";
}
if (this.debug) {
java.lang.System.out.println("MktView.init(): stockName=" + stockName + ", nrDays=" + nrDays);
}

// init view
this.init_view();
this.quoteView.setQuote(null);
this.textField.setText(stockName);

// init model
String hostName = this.getCodeBase().getHost();
this.init_model(hostName, nrDays);
this.setSymbol();
try {
this.setQuote(nrDays-1);
} catch (java.lang.Exception exception) {
// ignore it
}
}

private void init_model(String serverName, int nrDays) {
java.util.Date todaysDate = new java.util.Date();
/* Eliminate hours/minutes/seconds, they are irrelevant */
todaysDate.setHours(0);
todaysDate.setMinutes(0);
todaysDate.setSeconds(0);

/*
* Compute the start day by subtracting
* nrDays worth of time from today's date.
*/
long startMillisec = todaysDate.getTime()
- (nrDays-1)*24L*60L*60L*1000L;
java.util.Date startDate = new java.util.Date(startMillisec);
this.mktModel = new MktModel(serverName, startDate, nrDays,
MktModel.DAY_INTERVAL);
}

private void init_view() { // throws java.lang.Exception {
int hgap = 1;
int vgap0 = 0;
int vgap = 2;

this.westPanel = new java.awt.Panel();
this.westPanel.setLayout(new StackLayout(vgap));

/*
* The West panel sports the current quote.
*/
java.awt.Button updateButton = new java.awt.Button("UPDATE");
this.westPanel.add(updateButton);
this.textField = new java.awt.TextField();
this.westPanel.add(this.textField);

java.awt.Panel quotePanel = new java.awt.Panel();
quotePanel.setLayout(new java.awt.GridLayout(0,2,hgap,vgap0));

this.quoteView = new QuoteView(quotePanel); // adds itself to quotePanel
this.westPanel.add(quotePanel);


/*
* The East sports the graph-style menu, and
* below it, the graph itself.
*/

java.awt.Choice graphStyleChoice = new java.awt.Choice();
this.eastMenu = graphStyleChoice;
graphStyleChoice.addItem(this.gnameTab[this.LINE_GRAPH]);
graphStyleChoice.addItem(this.gnameTab[this.BAR_GRAPH]);
/*
graphStyleChoice.addItem(this.gnameTab[this.CANDLESTICK_GRAPH]);
*/
/* this.styleNr = MktView.LINE_GRAPH; */
this.styleNr = MktView.BAR_GRAPH;
graphStyleChoice.select(this.gnameTab[this.styleNr]);
this.add("Graph Style", graphStyleChoice);


this.setLayout(new java.awt.BorderLayout());
this.add("West", this.westPanel);
this.add("East", graphStyleChoice);
}

public boolean mouseUp(java.awt.Event event, int x, int y) {

if (x >= this.xmouse &&
y >= this.ymouse0 && y <= this.ymouse1) {

int x0 = x - xmouse;
int quoteNr = x0 / imouse;
if (quoteNr < this.mktModel.countQuotes()) {
if (this.debug) {
java.lang.System.out.println("MktView.mouseUp(): you selected quote number " + quoteNr);
}
try {
this.setQuote(quoteNr);
this.repaint();
} catch (java.lang.Exception exception) {
// ignore invalid clicks
}
}
}

return true;

}

public void paint(java.awt.Graphics g) {

int wv = 25; /* KLUDGEy approximation */
int wp = 25; /* KLUDGEy approximation */
int marginWidth = 8;
int tb = 4;

int xg = this.westPanel.size().width + marginWidth + wv;
// int yg = this.eastMenu.size().height + marginWidth;
int yg = 30; // KLUDGEy approximation of drop-down menu size
int wg = this.size().width - (xg + marginWidth + wp);
int hg = this.size().height - (yg + marginWidth);

if (this.debug) {
java.lang.System.out.println("MktView.paint(): xg=" + xg + ", yg=" + yg);
}

if (this.mktModel.isEmpty()) {
g.drawString("NO MKT DATA FOR " + this.mktModel.getName() + ", TRY 'FAKE'", xg, yg);
} else {
try {
hg -= this.paint_graph_dates_axis(g, xg+tb, yg, yg+hg, wg-tb); // also: writes clickable region
this.paint_graph_border(g, xg, yg, wg, hg, tb);
int nu = this.paint_graph_prices(g, xg, yg, wg, hg, wp); // reads clickable region
int ng = this.paint_graph_guidelines(g, xg, yg, wg, hg, tb, nu);
this.paint_graph_volumes(g, xg, yg, wg, hg, tb, wv, ng);
if (this.debug) {
java.lang.System.out.println("........PAINTING is done");
}
} catch (java.lang.Exception exception) {
// do nothing
}
}
}

private void paint_graph_border(java.awt.Graphics g, int xg, int yg, int wg, int hg, int tb2) {

if (this.debug) {
java.lang.System.out.println("-------.paint_graph_border(): xg=" + xg +
", yg=" + yg + "wg=" + wg + ", hg=" + hg + "tb2=" + tb2);
}

java.awt.Color oldColor = g.getColor();
int tb1 = tb2/2;
g.setColor(java.awt.Color.black);
g.fillRect(xg, yg, wg, tb1); // top
g.fillRect(xg, yg, tb1, hg); // left
g.fillRect(xg, yg+hg-tb1, wg, tb1); // bottom (black)
g.fillRect(xg+wg-tb1, yg, tb1, hg); // right (black)
g.setColor(java.awt.Color.white);
g.fillRect(xg+tb2, yg+hg-tb2, wg-2*tb2, tb1); // bottom (white)
g.fillRect(xg+wg-tb2, yg+tb2, tb1, hg-tb2-tb1); // right (white)
g.setColor(oldColor);
}

/*
* Paints the dates, and sets the mouse-clickable dates.
*/
private int paint_graph_dates_axis(java.awt.Graphics g, int xd, int yg, int yd3, int wd2) throws java.lang.Exception {
int halfCharWidth = 2; // KLUDGEy approximation
int hs = 4;
int strokeMargin = 2;
java.awt.Font dayFont = new java.awt.Font("Courier", java.awt.Font.PLAIN, 8);
java.awt.Font monthFont = new java.awt.Font("Courier", java.awt.Font.PLAIN, 10);
int yd2 = yd3 - monthFont.getSize(); // FUTURE: use FontMetrics
int yd1 = yd2 - (strokeMargin + dayFont.getSize()); // FUTURE: use FontMetrics
int yd0 = yd1 - hs;
int ns = this.mktModel.countQuotes();
int ds = wd2/ns;
int wd1 = ns * ds;
int dd = wd2-wd1;
int xi0 = xd + dd;
int xi = xi0;

if (this.debug) {
java.lang.System.out.println("-------.paint_dates_axis(): xd=" + xd + ", yd0=" + yd0 + ", wd2=" + wd2 + ", ns=" + ns + ", ds=" + ds + ", dd=" + dd);
}

for (int i = 0; i < ns; ++i, xi += ds) {
java.awt.Color oldColor = g.getColor();
if (i == this.selectedQuoteNr) {
g.setColor(java.awt.Color.yellow);
}
g.drawLine(xi, yd0, xi, yd1);

java.util.Date date;
date = this.mktModel.getQuote(i).getDate();

if (date.getDay() == 1) { // MONDAY
java.lang.Integer dayOfMonth = new java.lang.Integer(date.getDate());
g.drawString(dayOfMonth.toString(), xi-halfCharWidth, yd2);
}
if (i == this.selectedQuoteNr) {
g.setColor(oldColor);
}
if (date.getDate() == 15) { // mid-month
String monthTab[] = {
"Jan", "Feb", "Mar",
"Apr", "May", "Jun",
"Jul", "Aug", "Sep",
"Oct", "Nov", "Dec" };
String monthName = monthTab[date.getMonth()];
g.drawString(monthName, xi, yd3);
}
}

this.setClickableDates(xi0, ds, yg, yd2);
return yd3-yd0;
}


/*
* Paints the prices, and returns the number of units.
* Note: reads the clickable region.
*/
private int paint_graph_prices(java.awt.Graphics g, int xg, int yg, int wg, int hg, int wp) throws java.lang.Exception {
int maxNrUnits = 15;
int minNrUnits = 1;
int unitEighths = 1;
int maxEighths = this.mktModel.maxPrice();
int minEighths = this.mktModel.minPrice();
if (this.debug) {
java.lang.System.out.println("*** maxPrice=" + Pretty.udollars(maxEighths) + ", minPrice=" + Pretty.udollars(minEighths));
}

/*
* Using trial and error approach, compute what
* (unit, #units) will make the prettiest graph.
*/
int bestMetric = 0;
int bestUnitEighths = 0;
int bestNrUnits = 0;
while (unitEighths < 800) {
for (int nrUnits = maxNrUnits; nrUnits >= minNrUnits; --nrUnits) {
int rangeEighths = unitEighths * nrUnits;
if (rangeEighths > maxEighths) {
int ppu = hg/nrUnits;
int dp = hg - nrUnits*ppu;
int m1 = Pretty.metric(minEighths, maxEighths, rangeEighths);
int m2 = -dp;
int m3 = nrUnits; // -(java.lang.Math.abs(avgNrUnits - nrUnits));
int metric = (3*m1 + 2*m2 + m3);
if (metric > bestMetric) {
bestUnitEighths = unitEighths;
bestNrUnits = nrUnits;
bestMetric = metric;
/*
if (this.debug) {
java.lang.System.out.println("***NEW " + bestNrUnits + " @ " + Pretty.udollars(bestUnitEighths) + " => " + Pretty.udollars(bestNrUnits * bestUnitEighths) + " (" + bestMetric + ")" +
"\n m1=" + m1 + ", m2=" + m2 + ", m3=" + m3 + ", (hg=" + hg + ")");
}
*/
}
}
}

if (unitEighths < 8) {
unitEighths *= 2;
} else {
unitEighths += 8;
}
}
if (this.debug) {
java.lang.System.out.println("***UNITS " + bestNrUnits + " @ " + Pretty.udollars(bestUnitEighths) + " => " + Pretty.udollars(bestNrUnits * bestUnitEighths) + " (" + bestMetric + ")");
}

/*
* Paint prices axis.
*/
int halfCharHeight = 4; // KLUDGEy approx, better to use font info
int ws = 4;
int xp0 = xg + wg;
int xp1 = xp0 + ws;
int xp2 = xp1 + 2; // leave a little margin before the chars
int np = bestNrUnits;
int hs = hg/np;
int yp0 = yg;
int yp1 = yp0 + hg - 1;
int yi = yp1;
if (this.debug) {
java.lang.System.out.println("-------.paint_graph_prices_axis(): xp0=" + xp0 + ", yp1=" + yp1 + ", np=" + np);
}
for (int i = 0; i < np+1; ++i, yi -= hs) {
g.drawLine(xp0, yi, xp1, yi);
g.drawString(Pretty.udollars(i * bestUnitEighths).toString(), xp2, yi+halfCharHeight);
}

/*
* Paint the prices themselves (e.g. candlesquotes, etc.).
*/
int diameter = 2;
int radius = diameter/2;
int range = bestNrUnits * bestUnitEighths;
int xi0 = 0; int yi0 = 0;
for (int i = 0; i < this.mktModel.countQuotes(); ++i) {
int price = this.mktModel.getQuote(i).getLastPrice();
int xi1 = this.xmouse + (i*this.imouse);
int yi1 = yp1 - (price * hg) / range;
java.awt.Color oldColor = g.getColor();
if (i == this.selectedQuoteNr) {
g.setColor(java.awt.Color.yellow);
}
if (this.styleNr == MktView.BAR_GRAPH) {
int lo = (this.mktModel.getQuote(i).getHigh());
int hi = (this.mktModel.getQuote(i).getLow());
int yy1 = yp1 - (lo * hg) / range;
int yy2 = yp1 - (hi * hg) / range;

g.drawLine(xi1, yy1, xi1, yy2);
g.drawLine(xi1-diameter, yi1, xi1+diameter, yi1);
} else if (this.styleNr == MktView.LINE_GRAPH) {
g.drawOval(xi1-radius, yi1-radius, diameter, diameter);
}
if (i == this.selectedQuoteNr) {
g.setColor(oldColor);
}
if (this.styleNr == MktView.LINE_GRAPH && i>0) {
g.drawLine(xi0, yi0, xi1, yi1);
}
xi0 = xi1; yi0 = yi1;
}

return bestNrUnits;
}

private int paint_graph_guidelines(java.awt.Graphics g, int xg, int yg, int wg, int hg, int tb2, int nu) throws java.lang.Exception {
/*
* Using trial and error approach, compute how
* many guidelines will make the prettiest graph.
*/
int bestNrGuidelines = 1;
for (int nrGuidelines = 1; nrGuidelines < nu; ++nrGuidelines) {
if (nu % nrGuidelines == 0) {
bestNrGuidelines = nrGuidelines;
}
}

int ppu = hg/nu;
int ppg = ppu * (nu/bestNrGuidelines);
int yi = yg + hg - ppg;
if (this.debug) {
java.lang.System.out.println("***GUIDES " + bestNrGuidelines + "(units=" + nu + ", ppg=" + ppg + ")");
}
for (int i = 1; i < bestNrGuidelines; ++i) {
g.drawLine(xg+tb2, yi, xg+wg-2*tb2, yi);
yi -= ppg;
}
return bestNrGuidelines;
}

/*
* Paints the volumes. Note: reads the clickable region.
*/
private void paint_graph_volumes(java.awt.Graphics g, int xg, int yg, int wg, int hg, int tb, int wv, int nv) throws java.lang.Exception {
java.awt.Color oldColor = g.getColor();
g.setColor(java.awt.Color.magenta);
int strWidth = 10; // KLUDGEy approximation
int xv0 = xg;
int yv0 = yg;
int ws = 4;
int xv1 = xv0 - ws;
int xv2 = xv1 - strWidth;
int hs = hg/nv;
int yv2 = yv0 + hg;
int yv1 = yv2 - tb;
int yi = yv2;
if (this.debug) {
java.lang.System.out.println("-------.paint_graph_volumes_axis(): xv0=" + xv0 + ", yv2=" + yv2 + ", nv=" + nv);
}
/*
* Mark the volumes axis
*/
for (int i = 0; i <= nv; ++i, yi -= hs) {
if (i != nv) {
g.drawLine(xv0, yi, xv1, yi);
}
if (i != 0) {
g.drawString(new Integer(i).toString(), xv2, yi);
}
}

/*
* Compute the power of 10 used on the volumes axis.
* (The lower the better).
*/
int pow = 1;
int halfNv = nv/2;
int maxv = this.mktModel.maxVolume();
for (int i = 10000000; i > 1; i /= 10) {
if (i*halfNv > maxv) {
pow = i;
}
}

/*
* Paint the volumes.
*/
int xi = this.xmouse;
for (int i = 0; i <= this.mktModel.countQuotes(); ++i) {
if (i == this.selectedQuoteNr) {
g.setColor(java.awt.Color.yellow);
}
int h = (this.mktModel.getQuote(i).getVolume()*hs)/pow;
g.drawLine(xi, yv1, xi, yv1-h);
if (i == this.selectedQuoteNr) {
g.setColor(java.awt.Color.magenta);
}
xi += this.imouse;
}

g.setColor(oldColor);
}

private void setClickableDates(int x, int i, int y0, int y1) {
this.xmouse = x;
this.imouse = i;
this.ymouse0 = y0;
this.ymouse1 = y1;
}

private void setQuote(int quoteNr) {
QuoteModel quoteModel;
try {
quoteModel = this.mktModel.getQuote(quoteNr);
// if we were able to get the quote, we must have a valid quote nr
this.quoteView.setQuote(quoteModel);
this.selectedQuoteNr = quoteNr;
} catch (java.lang.Exception ex) {
this.quoteView.setQuote(null);
}
}

private void setSymbol() {
String stockName = this.textField.getText();
stockName = stockName.trim();
if (stockName.equals("")) {
stockName = this.mktModel.getName();
}
if (this.debug) {
java.lang.System.out.println("MktView.setSymbol(): stockName=" + stockName + "\n");
}
this.textField.setText(stockName);
this.mktModel.setSymbol(stockName);
this.setQuote(this.selectedQuoteNr);
if (this.debug) {
java.lang.System.out.println("MktView.setSymbol(): DONE\n");
}
}
}
