/*
 *  Copyright 2002-2009 Mikhail Kryshen <mikhail@kryshen.net>
 *
 *  This file is part of BMTron.
 *
 *  BMTron 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 3 of the License, or
 *  (at your option) any later version.
 *
 *  BMTron 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 BMTron.  If not, see <http://www.gnu.org/licenses/>.
 */

package kryshen.bmtron;

import java.awt.Graphics;
import java.awt.Image;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;

public class Field implements Runnable, KeyListener {
    /* Player movement directions. */
    final static int UP = 0;
    final static int RIGHT = 1;
    final static int DOWN = 2;
    final static int LEFT = 3;
    
    /* Snake sprites. */
    final static int BODY_H = 0;
    final static int BODY_V = 1;
    final static int BODY_TL = 2;
    final static int BODY_TR = 3;
    final static int BODY_BL = 4;
    final static int BODY_BR = 5;
    final static int HEAD_L = 6;
    final static int HEAD_R = 7;
    final static int HEAD_U = 8;
    final static int HEAD_D = 9;
    final static int BLOW_L = 10;
    final static int BLOW_R = 20;
    final static int BLOW_U = 30;
    final static int BLOW_D = 40;
    final static int APPEAR_U = 51;
    final static int APPEAR_D = 52;
    
    final static int BODY_IMAGES = 10;
    final static int NUM_IMAGES = 53;
    
    private int pointsToWin = 10;
    
    final int interval = 75;
    
    /* Field size. */
    final int cols = 50;
    final int rows = 35;
    
    /* Field cell size. */
    final int c2x = 10;
    final int r2y = 10;
    
    private Thread thread = null;
    
    final Game game;
    Player[] players;
    
    private String winnerName = null;
    private int winnerPoints = 0;
    
    /** Teamplay mode. */
    private boolean teamplay;
    
    private int[][] field = new int[cols][rows];
    
    private long startTime;
    private int time;
    private boolean paused = false;
    
    private Image background = null;
    
    Field(Game game) {
        this.game = game;
        game.addKeyListener(this);
    }
    
    private void clearField() {
        for (int i = 0; i < cols; i++) {
            for (int j = 0; j < rows; j++) {
                field[i][j] = 0;
            }
        }
    }
    
    void setup(Player[] players, boolean teamplay, int pointsToWin) {
        clearField();
        
        this.players = players;
        this.teamplay = teamplay;
        this.pointsToWin = pointsToWin;
        
        for (int i = 0; i < players.length; i++)
            players[i].reset();
        
        if (players.length == 2) {
            locatePlayer(0, 15, 17, APPEAR_U);
            locatePlayer(1, 34, 17, APPEAR_U);
        } else if (players.length == 3) {
            locatePlayer(0, 17, 12, APPEAR_U);
            locatePlayer(1, 17, 22, APPEAR_D);
            locatePlayer(2, 32, 17, APPEAR_U);
        } else if (players.length == 4 && !teamplay) {
            locatePlayer(0, 15, 10, APPEAR_U);
            locatePlayer(1, 34, 10, APPEAR_U);
            locatePlayer(2, 15, 24, APPEAR_D);
            locatePlayer(3, 34, 24, APPEAR_D);
        } else if (players.length == 4 && teamplay) {
            locatePlayer(0, 15, 17, APPEAR_U);
            locatePlayer(1, 15, 18, APPEAR_D);
            locatePlayer(2, 34, 17, APPEAR_U);
            locatePlayer(3, 34, 18, APPEAR_D);
        }
        
        game.setStatus("Score: " + getScore(),
                "" + pointsToWin + " points to win", false);
        
        for (int i = 0; i < players.length; i++)
            players[i].start();
        
        game.repaintField();
    }
    
    void start() {
        thread = new Thread(this);
        thread.start();
    }
    
    void stop() {
        Thread thread = this.thread;
        this.thread = null;
        
        if (thread != null) {
            thread.interrupt();
        }
    }
    
    private void locatePlayer(int player, int x, int y, int body) {
        players[player].setLocation(x, y);
        setPlayerCellType(x, y, player, body);
    }
    
    public void run() {
        waitToStart();
        
        long t = System.currentTimeMillis();
        boolean stopped;
        
        while (Thread.currentThread() == thread) {
            t = System.currentTimeMillis();
            
            if (paused) {
                pauseWait();
            }
            
            stopped = move();
            displayTime();
            
            long lag = System.currentTimeMillis() - t;
            
            if (interval > lag) {
                try {
                    Thread.currentThread().sleep(interval - lag);
                } catch (Exception e) {
                    break;
                }
            } else {
                Thread.yield();
            }
            
            if (Thread.currentThread() != thread)
                break;
            
            if (stopped) {
                int runningTeams = 0;
                int teamAlive = -1;
                
                for (int i = 0; i < players.length; i++) {
                    int running = getRunningPlayers(i);
                    if (running < 0) break;
                    if (running > 0) {
                        runningTeams++;
                        teamAlive = i;
                    }
                }
                
                if (runningTeams < 2) {
                    if (!endRound(runningTeams, teamAlive))
                        break;
                }
            }
        }
        
        game.showSettings();
    }
    
    private boolean endRound(int runningTeams, int teamAlive) {
        findWinner(runningTeams, teamAlive);
        
        for (int i = 0; i < players.length; i++) {
            if (!players[i].isStoped())
                players[i].stop();
        }
        
        if (winnerName == null)
            game.setStatus("There is no winner", null);
        else {
            game.setStatus(winnerName + " wins!", null);
        }
        
        try {
            Thread.currentThread().sleep(500);
        } catch (InterruptedException e) {
            return false;
        }
        
        if (winnerPoints >= pointsToWin) {
            game.setStatus(winnerName + " wins!",
                    "Score: " + getScore(), true);
            
            try {
                Thread.currentThread().sleep(2000);
            } catch (InterruptedException e) {
                return false;
            }
            
            game.setStatus(null, null, false);
            return false;
        }
        
        setup(players, teamplay, pointsToWin);
        waitToStart();
        return true;
    }
    
    private void findWinner(int runningTeams, int teamAlive) {
        if (runningTeams == 1) {
            Player winnerPlayer = null;
            
            for (int i = 0; i < players.length; i++) {
                if (!players[i].isStoped()) {
                    winnerPlayer = players[i];
                    winnerPlayer.points++;
                }
            }
            
            if (teamplay) {
                winnerName = "Team " + (teamAlive + 1);
                winnerPoints = getTeamScore(teamAlive);
                
                //winners = "";
                //for (int i = 0; i < players.length; i++) {
                //    if (players[i].team == teamAlive) {
                //        if (winners.length() > 0)
                //            winners += ", ";
                //        winners += players[i].name;
                //    }
                //}
            } else {
                winnerName = winnerPlayer.name;
                winnerPoints = winnerPlayer.points;
            }
        }
    }
    
    private void waitToStart() {
        boolean nodir = true;
        while (nodir & Thread.currentThread() == thread) {
            nodir = false;
            for (int i = 0; i < players.length; i++)
                if (players[i].getDirection() < 0) {
                nodir = true;
                break;
                }
            try {
                Thread.currentThread().sleep(interval);
            } catch (InterruptedException e) {}
        }
        
        time = 0;
        paused = false;
        startTime = System.currentTimeMillis();
    }
    
    private void pauseWait() {
        long t = System.currentTimeMillis();
        
        int pv = 0;
        while (paused) {
            if (pv <= 3)
                game.setStatus("*** Pause ***");
            else
                game.setStatus("");
            
            pv++;
            if (pv > 7) pv = 0;
            
            try {
                Thread.currentThread().sleep(250);
            } catch (InterruptedException e) {}
            
            time = -1;
        }
        
        startTime += System.currentTimeMillis() - t;
    }
    
    private boolean move() {
        boolean stoped = false;
        
        int cx1 = Integer.MAX_VALUE;
        int cy1 = Integer.MAX_VALUE;
        int cx2 = Integer.MIN_VALUE;
        int cy2 = Integer.MIN_VALUE;
        
        for (int i = 0; i < players.length; i++) {
            
            if (players[i].isStoped()) continue;
            
            Point l = players[i].getLocation();
            int x = l.x, y = l.y;
            int body = -1;
            int blow = -1;
            int head = getCellType(l.x, l.y);
            
            switch (players[i].getDirection()) {
                case UP:
                    y--;
                    switch (head) {
                        case HEAD_U:
                        case HEAD_D:
                        case APPEAR_U:
                        case APPEAR_D:
                            body = BODY_V;
                            break;
                        case HEAD_R:
                            body = BODY_BR;
                            break;
                        case HEAD_L:
                            body = BODY_BL;
                    }
                    head = HEAD_U;
                    blow = BLOW_U;
                    break;
                case RIGHT:
                    x++;
                    switch (head) {
                        case HEAD_U:
                        case APPEAR_U:
                            body = BODY_TL;
                            break;
                        case HEAD_R:
                        case HEAD_L:
                            body = BODY_H;
                            break;
                        case HEAD_D:
                        case APPEAR_D:
                            body = BODY_BL;
                    }
                    head = HEAD_R;
                    blow = BLOW_R;
                    break;
                case DOWN:
                    y++;
                    switch (head) {
                        case HEAD_R:
                            body = BODY_TR;
                            break;
                        case HEAD_D:
                        case HEAD_U:
                        case APPEAR_D:
                        case APPEAR_U:
                            body = BODY_V;
                            break;
                        case HEAD_L:
                            body = BODY_TL;
                    }
                    head = HEAD_D;
                    blow = BLOW_D;
                    break;
                case LEFT:
                    x--;
                    switch (head) {
                        case HEAD_U:
                        case APPEAR_U:
                            body = BODY_TR;
                            break;
                        case HEAD_D:
                        case APPEAR_D:
                            body = BODY_BR;
                            break;
                        case HEAD_L:
                        case HEAD_R:
                            body = BODY_H;
                    }
                    head = HEAD_L;
                    blow = BLOW_L;
            }
            
            if (isFree(x, y)) {
                setPlayerCellType(x, y, i, head);
                setPlayerCellType(l.x, l.y, i, body);
                players[i].setLocation(x, y);
            } else {
                players[i].stop();
                stoped = true;
                setPlayerCellType(l.x, l.y, i, body + blow);
                
                int ct = getCellType(x, y);
                
                if (ct >= 0) {
                    switch (ct) {
                        case HEAD_U:
                        case HEAD_D:
                            body = BODY_V;
                            break;
                        case HEAD_L:
                        case HEAD_R:
                            body = BODY_H;
                            break;
                        default:
                            body = -1;
                    }
                    
                    // Head collision
                    if (body >= 0) {
                        int j = getPlayer(x, y);
                        if (j < i && !players[j].isStoped()) {
                            players[j].stop();
                            stoped = true;
                            setPlayerCellType(x, y, j, body);
                        }
                    }
                }
            }
            
            if (x < cx1) cx1 = x;
            if (x > cx2) cx2 = x;
            if (y < cy1) cy1 = y;
            if (y > cy2) cy2 = y;
            if (l.x < cx1) cx1 = l.x;
            if (l.x > cx2) cx2 = l.x;
            if (l.y < cy1) cy1 = l.y;
            if (l.y > cy2) cy2 = l.y;
        }
        
        
        game.repaintField(cx1 * c2x, cy1 * r2y,
                (cx2 - cx1 + 1) * c2x, (cy2 - cy1 + 1) * r2y);
        
        for (int i = 0; i < players.length; i++)
            if (!players[i].isStoped())
                players[i].update();
        
        return stoped;
    }
    
    private int getRunningPlayers(int team) {
        boolean found = false;
        int runningPlayers = 0;
        
        for (int i = 0; i < players.length; i++) {
            if (players[i].team == team) {
                found = true;
                if (!players[i].isStoped())
                    runningPlayers++;
            }
        }
        
        return found ? runningPlayers : -1;
    }
    
    private int getTeamScore(int team) {
        boolean found = false;
        int score = 0;
        
        for (int i = 0; i < players.length; i++) {
            if (players[i].team == team) {
                found = true;
                score += players[i].points;
            }
        }
        
        return found ? score : -1;
    }
    
    private String getScore() {
        String s = "" + getTeamScore(0);
        for (int i = 1; i < players.length; i++) {
            int score = getTeamScore(i);
            if (score < 0) break;
            s += ":" + score;
        }
        return s;
    }
    
    private void displayTime() {
        int ntime = (int)(System.currentTimeMillis() - startTime) / 1000;
        if (ntime != time || time == 0) {
            time = ntime;
            int m = time / 60;
            int s = time - m * 60;
            game.setStatus("Time: " + ((m < 10) ? "0" : "") + m +
                    ((s < 10) ? ":0" : ":") + s);
        }
    }
    
    private void setPlayerCellType(int x, int y, int player, int cellType) {
        setValue(x, y, NUM_IMAGES * player + cellType + 1);
    }
    
    int getPlayer(int value) {
        return (value - 1) / NUM_IMAGES;
    }
    
    int getPlayer(int x, int y) {
        return getPlayer(getValue(x, y));
    }
    
    int getCellType(int value) {
        if (value <= 0)
            return -1;
        
        value--;
        
        return value - (value / NUM_IMAGES) * NUM_IMAGES;
    }
    
    int getCellType(int x, int y) {
        return getCellType(getValue(x, y));
    }
    
    int getBlow(int cellType) {
        if (cellType >= BLOW_U && cellType < BLOW_U + BODY_IMAGES)
            return BLOW_U;
        if (cellType >= BLOW_D && cellType < BLOW_D + BODY_IMAGES)
            return BLOW_D;
        if (cellType >= BLOW_L && cellType < BLOW_L + BODY_IMAGES)
            return BLOW_L;
        if (cellType >= BLOW_R && cellType < BLOW_R + BODY_IMAGES)
            return BLOW_R;
        
        return -1;
    }
    
    int getBody(int cellType) {
        if (cellType < BODY_IMAGES)
            return cellType;
        
        int blow = getBlow(cellType);
        
        if (blow >= 0)
            return cellType - blow;
        
        return -1;
    }
    
    boolean isFree(int x, int y) {
        return (getValue(x, y) == 0);
    }
    
    int getValue(int x, int y) {
        if (x < 0 || y < 0 || x >= field.length || y >= field[0].length)
            return -1;
        
        return field[x][y];
    }
    
    private void setValue(int col, int row, int value) {
        field[col][row] = value;
    }
    
    void render(Graphics g, int x, int y, int width, int height) {
        Rectangle clip = g.getClipBounds();
        
        if (clip == null) {
            clip = new Rectangle(x, y, width, height);
        } else {
            clip = clip.intersection(new Rectangle(x, y, width, height));
        }
        
        renderBackground(g, x, y, clip);
        
        int col1 = (clip.x - x) / c2x;
        int row1 = (clip.y - y) / r2y;
        int col2 = (clip.x - x + clip.width - 1) / c2x;
        int row2 = (clip.y - y + clip.height - 1) / r2y;
        
        renderSnakes(g, x, y, col1, row1, col2, row2);
        renderEffects(g, x, y, col1, row1, col2, row2);
    }
    
    private void renderBackground(Graphics g, int sx, int sy, Rectangle clip) {
        int w = game.skin.field.getWidth(null);
        int h = game.skin.field.getHeight(null);
        
        int x1 = ((clip.x - sx) / w) * w + sx;
        int y1 = ((clip.y - sy) / h) * h + sy;
        
        int x2 = clip.x + clip.width;
        int y2 = clip.y + clip.height;
        
        for (int x = x1; x < x2; x += w) {
            for (int y = y1; y < y2; y += h) {
                g.drawImage(game.skin.field, x, y, null);
            }
        }
    }
    
    private void renderSnakes(Graphics g, int x, int y,
            int col1, int row1, int col2, int row2) {
        
        for (int col = col1; col <= col2; col++) {
            for (int row = row1; row <= row2; row++) {
                int body = getBody(getCellType(col, row));
                
                if (body < 0)
                    continue;
                
                g.drawImage(players[getPlayer(col, row)].getImage(body),
                        x + col * c2x, y + row * r2y, null);
            }
        }
    }
    
    private void renderEffects(Graphics g, int x, int y,
            int col1, int row1, int col2, int row2) {
        
        col1 = Math.max(0, col1 - game.skin.effectWidth / c2x / 2);
        row1 = Math.max(0, row1 - game.skin.effectHeight / r2y / 2);
        col2 = Math.min(cols - 1, col2 + game.skin.effectWidth / c2x / 2);
        row2 = Math.min(rows - 1, row2 + game.skin.effectHeight / r2y / 2);
        
        for (int col = col1; col <= col2; col++) {
            for (int row = row1; row <= row2; row++) {
                int ct = getCellType(col, row);
                
                if (ct < 0)
                    continue;
                
                switch (ct) {
                    case APPEAR_U:
                        players[getPlayer(col, row)].getAppearU().drawFrame(
                                g,
                                x + col * c2x + (c2x - game.skin.effectWidth) / 2,
                                y + row * r2y + (r2y - game.skin.effectHeight) / 2,
                                game);
                        break;
                    case APPEAR_D:
                        players[getPlayer(col, row)].getAppearD().drawFrame(
                                g,
                                x + col * c2x + (c2x - game.skin.effectWidth) / 2,
                                y + row * r2y + (r2y - game.skin.effectHeight) / 2,
                                game);
                        break;
                }
                
                int blow = getBlow(ct);
                
                switch (blow) {
                    case BLOW_R:
                        players[getPlayer(col, row)].getBlow().drawFrame(
                                g,
                                x + col * c2x + c2x - game.skin.effectWidth / 2 - 1,
                                y + row * r2y + (r2y - game.skin.effectHeight) / 2,
                                game);
                        break;
                    case BLOW_L:
                        players[getPlayer(col, row)].getBlow().drawFrame(
                                g,
                                x + col * c2x - game.skin.effectWidth / 2 + 1,
                                y + row * r2y + (r2y - game.skin.effectHeight) / 2,
                                game);
                        break;
                    case BLOW_D:
                        players[getPlayer(col, row)].getBlow().drawFrame(
                                g,
                                x + col * c2x + (c2x - game.skin.effectWidth) / 2,
                                y + row * r2y + r2y - game.skin.effectHeight / 2 - 1,
                                game);
                        break;
                    case BLOW_U:
                        players[getPlayer(col, row)].getBlow().drawFrame(
                                g,
                                x + col * c2x + (c2x - game.skin.effectWidth) / 2,
                                y + row * r2y - game.skin.effectHeight / 2 + 1,
                                game);
                        break;
                }
            }
        }
    }
    
    public void keyPressed(KeyEvent e) {
        switch (e.getKeyCode()) {
            case 27:
                if (!paused) {
                    stop();
                    game.setStatus(game.TITLE + " " + game.VERSION,
                            "Score: " + getScore(), false);
                } else
                    paused = false;
                break;
            case 19:
                paused = !paused;
                break;
//            default:
//                System.err.println(e.getKeyCode());
        }
    }
    
    public void keyReleased(KeyEvent e) {
    }
    
    public void keyTyped(KeyEvent e) {
    }
}
