/** A Java Go board, written to learn Java. */

/* Formatted according to the GNU coding standards, w/ two exceptions:
 *
 * 1. Method calls have no space between the method's name and the
 *    open paren of its argument list.  Writing "foo.method (x, y)"
 *    seems shakier to me than "foo.method(x, y)", because the former
 *    can look like a field reference rather than a method call if
 *    you're just glancing at it quickly.
 *
 *
 * 2. Opening curly braces don't get their own lines when the body
 *    of the block inside will be only one line long.  Thus
 *
 *         public void my_method (int foo) {
 *           return (foo / bar);
 *         }
 *
 *    is okay, but if the body is two or more lines, then this way
 *
 *         public void my_method (int foo)
 *         {
 *           bar = this.bar() / 2;
 *           return (foo / bar);
 *         }
 *
 *    is preferred.
 *
 *
 * Comments about readability are welcome; I'm still suspicious of #1
 * above, but don't like the alternatives much either.
 */


import java.io.*;
import java.awt.*;
import java.applet.*;
import java.util.Vector;
import java.util.Enumeration;

/** The top level widget for the Go board applet. */
public class GoGame extends Applet {
  GoBoard board;
  BoardControls controls;
  GoCanvas canvas;
  
  public void init()
  {
    setLayout(new BorderLayout());
    add("Center", canvas = new GoCanvas(board = new GoBoard(19)));
    add("South", controls = new BoardControls(canvas, board));
  }
  
  public void start() {
    // controls.enable();
  }
  
  public void stop() {
    // controls.disable();
  }
  
  private void handle_action(int x, int y) {
    canvas.handle_action(x, y);
  }

  public boolean handleEvent(Event e)
  {
    // todo: use a switch statement here?
    if (e.id == Event.WINDOW_DESTROY)
      System.exit(0);
    else if (e.id == Event.MOUSE_DOWN)
      handle_action(e.x, e.y);
    return false;
  }
  
  public static void main(String args[])
  {
    GoGame game = new GoGame();

    game.init();
    game.start();
  }
}



/** GoGroup is a group of connected stones (of either color). */
class GoGroup
{
  private int id;
  private int color;
  private Vector points;

  public GoGroup(GoPoint pt)
  {
    points = new Vector();
    points.addElement(pt);
    color = pt.color();
    id = pt.gid();
  }

  public int gid() {
    return id;
  }

  public int color() {
    return color;
  }

  public boolean color_is(int color)
  {
    if (color == this.color)
      return true;
    else
      return false;
  }

  // Add a stone to this group.  todo: sanity check color here?
  public void add_point(GoPoint pt)
  {
    if (points.contains(pt))
      return; // add no point twice

    // else

    points.addElement(pt);
    pt.gid(id);
  }

  // This will only be used in rewinding.
  private void remove_point(GoPoint pt) {
    points.removeElement(pt);
  }

  public Enumeration elements() {
    return points.elements();
  }

  public boolean has_no_members()
  {
    if (this.points.isEmpty())
      return true;
    else
      return false;
  }

  // Make this group have no points.
  public void remove(GoBoard board)
  {
    GoPoint pt;
    Enumeration e = this.elements();
    while (e.hasMoreElements())
      {
        pt = (GoPoint) e.nextElement();

        /* If this point is not in any other groups (i.e., a
         * connection did not recently take place), then mark the
         * point as empty.
         */
        if (pt.gid() == this.gid())
            {
              pt.color(GoPoint.EMPTY);
              pt.gid(GoBoard.impossible_gid);
            }
      }
    board.delete_group(this);
  }

  /* Use this when a move connects two groups.
   * The `this' group dominates, the other group is remove()'d after
   * its points have all transferred to this group.
   */
  public void connect(GoGroup other_group, GoPoint stone, GoBoard board)
  {
    if (other_group == null) // no other group, just add stone here
      this.add_point(stone);
    else if (this.gid() == other_group.gid()) // same group, add stone here
      this.add_point(stone);
    else // two different groups, so absorb the stone and the other group
      {
        Enumeration e = other_group.elements();
        
        this.add_point(stone);

        while (e.hasMoreElements())
          this.add_point ((GoPoint) e.nextElement());

        other_group.remove(board);
      }
  }

  /* This does not detect shared liberties, so might return an
   * overcount, though never when real count is zero.  Oddly enough,
   * this is okay.  We don't need to know how close to death a group
   * is, only that it _is_ dead when it has no more liberties.
   */
  // todo: nevertheless, make this accurate someday!
   public int liberties()
   {
     int count = 0;
     GoPoint pt;
     Enumeration e = this.elements();
     
     while (e.hasMoreElements())
       {
         pt = (GoPoint) e.nextElement();
         count += pt.liberties();
       }
     return count;
   }
}


/** RecordPoint holds only small informations from GoPoint. */
class RecordPoint
{
  public int color, x, y;
  
  public RecordPoint(int color, int x, int y)
  {
    this.color = color;
    this.x     = x;
    this.y     = y;
  }

  public boolean color_is(int color) {
    return (this.color == color);
  }
}


/** GoPoint extends Point and represents a point on the board. */
class GoPoint extends Point
{
  /* Extending Point means we have x, y, Point(x,y),
   * move(x,y), and equals(Obj).  Also translate(dx,dy), but we don't
   * use that yet.
   */

  GoBoard board;
  private int color;
  int group_id;  // Which group is this a member of?

  // All points have a "color" (i.e., state):
  public static final int EMPTY = 0;
  public static final int BLACK = 1;
  public static final int WHITE = 2;

  // Constructors
  public GoPoint(GoBoard board, int x, int y, int color, int gid)
  {
    super(x, y);
    this.board = board;
    this.color(color);
    this.gid(gid);
  }

  public GoPoint(GoBoard board, Point p, int color, int gid)
  {
    super(p.x, p.y);
    this.board = board;
    this.color(color);
    this.gid(gid);
  }

  // Return the other color, wouldn't'cha know?
  public static int other_color(int color) {
    return ((color % 2) + 1);
  }

  public int gid() {
    return group_id;
  }

  public void gid(int id) {
    group_id = id;
  }

  public int color() {
    return color;
  }

  public void color(int color) {
    this.color = color;   // todo: check color's validity?
  }

  public void move(int x, int y) {
    // Do nothing.  Overrided because GoPoints can't be moved right now.
  }

  public boolean color_is(int color) {
    return (this.color == color);
  }

  // Returns the number of liberties for this point.
  public int liberties() {
    return this.board.liberties(this);
  }
}


/** The GoBoard class holds the state of the game; GoCanvas displays it. */
class GoBoard
{
  // Compass directions from a given point.  See get_point(pt, dir).
  static final int UP = 1, DOWN = 2, LEFT = 3, RIGHT = 4;

  // Whose move is it?  Black starts by default.
  // todo: make accessor method for this
  public int to_move = GoPoint.BLACK;

  // Are there handicap stones, and if so where are they?
  private Vector handicaps;   // todo: auto-init to null or not?

  // Every move is a member of a group.
  private Vector moves, board;
  public Vector groups; // todo: will be `private' someday again

  // How big is the board?
  private int size = -1;

  GoPoint last_point = null;
  private int next_gid = 1;
  public static final int impossible_gid = 0;

  // Constructor
  public GoBoard(int size) {
    this.size(size);
  }

  // Returns the size.
  public int size() {
    return size;
  }

  // Sets up new size, new vectors, returns new size.
  public int size(int new_size)
  {
    if (this.size == new_size)
      return this.size();

    // else

    this.size = new_size;
    next_gid = 1;
    groups = new Vector();

    moves = new Vector();
    board = new Vector(this.size());
    board.setSize(this.size());
    for (int i = 0; i < this.size(); i++)
      {
        Vector v = new Vector(this.size());
        v.setSize(this.size());
        for (int j = 0; j < this.size(); j++)
          {
            GoPoint p = new GoPoint (this, i, j, GoPoint.EMPTY, 0);
            v.setElementAt(p, j);
          }
        
        board.setElementAt(v, i);
      }
    
    return this.size();
  }

  public GoPoint last_point() {
    return last_point;
  }

  public GoPoint get_point(Point p)
  {
    Vector v;
    GoPoint pt;
    
    v = (Vector) board.elementAt(p.x);
    pt = (GoPoint) v.elementAt(p.y);

    return pt;
  }

  // Returns a point near p, or null if it would be off the board.
  public GoPoint get_point(Point p, int direction)
  {
    int x, y;

    try
      {
        x = p.x;
        y = p.y;

        switch (direction)
          {
          case GoBoard.UP:
            y--;
            break;
          case GoBoard.DOWN:
            y++;
            break;
          case GoBoard.LEFT:
            x--;
            break;
          case GoBoard.RIGHT:
            x++;
            break;
          default:
            // todo: sanity check direction here?
          }
            

        return get_point(new Point(x, y));
      }
    catch (IndexOutOfBoundsException e)
      {
        return null;
      }
  }

  // todo: was private, perhaps will be again...
  public GoGroup get_group(int gid)
  {
    Enumeration e = groups.elements();
    while (e.hasMoreElements())
      {
        GoGroup g;
        g = (GoGroup) e.nextElement();

        if (g.gid() == gid)
          return g;
      }
    return null;
  }

  // Remove from this board all references to group.
  public void delete_group(GoGroup group)
  {
    if (group == null)
      return;
    // else
    groups.removeElement(group);
  }

  // Returns true if any got removed
  private boolean remove_dead_groups(int color)
  {
    boolean what_happened = false;
    Enumeration e = groups.elements();
    while (e.hasMoreElements())
      {
        GoGroup g;
        g = (GoGroup) e.nextElement();

        if (g.color_is(color) && (g.liberties() == 0))
          {
            g.remove(this);
            what_happened = true;
          }
      }

    return what_happened;
  }

  // Helper for move().
  private boolean do_move(GoPoint pt)
  {
    // We could at most connect to four different groups from here. 
    GoPoint np;
    int up_id = 0, down_id = 0, left_id = 0, right_id = 0;
    GoGroup up = null, down = null, left = null, right = null;

    pt.color(to_move);
    to_move = GoPoint.other_color(to_move);

    // See if there are any groups to connect to.
    if ((np = get_point(pt, UP)) != null)
      if (np.color_is(pt.color()))
        up_id = np.gid();

    if ((np = get_point(pt, DOWN)) != null)
      if (np.color_is(pt.color()))
        down_id = np.gid();

    if ((np = get_point(pt, LEFT)) != null)
      if (np.color_is(pt.color()))
        left_id = np.gid();

    if ((np = get_point(pt, RIGHT)) != null)
      if (np.color_is(pt.color()))
        right_id = np.gid();

    if ((up = get_group(up_id)) != null)
      {
        up.connect(get_group(down_id), pt, this);
        up.connect(get_group(left_id), pt, this);
        up.connect(get_group(right_id), pt, this);
      }
    else if ((down = get_group(down_id)) != null)
      {
        down.connect(get_group(left_id), pt, this);
        down.connect(get_group(right_id), pt, this);
      }
    else if ((left = get_group(left_id)) != null)
      left.connect(get_group(right_id), pt, this);
    else if ((right = get_group(right_id)) != null)
      right.add_point(pt);
    else // make new group, because not connected to any old groups
      {
        pt.gid(next_gid);
        next_gid++;
        GoGroup new_group = new GoGroup(pt);
        groups.addElement(new_group);
      }

    /* The elegant algorithm is: after placing a stone, remove all
     * groups of the other color that have no liberties, then remove
     * all groups of this color that have no liberties.  This handles
     * Ko (Jie) and everything else correctly.  
     *
     * Note that if groups of the color that just moved get deleted,
     * it means the move was illegal and was removed from the board,
     * so we must toggle to_move.
     */
    remove_dead_groups(GoPoint.other_color(pt.color()));

    if (remove_dead_groups(pt.color()))
      {
        to_move = GoPoint.other_color(to_move);
        return false; // because the board has NOT changed
      }
    else
      {
        moves.addElement(new RecordPoint(pt.color(), pt.x, pt.y));
        last_point = pt;
        return true; // because the board has changed
      }
  }

  // Returns true if the board changed.
  public boolean move(Point p)
  {
    GoPoint pt = get_point(p);
    
    // todo: why doesn't do_move check this?
    if (pt.color_is(GoPoint.EMPTY))
      return do_move(pt);
    else
      return false;
  }

  public int liberties(Point p)
  {
    GoPoint pt;
    int count = 0;

    if ((pt = get_point(p, GoBoard.UP)) != null)
      if (pt.color_is(GoPoint.EMPTY))
        count++;
             
    if ((pt = get_point(p, GoBoard.DOWN)) != null)
      if (pt.color_is(GoPoint.EMPTY))
        count++;
             
    if ((pt = get_point(p, GoBoard.LEFT)) != null)
      if (pt.color_is(GoPoint.EMPTY))
        count++;
             
    if ((pt = get_point(p, GoBoard.RIGHT)) != null)
      if (pt.color_is(GoPoint.EMPTY))
        count++;

    return count;
  }

  public String toString()
  {
    StringBuffer sbuf = new StringBuffer("WeiQi Saved Game Format 0.0\n");
    Enumeration e = moves.elements();
    
    sbuf.append("\n");
    sbuf.append("(This file is machine-generated and machine-readable;\n");
    sbuf.append("do not edit it unless you know what you are doing.)\n");
    sbuf.append("\n");
    sbuf.append("Board size: " + this.size() + "\n");
    sbuf.append("-*- -*- -*- -*- -*- -*- -*- -*- -*- -*- -*-\n\n");

    // todo: bug: if a point's color changes over time, the last
    // color will be recorded for all moves on that point!  Duh.

    while (e.hasMoreElements())
      {
        RecordPoint pt = (RecordPoint) e.nextElement();
        sbuf.append(pt.color_is(GoPoint.BLACK) ? 'B' : 'W');
        sbuf.append(' ');
        sbuf.append(String.valueOf(pt.x));
        sbuf.append(' ');
        sbuf.append(String.valueOf(pt.y));
        sbuf.append('\n');
      }
    return sbuf.toString();
  }

  public void save(String filename)
  {
    try
      {
        File outfile = new File(filename);
        FileOutputStream outstream = new FileOutputStream(outfile);
        String s = toString();
        byte[] bytes;

        s.getBytes(0, s.length(), bytes = new byte[s.length()], 0);
        outstream.write(bytes);
        outstream.close();
      }
    catch (FileNotFoundException e) {
      System.err.println("GoBoard.save: FileNotFoundException: " + e);
    }
    catch (IOException e) {
      System.err.println("GoBoard.save: IOException: " + e);
    }
  }

  public void save() {
    save("weiqi.txt");
  }

  public void pass() {
    to_move = GoPoint.other_color(to_move);
  }

  public void score() {
    // does nothing yet
  }
}



/** The GoCanvas class is what displays a GoBoard data structure. */
class GoCanvas extends Canvas
{
  GoBoard       board;
  Font          font;
  private int   size;
  private int   x_incr, y_incr, x_start, x_end, y_start, y_end;

  public GoCanvas(GoBoard board)
  {
    this.board = board;
    this.size(board);
  }

  private void size(GoBoard board) {
    this.size = board.size();
  }

  // These two convert from Go-board point to pixel and vice_versa.

  private Point to_pixel(int x, int y)
  {
    int ret_x, ret_y;

    ret_x = ((x * x_incr) + x_start);
    ret_y = ((y * y_incr) + y_start);

    // Restrict it to be within the boundaries of the board... though
    // I don't see why it would ever be an issue, frankly.
    if (ret_x < x_start)  ret_x = x_start;
    if (ret_x > x_end)    ret_x = x_end;
    if (ret_y < y_start)  ret_y = y_start;
    if (ret_y > y_end)    ret_y = y_end;

    return new Point(ret_x, ret_y);
  }

  private Point from_pixel(int x, int y)
  {
    int x_mod, y_mod;

    // Put it within the bounds of the board before doing anything else.
    if (x < x_start) x = x_start;
    if (x > x_end) x = x_end;
    if (y < y_start) y = y_start;
    if (y > y_end) y = y_end;

    // Calculate which side is nearer before trimming the garbage.
    x -= x_start;
    y -= y_start;
    x_mod = (x % x_incr);
    y_mod = (y % y_incr);

    if (x_mod > (x_incr / 2))
      x += (x_incr - x_mod);
    else
      x -= x_mod;

    if (y_mod > (y_incr / 2))
      y += (y_incr - y_mod);
    else
      y -= y_mod;

    // Now we are ready to translate to grid points:
    x /= x_incr;
    y /= y_incr;

    return new Point(x, y);
  }

  public void paint(Graphics g)
  {
    GoPoint pt;
    Rectangle r = bounds();
    int height, width;

    g.setFont(font);

    // Insure our grid is truly square.  todo: where is abs() ??
    if (r.height < r.width)
      {
        height = r.height;
        width  = r.height;
      }
    else if (r.width < r.height)
      {
        height = r.width;
        width  = r.width;
      }
    else
      {
        height = r.height;
        width  = r.width;
      }

    /* Make sure there's a margin around the playing grid, of course. */
    x_incr  = width  / (size + 1);
    y_incr  = height / (size + 1);
    x_start = x_incr;
    x_end   = x_start + (x_incr * (size - 1));
    y_start = y_incr;
    y_end   = y_start + (y_incr * (size - 1));
    
    // Give it a wooden background.
    g.setColor(Color.orange);
    g.fillRect(0, 0, width, height);

    // Give it border lines.
    g.setColor(Color.red);
    for (int i = 0; i < 3; i++)
      g.drawLine(0, i, width, i);
    for (int i = height - 3; i < height; i++)
      g.drawLine(0, i, width, i);
    for (int i = 0; i < 3; i++)
      g.drawLine(i, 0, i, height);
    for (int i = width - 3; i < width; i++)
      g.drawLine(i, 0, i, height);

    // Draw a black grid on the playing area.
    g.setColor(Color.black);
    for (int i = 1; i <= size; i++)
      {
        g.drawLine(x_start, i * y_incr, x_end, i * y_incr);
        g.drawLine(i * x_incr, y_start, i * x_incr, y_end);
      }

    // Draw whatever stones are on the board
    for (int i = 0; i < this.board.size(); i++)
      for (int j = 0; j < this.board.size(); j++)
        {
          int color;
          Point spot = new Point(i, j);

          pt = this.board.get_point(spot);
          Point pix = to_pixel(i, j);
          
          /* 
           * // todo: draw a group id on the stone, for debugging
           * g.setColor(Color.red);
           * g.drawString("" + pt.gid(), pix.x - 5, pix.y + 6);
           */

          if (pt.color_is(GoPoint.BLACK))
            g.setColor(Color.black);
          else if (pt.color_is(GoPoint.WHITE))
            g.setColor(Color.white);
          else if (pt.color_is(GoPoint.EMPTY))
            continue; // this would skip everything below:
          else
            g.setColor(Color.blue);
          
          g.fillOval ((pix.x - ((x_incr / 2) - 1)),
                      (pix.y - ((y_incr / 2) - 1)),
                      x_incr - 2,
                      y_incr - 2);
   
          /*
           * // Draw the gid again, it's important:
           * g.setColor(Color.red);
           * g.drawString("" + pt.gid(), pix.x - 10, pix.y + 6);
           * g.drawString(":", pix.x, pix.y + 6);
           * g.drawString("" + (this.board.get_group(pt.gid())).liberties(),
           *              pix.x + 3, pix.y + 6);
           */
        }

    /*
     * if (this.board.groups != null)
     * {
     * g.setColor(Color.white);
     * g.drawString("L = " + this.board.groups.size(), 250, 270);
     * }
     */
  }

  public void handle_action(int x, int y)
  {
    Point p = from_pixel(x, y);

    if(board.move(p))
      repaint();
  }

  public void redraw(int new_size)
  {
    this.board.size(new_size);
    this.size(this.board);
    repaint();
  }
}



class BoardControls extends Panel
{
  GoBoard board;
  // TextArea  a;
  TextField t, f;
  GoCanvas canvas;

  public BoardControls(GoCanvas canvas, GoBoard board)
  {
    this.canvas = canvas;
    this.board = board;
    // add("South", a = new TextArea("Black..."));
    add(t = new TextField("19", 4));
    add(f = new TextField("weiqi", 5));
    add(new Button("Clear"));
    add(new Button("Pass"));
    add(new Button("Redraw"));
    add(new Button("Save"));
  }

  /* This action() method overrides the action() method in Panel,
   * which is being called by the TextField objects.  So we can just
   * write stuff here to make the TextFields behave differently.
   * Uh, right?
   */
  public boolean action(Event ev, Object arg)
  {
    if (ev.target instanceof Button)
      {
        String label = (String) arg;
        
        if (label.equals("Clear"))
          {
            canvas.redraw(0);
            canvas.redraw(Integer.parseInt((t.getText()).trim()));
          }
        else if (label.equals("Redraw"))
          canvas.redraw(Integer.parseInt((t.getText()).trim()));
        else if (label.equals("Save"))
          board.save((f.getText()).trim());
        else if (label.equals("Pass"))
          board.pass();
        else if (label.equals("Score"))
          board.score();
        return true;
      }
    else if (ev.target instanceof TextField)
      canvas.redraw(Integer.parseInt((t.getText()).trim()));

    return false;
  }
}
