import java.awt.*;
import java.awt.image.*;
import java.awt.event.*;


// Grid - panel to display grid for maze routing
//
// Layout:
//		+--------------------------+
//		| Layer 1                  |
//		+--------------------------+
//		|   |   |  |   |   |   |   |
//		+--------------------------+
//		|   |   |  |   |   |   |   |
//		+--------------------------+
//		| Layer 2                  |
//		+--------------------------+
//		|   |   |  |   |   |   |   |
//		+--------------------------+
//		|   |   |  |   |   |   |   |
//		+--------------------------+
//			(additional layers)
//		+--------------------------+
//		| Message Area             |
//		+--------------------------+

class Grid extends Panel {

  private String msg = null;

  private boolean displayParallelMode = false;

  public void setParallelExpand() { displayParallelMode = true; }
  public void setSerialExpand() { displayParallelMode = false; }

  Image myOffScreenImage = null;
  Graphics myOffScreenGraphics = null;

  public Grid(int w, int h, int d) {
    gridArray = new GridPoint[w][h][d];
    for (int i = 0; i < w; i++ )
      for (int j = 0; j < h; j++)
        for (int k = 0; k < d; k++)
          gridArray[i][j][k] = new GridPoint(i,j,k);

    GridPoint.myGrid = this;
    setSize(pixelWidth(), pixelHeight());
    addMouseListener(new MouseAdapter()
    { public void mouseClicked(MouseEvent m) {
        System.out.println("mouseEvent: " + m);
        handleMouseClick(m.getX(), m.getY());
      }
    } );

  }

  public void setMessage(String s) {
    msg = s;
    redrawGrid();
  }

  public void flash(GridPoint gp) throws InterruptedException {
    for (int i=0;i<3;i++) {
      gp.highlight(true);
      redrawGrid();
      gridDelay(4);
      gp.highlight(false);
      redrawGrid();
      gridDelay(4);
    }
  }

  public void redrawGrid() {
    if (myOffScreenImage == null) { // this doesn't work in constructor
      myOffScreenImage = createImage(getSize().width, getSize().height );
      myOffScreenGraphics = myOffScreenImage.getGraphics();
     }
    repaint();
  }

  public int width() { return gridArray.length; }

  public int height() { return gridArray[0].length; }

  public int depth() { return gridArray[0][0].length; }

  public int pixelWidth() { return width() * Grid.GRIDSIZE; }

  public int pixelHeight() { return depth() * (height() + 1) * Grid.GRIDSIZE; }

  public static int calculateCols(int pixelWidth) { return (pixelWidth - 2) / GRIDSIZE; }

  public static int calculateRows(int pixelHeight, int nLayers) {
    int rawCols = ((pixelHeight - 30) / GRIDSIZE) - 2; // adjust for msg, clr btn
    return rawCols / nLayers - 1; // adjust  -1 for layer labels
  }

  public GridPoint gridPointAt(int x, int y, int z) {
    if (x < 0 || x >= width() ) return null;
    if (y < 0 || y >= height() ) return null;
    if (z < 0 || z >= depth() ) return null;
    return gridArray[x][y][z];
  }

  public void reset() {
    for (int i = 0; i < width(); i++ )
      for (int j = 0; j < height(); j++)
        for (int k = 0; k < depth(); k++) {
          GridPoint gp = gridPointAt(i,j,k);
          if (!gp.isObstacle()) gp.reset();
        }
  }

  public void clear() {
    for (int i = 0; i < width(); i++ )
      for (int j = 0; j < height(); j++)
        for (int k = 0; k < depth(); k++) {
          GridPoint gp = gridPointAt(i,j,k);
          gp.reset();
        }
  }
    private class GridLink {
      public GridLink(GridPoint gp) {
        wot = gp;
        next = null;
      }
      private GridLink next;
      private GridPoint wot;
    }

  private GridLink gridPointHead;
  private GridLink gridPointTail;

  void printGridPointQueue() { // for debugging - package visible
    for (GridLink gl = gridPointHead; gl != null; gl = gl.next) {
      System.out.print(gl.wot + " ");
    }
    System.out.println();
  }

  public void enqueueGridPoint(GridPoint gp) throws InterruptedException {
    // if (gp.isEnqueued()) return;  // already there!
    if (gridPointHead == null) gridPointHead = gridPointTail = new GridLink(gp);
    else {
      GridLink gl = new GridLink(gp);
      gridPointTail.next = gl;
      gridPointTail = gl;
    }
    gp.setEnqueued(true);
   if (!displayParallelMode) {
      redrawGrid();
      gridDelay();
    }
  }

  public GridPoint dequeueGridPoint() {
    if (gridPointHead == null) return null;
    else {
      GridPoint gp = gridPointHead.wot;
      gridPointHead = gridPointHead.next;
      gp.setEnqueued(false);
      // debug
//      System.out.println("GridPoint.dequeuePoint - " + gp);
      return gp;
    }
  }

  public void clearQueue() {
    gridPointHead = null;
  }

  private static final int DELAY = 100; // 10ms = 0.1s

  public static void gridDelay(int d) throws InterruptedException {
      Thread.sleep(d * DELAY);
  }

  public static void gridDelay() throws InterruptedException {
    gridDelay(1);
  }

  public int expansion() throws InterruptedException {
    GridPoint gp;
    int actualLength;
    int curVal = 0;
    setMessage("Expansion phase");
    gridDelay(3);
    if (src != null && tgt != null ) {
      src.initExpand();
      if ((actualLength = src.expand()) > 0) {
        clearQueue();
        return actualLength; // found it right away!
      }
      while ((gp = dequeueGridPoint()) != null) {
        if (displayParallelMode && (gp.getVal() > curVal)) {
          curVal = gp.getVal();
          redrawGrid();
          gridDelay(2);
        }
        if ((actualLength = gp.expand()) > 0) {
          clearQueue();
          return actualLength;  // found it!
        }
      }
    }
    return -1;
  }

  public void traceBack() throws InterruptedException {
    // start at target, then work back
    GridPoint current = tgt;
    while (!current.isSource()) {
      GridPoint next;
      int curval = current.getVal();
      setMessage("Traceback: distance = " + curval);
      current.setRouted();
      redrawGrid();
      gridDelay(3);
      next = current.westNeighbor();
      if (next != null && !next.isObstacle() && next.getVal() < curval) {
        current = next;
        continue;
      }
      next = current.eastNeighbor();
      if (next != null && !next.isObstacle() && next.getVal() < curval) {
        current = next;
        continue;
      }
      next = current.southNeighbor();
      if (next != null && !next.isObstacle() && next.getVal() < curval) {
        current = next;
        continue;
      }
      next = current.northNeighbor();
      if (next != null && !next.isObstacle() && next.getVal() < curval) {
        current = next;
        continue;
      }
      next = current.upNeighbor();
      if (next != null && !next.isObstacle() && next.getVal() < curval) {
        current = next;
        continue;
      }
      next = current.downNeighbor();
      if (next != null && !next.isObstacle() && next.getVal() < curval) {
        current = next;
        continue;
      }
      System.out.println("AWK! can't trace back! current= " + current);
      break;
    }
    if (current.isSource()) {
      setMessage("Traceback complete");
      flash(current);
      current.setRouted();
    } else System.out.println("Warning: traceBack failed!");
  }

   // the following are used in drawing into the grid panel and are package visible

  static final int GRIDSIZE = 19;
  static final int CHARXOFFSET = 6;
  static final int CHARYOFFSET = 14;

  int gridPanelX(int i, int j, int k) { return i * GRIDSIZE;}

  int gridPanelY(int i, int j, int k) {
    return(k * (GRIDSIZE * (height() + 1) )) + ((j+1) * GRIDSIZE);
  }

  int gridLyrY(int layer) { return (GRIDSIZE * layer * (height()+1) + CHARYOFFSET); }

  int gridMsgY() { return (GRIDSIZE * depth() * (height() + 1)) + CHARYOFFSET; }

  private GridPoint clickedPoint = null;
  private boolean clearPending = false;

  public synchronized void handleMouseClick(int x, int y) {
    clickedPoint = mouseToGridPoint(x, y);
    notifyAll();
    System.out.println("handleMouseClick: " + clickedPoint);
  }

  public synchronized void requestClear() {
    clearPending = true;
    notifyAll();
  }

  GridPoint mouseToGridPoint(int x, int y) {
    int i, j, k;
    i = x / GRIDSIZE;
    for (k = 0; k < depth(); k++) {
      if (y >= gridPanelY(0,0,k) && y <= gridPanelY(0,0,k+1)-GRIDSIZE) {
        j = (y % ((height() + 1) * GRIDSIZE) / GRIDSIZE) - 1;
        //System.out.println("mouseToGridPoint: found GridPoint at ("
         //    + i + "," + j + "," + k + ") " + gridPointAt(i,j,k));
        if (i >= 0 && i < width() && j >= 0 && j < height() && k >= 0 && k < depth())
          return gridPointAt(i,j,k);
        else return null;
      }
    }
    return null;
  }


  private static final int WAITFORSRC = 0;
  private static final int WAITFORTGT = 1;

  private synchronized void waitForInput() throws InterruptedException {
    while (!clearPending && clickedPoint == null) wait();
  }

  public void run() throws InterruptedException {
    clear();
    int state = WAITFORSRC;
    setMessage("Click on Source");
    while (true) {
      waitForInput();
      if (state == WAITFORSRC) {
        if (clearPending) {
          clear();
          setMessage("Grid cleared!");
          gridDelay(5);
          setMessage("Click on Source");
          clearPending = false;
        } else if (clickedPoint != null) {
          if (!clickedPoint.isRouted()) {
            setSource(clickedPoint);
            System.out.println("Setting source: " + clickedPoint);
            state = WAITFORTGT;
            setMessage("Click on Target");
          } else {
            setMessage("Already routed!");
            gridDelay(5);
            setMessage("Click on Source");
          }
          clickedPoint = null;
        }
      } else if (state == WAITFORTGT) {
        if (clickedPoint != null) {
          setTarget(clickedPoint);
          System.out.println("Setting target: " + clickedPoint);
          clickedPoint = null;
          setMessage("Ready to Route!");
          gridDelay(5);
          redrawGrid();
          route();
          src = null;
          tgt = null;
          state = WAITFORSRC;
          setMessage("Click on Source");
          redrawGrid();
        }
        clearPending = false;
      }
    }
  }

  public void paint(Graphics g) {
    g.setColor(getBackground());
    g.fillRect(0,0,getSize().width,getSize().height);
    for (int k = 0; k < depth(); k++ ) {
      g.setColor(Color.black);
      g.drawString("Layer " + (k+1), CHARXOFFSET, gridLyrY(k));
      for (int j = 0; j < height(); j++)
        for (int i = 0; i < width(); i++)
          gridArray[i][j][k].paintGridPoint(g);
    }
    if (msg != null) {
      g.setColor(Color.black);
      g.drawString(msg, CHARXOFFSET, gridMsgY());
    }
  }

  public void update(Graphics g) {
    paint(myOffScreenGraphics);
    g.drawImage(myOffScreenImage,0,0,this);
  }

  public void setSource(int x, int y, int z) {
    setSource(gridArray[x][y][z]);
  }

  public void setSource(GridPoint s) {
    src = s;
  }

  public void setTarget(int x, int y, int z) {
    tgt = gridArray[x][y][z];
  }

  public void setTarget(GridPoint t) { tgt = t; }

  public GridPoint getSource() { return src; }

  public GridPoint getTarget() { return tgt; }

  public int route() throws InterruptedException {
    if (src == null || tgt == null) return -1;
    GridPoint.nextRouteColor();
    reset();
    if (src == tgt) {  // trivial case
      src.setRouted();
      return 0;
    } else {
      int actualLength = expansion();
      clearQueue();
      redrawGrid();
      if (actualLength > 0) {
        setMessage("Target Found!");
        flash(tgt);
        traceBack();
      } else setMessage("Target not found!");
      reset();
      redrawGrid();
      return actualLength;
    }
  }

  public int route(GridPoint s, GridPoint t) throws InterruptedException {
    setSource(s);
    setTarget(t);
    return route();
  }

  public int route(int sx, int sy, int sz, int dx, int dy, int dz) throws InterruptedException {
    return route(gridPointAt(sx,sy,sz),gridPointAt(dx,dy,dz));
  }

  GridPoint [][][] gridArray;
  GridPoint src;
  GridPoint tgt;

}
