/*
 * $Id: Board.java,v 1.1 2005/06/24 13:56:04 boehm Exp $
 *
 * Copyright (c) 2001 Oliver Boehm (mailto:boehm@2xp.de)
 * http://www.2xp.de
 * All rights reserved.
 *
 * -----------------------------------------
 *
 * $Author: boehm $
 * $Date: 2005/06/24 13:56:04 $
 * $Revision: 1.1 $
 *
 * $Log: Board.java,v $
 * Revision 1.1  2005/06/24 13:56:04  boehm
 * Einspielen der Beispiele und Uebungen inkl. HTML-Seiten
 *
 * Revision 1.11  2001/12/27 16:47:04  oliver
 * in BoardView the ships are only shown on the right side
 *
 * Revision 1.10  2001/12/26 09:50:18  oliver
 * computer plays against computer
 *
 * Revision 1.9  2001/12/23 22:54:46  oliver
 * main() added to Player and tested intensiv
 *
 * Revision 1.8  2001/12/23 20:39:35  oliver
 * Player.turn() implemented
 *
 * Revision 1.7  2001/12/22 18:54:29  oliver
 * Shot object added
 *
 * Revision 1.6  2001/11/10 21:04:43  oliver
 * test classes added
 *
 * Revision 1.5  2001/10/15 20:24:42  oliver
 * merge with stans_bugfix_branch
 *
 * Revision 1.4  2001/10/14 20:16:19  oliver
 * show(StringWriter) added
 *
 * Revision 1.3  2001/10/10 16:39:08  oliver
 * show() now prints the board on the console
 *
 * Revision 1.2  2001/10/06 21:39:45  oliver
 * Constructor added
 *
 * Revision 1.1.1.1  2001/10/06 15:23:10  oliver
 * imported sources
 */



package armada;



import java.io.Serializable;
import java.util.*;

import org.apache.log4j.Logger;



/**
 * Title:        Armada
 * Description:  Ein einfaches "Schiffe versenken"-Beispiel
 * Copyright:    Copyright (c) 2001
 * @author Oliver Boehm
 * @version 1.0
 */

public class Board implements Serializable, Cloneable {
    
    /** needed for Serializable */
    private static final long serialVersionUID = 3906930084220057913L;

    /** where to log */
    private static Logger log = Logger.getLogger(Board.class);

    /** a field which is not allowed (e.g. nothing is expected) */
    static final public char NOTHING = '-';

    /** an empty field (e.g. water */
    static final public char EMPTY = ' ';
    
    /** if a ship is hit */
    static final public char HIT = '#';
    
    /** if an empty field was hit */
    static final public char NO_HIT = '~';
    
    /** width */
    private int w = 10;
    
    /** height */
    private int h = 10;

    /** board with 10 x 10 fields and a boarder around */
    private char[][] field = null;



    /**
     * Constructor Board
     * initializes the board, where the boarder is marked with 'NOTHING'.
     */

    public Board() {
        this(Config.getInstance().getBoardWidth(),
             Config.getInstance().getBoardHeight());
    }
    
    public Board(int width, int height) {
        log.info("initializing board(" + width + "x" + height + ")");
        w = width;
        h = height;
        field = new char[w+2][h+2];
        for (int i = 0; i < (w+2); i++) {
            for (int j = 0; j < (h+2); j++) {
                field[i][j] = NOTHING ;
            }
        }
        for (int i = 1; i <= w; i++) {
            for (int j = 1; j <= h; j++) {
                field[i][j] = EMPTY;
            }
        }
        this.setShips();
    }



    /**
     * setShips sets 1 large ship (length = 4), 2 medium ships (length = 3),
     * 3 short ships (length = 2) and 4 yellow submarines (length = 1).
     * There must be at least 1 free board field between the single ships
     * (no contact allowed).
     */

    public void setShips() {
        for (int length = 4; length >= 1; length--) {
            for (int n = 1; n <= (5 - length); n++) {
                findPlaceForShip(length);
            }
        }
        //  now replace the NOTHING fields by EMPTY fields
        for (int ix = 1; ix <= w; ix++) {
            for (int iy = 1; iy <= h; iy++) {
                if (field[ix][iy] == NOTHING) {
                    field[ix][iy] = EMPTY;
                }
            }
        }
    }

    private void findPlaceForShip(int length) {
        Random rand = new Random();
        int xa, xe, ya, ye;
        //  find a place for a ship
        do {
            //  start coordinates for the ship
            xa = xe = rand.nextInt(w) % w + 1;
            ya = ye = rand.nextInt(h) % h + 1;
            //  which direction (left, right, up, down)
            switch (rand.nextInt(4)) {
                case 0: xe += (length - 1);
                        break;
                case 1: xa += (1 - length);
                        break;
                case 2: ya += (1 - length);
                        break;
                case 3: ye += (length - 1);
                        break;
            }
        } while (shipCollidet(xa, xe, ya, ye));
        markWithShip(xa, xe, ya, ye, length);
        log.info("" + length + "-ship set (" + xa + "/" + ya + " - "
                + xe + "/" + ye + ")");
    }

    /**
     * Mark the board with the given ship. First the surrounding is marked
     * then the ship fields.
     * 
     * @param xa     x begin
     * @param xe     x end
     * @param ya     y begin
     * @param ye     y end
     * @param length of the ship
     */
    private void markWithShip(int xa, int xe, int ya, int ye, int length) {
        //  mark the board with the ships...
        //  ... first the surrounding
        for (int ix = xa - 1; ix <= (xe + 1); ix++) {
            for (int iy = ya - 1; iy <= (ye + 1); iy++) {
                field[ix][iy] = NOTHING;
            }
        }
        //  ... and now the ship fields
        for (int ix = xa; ix <= xe; ix++) {
            for (int iy = ya; iy <= ye; iy++) {
                field[ix][iy] = Character.forDigit(length, 10);
            }
        }
    }



    /**
     * shipCollidet checks the given coordinates if there is enough room
     * for the given ship.
     *
     * @param  xa  x-coordinate (beginning)
     * @param  xe  x-coordinate (end)
     * @param  ya  y-coordinate (beginning)
     * @param  ye  y-coordinate (end)
     * @returns    true if the coordinates are correct and there is enough
     *             room (otherwise false)
     */

    private boolean shipCollidet(int xa, int xe, int ya, int ye) {

        //  ship outside board?
        if ((xa < 1) || (ya < 1) || (xe > w) || (ye > h)) {
            log.debug("ship (" + xa + "/" + ya + " - "
                    + xe + "/" + ye + ") collides with boarder");
            return true;
        }
        
        //  enough room for the ship?
        for (int ix = xa; ix <= xe; ix++) {
            for (int iy = ya; iy <= ye; iy++) {
                if (field[ix][iy] != EMPTY) {
                    log.debug("ship (" + xa + "/" + ya + " - "
                            + xe + "/" + ye + ") collides");
                    return true;
                }
            }
        }
        log.debug("no collision for ship (" + xa + "/" + ya + " - "
                + xe + "/" + ye + ")");
        return false;
    }



    /**
     * markShipEnvironment marks each neighbour field of the hit ship
     * with NOTHING.
     * 
     * @param	shot	one of the ship coordinates
     */
   
    public void markShipEnvironment(Shot shot) {
        Shot up = shot;
        Shot down = shot;
        Shot left = shot;
        Shot right = shot;

        // search start coordinates
        try {
            for (Shot sh = shot.down(); isHit(sh); sh = sh.down()) {
                down = sh;
            }
            for (Shot sh = shot.up(); isHit(sh); sh = sh.up()) {
                up = sh;
            }
            for (Shot sh = shot.left(); isHit(sh); sh = sh.left()) {
                left = sh;
            }
            for (Shot sh = shot.right(); isHit(sh); sh = sh.right()) {
                right = sh;
            }
        } catch (ShotException e) {
            System.err.println("internal error\n" + e);
        }

        // mark water (EMPTY) with NOTHING
        for (int ix = left.getY() - 1; ix <= right.getX() + 1; ix++) {
            for (int iy = down.getY() - 1; iy <= up.getY() + 1; iy++) {
                if (field[ix][iy] == EMPTY) {
                    field[ix][iy] = NOTHING;
                }
            }
        }
    }
    	


    /**
     * isHit checks if there is any hit on a ship.
     *
     * @returns true, if a ship was hit
     */

    public boolean isHit() {
        int hits = 0;
        hits += this.getShipHitRate('1');
        hits += this.getShipHitRate('2');
        hits += this.getShipHitRate('3');
        hits += this.getShipHitRate('4');
        return (hits == 0);
    }



    /**
     * getShipHitRate calculates the ratio of the sunk ships.
     *
     * @param   ship   which ship: '4', '3', '2' or '1'
     * @returns the ratio of ships which sunk in percent (0 - 100%)
     * @todo    implementation
     */

    public int getShipHitRate(char ship) {
        return (this.getShipHits(ship) * 100) / this.getShipItems(ship);
    }



    /**
     * getShipHits returns the hits of all ships.
     *
     * @returns the number of hits
     */

    public int getShipHits() {
        int hits = 0;
        hits += this.getShipHits('1');
        hits += this.getShipHits('2');
        hits += this.getShipHits('3');
        hits += this.getShipHits('4');
        return hits;
    }



    /**
     * getShipHits returns the hit(s) of the given ship
     *
     * @param ship  ship ('1'..'4')
     * @returns the number of hits
     */

    public int getShipHits(char ship) {
        return this.getShipItems(ship) - this.getShipIntactItems(ship);
    }
    
    
    
    /**
     * getShipIntactItems counts the ship items with no hit
     *
     * @return	number of intact ship items
     */
    
    public int getShipIntactItems() {
    	return getShipIntactItems('1') + getShipIntactItems('2') +
    	       getShipIntactItems('3') + getShipIntactItems('4');
    }
    
    
    
    /**
     * getShipIntactItems counts the ship items with no hit
     *
     * @param	ship   ship ('1'..'4')
     * @return	number of intact ship items
     */
    private int getShipIntactItems(char ship) {
        int intactItems = 0;
        for (int i = 1; i <= w; i++) {
            for (int j = 1; j <= h; j++) {
                if (field[i][j] == ship) {
                    intactItems++;
                }
            }
        }
        log.debug("" + intactItems + " of " + ship + "-er ship intact");
        return intactItems;
    }



    /**
     * getShipItems() returns the number of all ship items
     *
     * @returns the number of ship items
     */
    public int getShipItems() {
        int n = 0;
        n += this.getShipItems('1');
        n += this.getShipItems('2');
        n += this.getShipItems('3');
        n += this.getShipItems('4');
        return n;
    }



    /**
     * getShipItems returns the mumber of items for the given ship type
     * (e.g. all items of type '2' or 2 ships x 3 items = 6 items)
     *
     * @param ship  the ship type ('1'..'4')
     */
    public int getShipItems(char ship) {
    	switch (ship) {
    	    case '1':   return 4;
    	    case '2':   return 6;
    	    case '3':   return 6;
    	    case '4':   return 4;
    	}
    	return 0;
    }


    /**
     * getField returns the wanted field element.
     *
     * @params s    the adress of a board field. The range the board is from
     *              "A0" (upper left corner) till "J9" (lower right corner).
     */
    public char getField(String s) {
        char x = s.charAt(0);
        int  y = Character.digit(s.charAt(1), 10);
        return this.getField(x, y);
    }



    /**
     * getField returns the wanted field element.
     *
     * @params  cx  the x-coordinate ('A' - 'J')
     *          y   the y-coordinate ( 0  -  9 )
     */
    public char getField(char cx, int y) {
        int x = Character.toUpperCase(cx) - 'A';
        return field[x+1][y+1];
    }
    
    
    
    /**
     * isValidShot returns true if the given shot is valid.
     * 
     * @params	shot	the shot
     */
    public boolean isValidShot(Shot shot) {
    	int x = shot.getX();
    	int y = shot.getY();
    	if ((x < 1) || (x > w) || (y < 1) || (y > h)) {
    	    return false;
    	}
    	return ((field[x][y] == EMPTY) || isShip(x,y));
    }
    
    
    
    /**
     * isInvalidShot returns true if the given shot is invalid.
     * 
     * @params	shot	the shot
     */
    public boolean isInvalidShot(Shot shot) {
    	return !this.isValidShot(shot);
    }
    

    
    /**
     * @param	shot	the shot
     * @return	true if the given shot points to a hit
     *          false otherwise
     */
    
    public boolean isHit(Shot shot) {
    	int x = shot.getX();
    	int y = shot.getY();
    	return (field[x][y] == HIT);
    }
    
    
    
    /**
     * countFreeVerticalFields
     * counts the fields up and down which are free, e.g. where a shot
     * can be placed.
     * 
     * @param	shot	the origin from which the free fields are
     * 			counted
     * @return	the number of "free" fields
     * 		(a field where no shot was set before)
     */
    public int countFreeVerticalFields(Shot shot) {
        int sum = 0;
        sum += countFreeFieldsUp(shot);
        sum += countFreeFieldsDown(shot);
        return sum;
    }
    
    public int countFreeFieldsUp(Shot shot) {
        int sum = 0;
        try {
            Shot sh = null;
            for (sh = shot.up(); isHit(sh); sh = sh.up())
                ; // ignore hits
            for (; isValidShot(sh); sh = sh.up()) {
                sum++;
            }
        } catch (ShotException e) {
        } // ignored
        return sum;
    }
    
    public int countFreeFieldsDown(Shot shot) {
        int sum = 0;
        try {
            Shot sh = null;
            for (sh = shot.down(); isHit(sh); sh = sh.down())
                ;
            for (; isValidShot(sh); sh = sh.down()) {
                sum++;
            }
        } catch (ShotException e) {} // ignored
        return sum;
    }
    
    
    
    /**
     * countFreeHorizontalFields
     * counts the fields up and down which are free, e.g. where a shot
     * can be placed.
     * 
     * @param	shot	the origin from which the free fields are
     * 			counted
     * @return	the number of "free" fields
     *          (a field where no shot was set before)
     */
    public int countFreeHorizontalFields(Shot shot) {
    	int sum = 0;
    	if (isValidShot(shot)) {
    	    sum += 1;
    	}
    	sum += countFreeFieldsLeft(shot);
    	sum += countFreeFieldsRight(shot);
    	return sum;
    }
    
    public int countFreeFieldsLeft(Shot shot) {
    	int sum = 0;
    	try {
    	    Shot sh = shot.left();
    	    for (; isHit(sh); sh = sh.left());	// ignore hits
	    for (; isValidShot(sh); sh = sh.left()) {
	        sum++;
	    }
    	} catch (ShotException e) {}	// ignored
    	return sum;
    }
    
    public int countFreeFieldsRight(Shot shot) {
    	int sum = 0;
    	try {
    	    Shot sh = shot.right();
    	    for (; isHit(sh); sh = sh.right());	// ignore hits
    	    for (; isValidShot(sh); sh = sh.right()) {
    	        sum++;
    	    }
    	} catch (ShotException e) {}	// ignored
    	return sum;
    }

    
    
    /**
     * shootTo marks the given shot in the board as HIT (if there is a ship
     * at the given position or as NO_HIT (if there is no ship).
     * 
     * @param	shot	the given shot
     * @return	'1'..'4', if a ship was hit,
     * 		EMPTY otherwise
     */
    public char shootTo(Shot shot) {
    	int x = shot.getX();
    	int y = shot.getY();
    	char hit = field[x][y];
    	if (this.isShip(x, y)) {
    	    field[x][y] = HIT;
    	} else {
    	    field[x][y] = NO_HIT;
    	}
    	return hit;
    }
    
    
    
    /**
     * isShip returns true if at the given position there is an intact
     * ship.
     * 
     * @param	x   x-coordinate
     * @param   y   y-coordinate
     */
    private boolean isShip(int x, int y) {
    	switch (field[x][y]) {
    		case '1':
    		case '2':
    		case '3':
    		case '4':  return true;
    		default:   return false;
    	}
    }
    
    public int getWidth() {
        return this.w;
    }
    
    public int getHeight() {
        return this.h;
    }

    public boolean equals(Object other) {
        if (other instanceof Board) {
            return equals((Board) other);
        } else {
            return false;
        }
    }
    
    public boolean equals(Board other) {
        for (int x = 0; x < (w+2); x++) {
            for (int y = 0; y < (h+2); y++) {
                if (field[x][y] != other.field[x][y]) {
                    return false;
                }
            }
        }
        return true;
    }

    public Object clone() {
        Board cloned = new Board();
        for (int x = 0; x < (w+2); x++) {
            for (int y = 0; y < (h+2); y++) {
                cloned.field[x][y] = field[x][y];
            }
        }
        return cloned;
    }

}
