import java.util.ArrayList;
import java.awt.*;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.image.BufferedImage;
import javax.swing.*;

/**
 * Overview:<br>
 * The purpose of this class is to create a JFrame displaying a 2D graphical 
 * representation of given sets of coordinates within a 3D hexagonal lattice. 
 * No representation of the lattice itself is maintained; therefore, the size 
 * and location of the lattice is arbitrary. The display automatically scales 
 * to fit whatever coordinates are given. The 2D representation used is a 
 * parallel projection straight down the z-axis. Increasing y-coordinates are 
 * displayed higher on the screen.<p>
 * 
 * Usage:<br>
 * <ol>
 * <li>The constructor creates and displays the resizable JFrame. </li>
 * <li>The user of this class then calls the method 
 *     <CODE>addCells(ArrayList&lt;Point3i&gt; idxList)</CODE> to specify 
 *     lattice coordinates.</li>
 * </ol>  
 * 
 * @author  Joel Castellanos
 * @version 1.0,  Created: January 24, 2009.
*/
public class SnowflakeDraw extends JFrame
{ private static final long serialVersionUID = 1L;
  private static final String THISFILE = "SnowflakeDraw::";
  private DrawPanel drawPanel;
  
  private BufferedImage image_buf;
  private Graphics2D graphics_buf;
  
  //private static final Color BACKGROUND1 = new Color(110,132,241);
  //private static final Color BACKGROUND2 = new Color(159,186,255);
  private static final Color BACKGROUND1 = new Color(120,40,185);
  private static final Color BACKGROUND2 = new Color(0,0,80);
  private static GradientPaint backgroundGradient;
  
  
  
  private static final Color[] COLOR_EDGE = 
  { Color.WHITE,              new Color(235, 255, 255), new Color(220, 255, 255),
    new Color(200, 255, 255), new Color(170, 255, 255), new Color(140, 255, 255)
  };
  
  private static final Color[] COLOR_RIDGE =
  { new Color(145,145,245), new Color(155,145,245), new Color(165,150,250),
    new Color(180,150,255), new Color(190,150,255), new Color(200,150,255),
    new Color(215,150,255), new Color(230,155,255), new Color(250,160,255),
  };
  
  
  private static final double MIN_SLOPE = 1.0/5.0;
  private static final double MAX_SLOPE = 5.0;
  private static final double SLOPE_RANGE = MAX_SLOPE - MIN_SLOPE;
  private static final Color[] COLOR_TROUGH =
  { new Color(0, 0, 240), new Color(0, 0, 220), new Color(0, 0, 200), 
    new Color(0, 0, 180), new Color(0, 0, 170), new Color(0, 0, 150), 
    new Color(0, 0, 130), new Color(0, 0, 110), 
    new Color(0, 0, 80),Color.BLACK
  };
  
  private static final Color[] COLOR_SIDE =
  { new Color(113, 137, 255), new Color(120, 145, 255), 
    new Color(129, 154, 255), new Color(138, 163, 255),
    new Color(145, 171, 255), new Color(153, 178, 255),
    new Color(162, 187, 255),
  };
  
  
  //private static final Color[] COLOR_THICKNESS =
  //{ new Color(110,132,241), new Color(105, 127, 236), new Color(100, 125, 225), 
  //  new Color(100,120,215), new Color(100, 120, 195), new Color(100, 120, 180),
  //  new Color(100,120,165), new Color(100, 120, 150), new Color(100, 110, 145)
  //};
  
  private static final Color[] COLOR_THICKNESS =
  { new Color(255,255,255), new Color(240,240,255),
    new Color(235,235,255), new Color(225,225,255), new Color(215,215,255), 
    new Color(205,205,255), new Color(195,195,255), new Color(185,185,255),
    new Color(175,175,255), new Color(165,165,255), new Color(155,155,255),
    new Color(145,145,255), new Color(135,135,255), new Color(125,125,255),
    new Color(115,115,255), new Color(105,105,255), new Color(90, 90, 255),
    new Color(75, 75 ,255), new Color(60, 60, 255), new Color(40, 40, 255),
    new Color(20, 20 ,255), new Color(0,  0,  255), new Color(0,  0,  235),
    new Color(0,  0  ,220), new Color(0,  0,  205), new Color(0,  0,  190),
    new Color(0,  0  ,175), new Color(0,  0,  160), new Color(0,  0,  145),
    new Color(0,  0  ,120), new Color(0,  0,  90 ), new Color(0,  0,  75),
  };
  
  private static final int NORTH = 0;
  private static final int NORTHEAST = 1;
  private static final int SOUTHEAST = 2;
  private static final int SOUTH = 3;
  private static final int SOUTHWEST = 4;
  private static final int NORTHWEST = 5;
  
  
  
  private static final float SIN60 = (float) Math.sin(2*Math.PI/6);
  private static final float COS60 = (float) Math.cos(2*Math.PI/6);
 
  private float drawScale;
  private int drawOffsetX, drawOffsetY;
  private int minX, minY, maxX, maxY, minZ, maxZ;
  private int drawWidth, drawHeight;
  private int generationCount, totalCellCount;
  
  public static final short GRID_SIZE = 504;
  private short[][] grid = new short[GRID_SIZE][GRID_SIZE];
  
  
  /** 
   * Creates and displays the resizable JFrame.<p>
   * Closing the frame terminates the currently running Java Virtual Machine.
   * 
   * @param frameWidth  initial outside width of JFrame in pixels.
   * @param framwHeight initial outside height of JFrame in pixels.
   */
  public SnowflakeDraw(int frameWidth, int framwHeight)
  { 
    this.setTitle("Digital Snowflake");
    this.setBounds(0,0,frameWidth, framwHeight);
    this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

    //The frame must be visible before getInsets() is called.
    this.setVisible(true);
    Insets inset = this.getInsets();
    Dimension d = this.getSize();
    drawWidth = d.width - inset.left - inset.right;
    drawHeight = d.height - inset.top - inset.bottom;

    drawPanel = new DrawPanel();
    this.add(drawPanel);
    frameWasResized();
    clearAll();
    
    this.addComponentListener ( new ComponentAdapter()
    {  public void componentResized(ComponentEvent e) 
       { frameWasResized(); 
       }
    });
    
  }
  
  /**
   * Except for the frame's size and location, 
   * this method resets the frame to its initial state.<p>
   * <ul>
   * <li> Erases all graphics in the frame.</li>
   * <li> Empties the private instance variable <CODE>ArrayList&lt;Cell&gt; 
   *      cellList</CODE> of all cells accumulated from any previous calls to 
   *      the public method <CODE>addCells(ArrayList&lt;Point3i&gt; idxList)
   *      </CODE>.</li>
   * 
   * <li> Resets the private instance variable <CODE>int generationCount</CODE> 
   *      to zero. This variable is used to assign different colors to cells 
   *      added in successive calls to <CODE>addCells(ArrayList&lt;Point3i&gt; 
   *      idxList)</CODE>.</li>
   * </ul>
   */
  public void clearAll() 
  { graphics_buf.setColor(BACKGROUND1);
    graphics_buf.fillRect(0,0,drawWidth, drawHeight);
    generationCount = 0;
    totalCellCount = 0;
    minX = Integer.MAX_VALUE;
    minY = Integer.MAX_VALUE; 
    minZ = Integer.MAX_VALUE;
    maxX = Integer.MIN_VALUE; 
    maxY = Integer.MIN_VALUE;
    maxZ = Integer.MIN_VALUE;
    
    drawPanel.repaint();
    for (int i=0; i<GRID_SIZE; i++)
    { for (int k=0; k<GRID_SIZE; k++)
      { grid[i][k] = 0;
      }
    }
  }
  
  /**
   * This method appends the given cells to a cumulative cell list. 
   * Then all of the cells in the list are rescaled to fit the frame and 
   * redrawn.<p>
   * 
   * Each call to this method increments a counter. This counter is used
   * to color code cells according to the generation in which they were 
   * added. All cells added after the 6th call to this method are rendered
   * in the same color.<p>
   * 
   * @param idxList Each <code>Point3i</code> in <code>idxList</code> 
   * contains the 3D hex grid-coordinates of a cell.<br>
   * The range of these coordinates is irrelevant since the image is 
   * centered in the display frame. For example, rendering of the cells:
   * {(0,0,0), (1, 0, 0)} will be identical to rendering of the cells:
   * {(-50,-75,100), (-51,-75,100)}.<br>
   * <code>idxList</code> should only contain new cells.
   * This method sill not change the values pointed to by the parameters
   */
  public void addCells(ArrayList<Point3i> idxList)
  { 
    //Find range of cell locations for image centering and scaling.
    for (int i = 0; i < idxList.size(); i++)
    { Point3i p = idxList.get(i);
      int x = p.x+2;
      int y = p.y+2;
      
      if (p.z > grid[x][y]) grid[x][y] = (short)p.z;
      if (x < minX) minX = x;
      if (y < minY) minY = y;
      if (x > maxX) maxX= x;
      if (y > maxY) maxY = y;
      if (p.z < minZ) minZ = p.z;
      if (p.z > maxZ) maxZ = p.z;

    }
    
    totalCellCount += idxList.size();
    System.out.println("Generation="+generationCount + ", cells=" + 
                        totalCellCount + ", new cells="+ idxList.size() +
                        ", maxZ="+maxZ + ", maxX="+maxX + ", maxY="+maxY);
    
    calculateScale();
  
    drawCells();
    generationCount++;
  }
  
  
  private void frameWasResized()
  { Dimension d = this.getSize();
    Insets inset = this.getInsets();
    drawWidth = d.width - inset.left - inset.right;
    drawHeight = d.height - inset.top - inset.bottom;
    
    image_buf = new BufferedImage (drawWidth, drawHeight, 
        BufferedImage.TYPE_INT_RGB);
    graphics_buf = image_buf.createGraphics();
    
    drawPanel.setBounds(0, 0, drawWidth, drawHeight); 
    
    backgroundGradient = 
      new GradientPaint(0, 0, BACKGROUND1, drawWidth, drawHeight, BACKGROUND2); 
    
    calculateScale();
    drawCells();
    
  }
  
  
  
  public static int nextNorthEastY(int x, int y)
  { if (x % 2 == 0) return y;
    return y-1;
  }
  
  public static int nextSouthEastY(int x, int y)
  { if (x % 2 == 0) return y+1;
    return y;
  }
  
  
  public static int nextSouthWestY(int x, int y)
  { if (x % 2 == 0) return y+1;
    return y;
  }
  
  public static int nextNorthWestY(int x, int y)
  { if (x % 2 == 0) return y;
    return y-1;
  }
  
 
 
  private void setSurfaceGeom(int x, int y, SurfaceGeometry geom)
  { for (int dir = 0; dir<6; dir++)
    { int x2 = x;
      int y2 = y;
      int z = grid[x][y];
      int d = 0;
      boolean directionDone = false;
      geom.edgeDistance[dir] = 0; //0=edge not reached.
      geom.slope[dir] = 0;
      while (d<6 && !directionDone)
      { d++;
        if (dir == NORTH) y2--;
        else if (dir == NORTHEAST) 
        { if (x2 % 2 != 0) y2--;
          x2++;
        }
        else if (dir == SOUTHEAST) 
        { if (x2 % 2 == 0) y2++;
          x2++;
        }
        else if (dir == SOUTH) y2++;
        else if (dir == SOUTHWEST) 
        { if (x2 % 2 == 0) y2++;
          x2--;
        }
        else if (dir == NORTHWEST) 
        { if (x2 % 2 != 0) y2--;
          x2--;
        }    
      
        int z2 = grid[x2][y2];
        if (z2 == 0) 
        { geom.edgeDistance[dir] = d;
          directionDone = true;
        }
        else if (geom.slope[dir]*(z2 - z) < 0) 
        { //The slope has changed direction.
          directionDone = true;
        }
        else 
        { geom.slope[dir] = ((double)(z2 - z))/(double)d;
        }
      }
    }
  }
  
  
 

  
  
  private Color getColor(int x, int y, SurfaceGeometry geom)
  { 
    //Colors are applied in the following order:
    //  If a cell is in a trough, then use a trough color.
    //  If a cell is near an edge, then use an edge color.
    //  If a cell is on a ridge, then use a ridge color.
    //  If a cell is on a slide slope, then use a side color.
    //  If a cell is in a flat area, then use a thickness color.
    
    //System.out.print("["+x+", "+y+"]: ");
    //if (x==250 && y==252)
    //{ geom.slope[0]=0.0;
    //}
    
    /*
    setSurfaceGeom(x, y, geom);
    
    //=========================================================================
    //Is cell in a trough?
    
    
    double maxSlope = 0;
    if (geom.slope[NORTH] > 0 && geom.slope[SOUTH] > 0)
    { double tmpSlope = (geom.slope[NORTH] + geom.slope[SOUTH])/2.0;
      if (tmpSlope > maxSlope) maxSlope = tmpSlope;
    }
    
    if (geom.slope[NORTHEAST] > 0 && geom.slope[SOUTHWEST] > 0)
    { double tmpSlope = (geom.slope[NORTHEAST] + geom.slope[SOUTHWEST])/2.0;
      if (tmpSlope > maxSlope) maxSlope = tmpSlope;
    }
    
    if (geom.slope[NORTHWEST] > 0 && geom.slope[SOUTHEAST] > 0)
    { double tmpSlope = (geom.slope[NORTHWEST] + geom.slope[SOUTHEAST])/2.0;
      if (tmpSlope > maxSlope) maxSlope = tmpSlope;
    }
    
    if (maxSlope > 0)
    { int idx = (int)(COLOR_TROUGH.length*(maxSlope - MIN_SLOPE)/SLOPE_RANGE);
      if (idx < 0) idx = 0;
      if (idx >= COLOR_TROUGH.length) idx = COLOR_TROUGH.length -1;
      //System.out.println("trough idx = "+idx);
      return COLOR_TROUGH[idx];
    }
    
    //for (int i=0; i<6; i++)
    //{ System.out.print(geom.slope[i]+", ");
    //}
    //System.out.println("-- End of line.");
  
     
    
    //=========================================================================
    //Is cell near an edge?
    int edgeDistance = 77;
    for (int dir=0; dir<6; dir++)
    { if (geom.edgeDistance[dir] > 0) 
      { if (geom.edgeDistance[dir] < edgeDistance) edgeDistance = geom.edgeDistance[dir];
      }
    }
    //if (edgeDistance < 77)
    if (edgeDistance == 1)
    { int idx = edgeDistance-1;
      if (idx < 0) idx = 0;
      if (idx >= COLOR_EDGE.length) idx = COLOR_EDGE.length-1;
      System.out.println("edge idx = "+idx);
      return COLOR_EDGE[idx];
      //return COLOR_EDGE[1];
    }
    
    
    //=========================================================================
    //Is cell on ridge?
    maxSlope = 0;
    if (geom.slope[NORTH] < 0 && geom.slope[SOUTH] < 0)
    { double tmpSlope = -(geom.slope[NORTH] + geom.slope[SOUTH])/2.0;
      if (tmpSlope > maxSlope) maxSlope = tmpSlope;
    }
    
    if (geom.slope[NORTHEAST] < 0 && geom.slope[SOUTHWEST] < 0)
    { double tmpSlope = -(geom.slope[NORTHEAST] + geom.slope[SOUTHWEST])/2.0;
      if (tmpSlope > maxSlope) maxSlope = tmpSlope;
    }
    
    if (geom.slope[NORTHWEST] < 0 && geom.slope[SOUTHEAST] < 0)
    { double tmpSlope = -(geom.slope[NORTHWEST] + geom.slope[SOUTHEAST])/2.0;
      if (tmpSlope > maxSlope) maxSlope = tmpSlope;
    }
    
    if (maxSlope > 0)
    { int idx = (int)(COLOR_RIDGE.length*(maxSlope - MIN_SLOPE)/SLOPE_RANGE);
      if (idx < 0) idx = 0;
      if (idx >= COLOR_RIDGE.length) idx = COLOR_RIDGE.length -1;
      //System.out.println("ridge idx = "+idx);
      return COLOR_RIDGE[idx];
    }
   
    
   
    //=========================================================================
    //Is cell on side slope?
    maxSlope = 0;
    if (geom.slope[NORTH] * geom.slope[SOUTH] < 0)
    { double tmpSlope = (Math.abs(geom.slope[NORTH]) + Math.abs(geom.slope[SOUTH]))/2.0;
      if (tmpSlope > maxSlope) maxSlope = tmpSlope;
    }
    
    if (geom.slope[NORTHEAST] < 0 && geom.slope[SOUTHWEST] < 0)
    { double tmpSlope = (Math.abs(geom.slope[NORTHEAST]) + Math.abs(geom.slope[SOUTHWEST]))/2.0;
      if (tmpSlope > maxSlope) maxSlope = tmpSlope;
    }
    
    if (geom.slope[NORTHWEST] < 0 && geom.slope[SOUTHEAST] < 0)
    { double tmpSlope = (Math.abs(geom.slope[NORTHWEST]) + Math.abs(geom.slope[SOUTHEAST]))/2.0;
      if (tmpSlope > maxSlope) maxSlope = tmpSlope;
    }
    
    if (maxSlope > 0)
    { int idx = (int)(COLOR_SIDE.length*(maxSlope - MIN_SLOPE)/SLOPE_RANGE);
      if (idx < 0) idx = 0;
      if (idx >= COLOR_SIDE.length) idx = COLOR_SIDE.length -1;
      
      //System.out.println("side idx = "+idx);
      return COLOR_SIDE[idx];
    }
    
    */
    
    int tmp = Math.max(maxZ, minZ+COLOR_THICKNESS.length);
    int z = grid[x][y];
    int idx = (COLOR_THICKNESS.length*(z-minZ))/(tmp-minZ);
    if (idx < 0) idx = 0;
    if (idx >= COLOR_THICKNESS.length) idx = COLOR_THICKNESS.length -1;
    return COLOR_THICKNESS[idx];
   

 
  }
  
  
  private void drawCells() 
  { 
    //graphics_buf.setColor(BACKGROUND1);
    graphics_buf.setPaint(backgroundGradient);
    //g2d.fill(getCircle());
    //g2d.setPaint(Color.black);
    //g2d.draw(getCircle());
    
    
    
    
    graphics_buf.fillRect(0,0,drawWidth, drawHeight);
    
    SurfaceGeometry geom = new SurfaceGeometry();
    
    for (int xi=0; xi<GRID_SIZE; xi++)
    { for (int yi=0; yi<GRID_SIZE; yi++)
      { int zi = grid[xi][yi];
        if (zi > 0)
        { graphics_buf.setColor(getColor(xi, yi, geom));
    
          float x = gridToRealX(xi);
          float y = gridToRealY(xi, yi);
    
          int[] xpoints = new int[6];
          int[] ypoints = new int[6];
    
          for (int k=0; k<6; k++)
          { xpoints[k]=(int)(drawScale*(x+HexagonalPrism.vertex[k].x)) +drawOffsetX;
            ypoints[k]=drawHeight-((int)(drawScale*(y+HexagonalPrism.vertex[k].y)) +drawOffsetY);
            //ypoints[k]=(int)(drawScale*(y+HexagonalPrism.vertex[k].y)) +drawOffsetY;
          }
    
          graphics_buf.fillPolygon(xpoints, ypoints, 6); 
        }
      }
    }
    drawPanel.repaint();
  }
  
  
  
  
  private void calculateScale()
  { final int BLANK_BORDER_SIZE = 10; //pixels
    float xRange = gridToRealX(maxX) - gridToRealX(minX);
    float yRange = gridToRealY(0, maxY) - gridToRealY(1, minY);
    
    xRange += HexagonalPrism.SIZE_X;
    yRange += 2*HexagonalPrism.SIZE_Y;
    
    float drawScaleX = (drawWidth - BLANK_BORDER_SIZE)/xRange;
    float drawScaleY = (drawHeight - BLANK_BORDER_SIZE)/yRange;
    drawScale = Math.min(drawScaleX, drawScaleY);
    
    int minScreenX = 
      (int)(drawScale*(gridToRealX(minX) - HexagonalPrism.SIZE_X/2.0));
    
    int maxScreenX = 
      (int)(drawScale*(gridToRealX(maxX) + HexagonalPrism.SIZE_X/2.0));
    
    int minScreenY = 
      (int)(drawScale*(gridToRealY(1, minY)));
    
    int maxScreenY = 
      (int)(drawScale*(gridToRealY(0, maxY) - HexagonalPrism.SIZE_Y/2.0));
    
    //System.out.println(THISFILE + "HexagonalPrism.SIZE_Y/2.0="+HexagonalPrism.SIZE_Y/2.0 +
    //                    ", minScreenY="+minScreenY + ", maxScreenY="+maxScreenY);
    
    
    int extraSpaceX = drawWidth  - (maxScreenX - minScreenX);
    int extraSpaceY = drawHeight - (maxScreenY - minScreenY); 
    
    drawOffsetX = - minScreenX + extraSpaceX/2;
    drawOffsetY = - minScreenY + extraSpaceY/2;
    
    //System.out.println(THISFILE + "scale="+drawScale +
    //                   ", X-offset="+drawOffsetX + ", Y-offset="+drawOffsetY);
    
  }
  
  
 
  private float gridToRealX(int ix)
  { return ix*(1.0f+COS60);
  }
  
  private float gridToRealY(int ix, int iy)
  { return iy*(2.0f*SIN60) - (ix % 2)*SIN60;
  }
  
  private float gridToRealZ(int iz)
  { return iz*1.0f;
  }
  
  class SurfaceGeometry
  { double[] slope = new double[6];
    int[] edgeDistance = new int[6];
  }


  class DrawPanel extends JPanel
  { private static final long serialVersionUID = 1L;
	  
	  public void paintComponent (Graphics g) 
	  { super.paintComponent(g);
		  g.drawImage(image_buf,0,0,null);
	  }
  }
  
}