/*
 *  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.applet.Applet;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Desktop;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Transparency;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.util.Random;
import java.util.StringTokenizer;

/**
 * The main class.
 */
public class Game extends Applet implements Runnable, MouseListener,
        MouseMotionListener {
    public static final String TITLE = "BMTron";
    public static final String VERSION = "1.3.1";
    public static final String AUTHOR = "Mikhail Kryshen";
    public static final String HOME_PAGE =
            "http://kryshen.net/games/bmtron.html";
    
    static final boolean DISPLAY_URL = true;
    static final boolean DEBUG = false;
    
    private final int WIDTH = 520;
    private final int HEIGHT = DISPLAY_URL ? 415 : 400;

    private static final String DEFAULT_SKIN = "skins/default.bin";

    final Rectangle fieldArea = new Rectangle(10, 40, 500, 350);
    final Rectangle statusArea = new Rectangle(200, 5, 310, 26);
    final Rectangle urlArea = new Rectangle(10, HEIGHT - 20, WIDTH - 20, 20);
    
    private boolean statusCenter = true;
    private String status1 = "Loading " + TITLE + "...";
    private String status2 = "wait, please";
    private final Object statusLock = new Object();
    
    private boolean titleHover = false;
    
    private Image offScreenImage = null;
    private Graphics offScreenGraphics = null;

    private boolean application = false;
    
    private Thread loader = null;
    
    private static boolean antialias = checkJavaVersion(1, 2);
    
    Field field = null;
    private boolean renderField = false;
    
    SettingsScreen settings = null;
    Skin skin = null;
    
    public static void main(String[] args) {
        Frame frame = new Frame(TITLE + " " + VERSION);
        frame.setResizable(false);
        frame.setLayout(new BorderLayout());
        Game game = new Game();
        game.application = true;
        frame.add(game, "Center");
        
        URL skinSource = null;
        try {
            // Note: File.toURI() is available since Java 1.4.
            // Java 1.4 or later is required to run BMTron as
            // a stand-alone application.
            skinSource = ((args.length > 0) ?
                (new File(args[nextRandom(0, args.length - 1)])) :
                (new File("skins/default.bin"))).toURI().toURL();
        } catch (MalformedURLException e) {
            // Ignore errors here. Exception will be thrown
            // at skin.load() and then reported.
            System.err.println(e);
        }
        
        game.init(frame, new Skin(game, skinSource));
        
        frame.addWindowListener(new WindowAdapter() {
            public void windowClosing(WindowEvent e) {
                System.exit(0);
            }
        });
    }
    
    public void init() {
        URL skinSource = null;
        
        try {
            String skinName = getParameter("skin");
            if (skinName == null) {
                int n = 0;
                while (getParameter("skin" + (n + 1)) != null) {
                    n++;
                }
                
                if (n > 0) {
                    int s = nextRandom(1, n);
                    
                    skinName = getParameter("skin" + s);
                } else {
                    skinName = DEFAULT_SKIN;
                }
            }
            
            skinSource = new URL(getCodeBase(), skinName);
        } catch (MalformedURLException e) {
            // Ignore errors here. Exception will be thrown
            // at skin.load() and then reported.
            System.err.println(e);
        }
        
        init(null, (skin == null) ? new Skin(this, skinSource) : skin);                
    }
    
    public void init(Frame frame, Skin skin) {
        System.err.println(TITLE + " " + VERSION + " by " +  AUTHOR);
        System.err.println();
        System.err.println("Distributed under the terms of the");
        System.err.println("GNU General Public License (version 3 or later)");
        System.err.println();
        System.err.println("Loading skin: " + skin);
        
        this.skin = skin;
        
        setLayout(null);
        setBackground(Color.white);
        
        if (frame != null) {
            frame.pack();
            frame.setVisible(true);
        }
        
        addMouseListener(this);
        addMouseMotionListener(this);
        
        loader = new Thread(this);
        loader.start();
    }
    
    public void destroy() {
        if (field != null)
            field.stop();
        
        removeMouseListener(this);
        removeMouseMotionListener(this);
        removeAll();
    }
    
    public void run() {
        try {
            setStatus("loading...");
            
            if (!skin.isLoaded())
                skin.load();
            
            setStatus("wait, please");
            
            settings = new SettingsScreen(this);
            settings.setLocation(fieldArea.x, fieldArea.y);
            settings.setSize(fieldArea.width, fieldArea.height);
            
            add(settings);
            
            field = new Field(this);
           
            Dimension size = getSize();
            if (size.width < WIDTH || size.height < HEIGHT) {
                setStatus("Warning",
                        "Wrong applet size: " + size.width + "x" +
                        size.height);
                Thread.sleep(3000);
            } else {
                setStatus(TITLE, "by " + AUTHOR);
                Thread.sleep(1500);
            }
           
            if (statusCenter) {
                setStatus("version " + VERSION, "by " + AUTHOR, false);
            }
            
        } catch (Throwable e) {
            e.printStackTrace();
            setStatus("Error", e.getLocalizedMessage());
            
            return;
        }
    }
    
    void showSettings() {
        settings.setVisible(true);
        renderField = false;
        repaint();
    }
    
    void showField() {
        renderField = true;
        settings.setVisible(false);
        repaint();
    }
    
    private static boolean checkJavaVersion(int major, int minor) {
        try {
            String version = System.getProperty("java.version");                        
            
            if (version == null)
                return false;
            
            StringTokenizer tokenizer = new StringTokenizer(version, "._");
            
            // Check major version
            if (!tokenizer.hasMoreTokens())
                return false;
            
            if (Integer.parseInt(tokenizer.nextToken()) > major)
                return true;
            
            // Check minor version
            if (!tokenizer.hasMoreTokens())
                return false;
            
            if (Integer.parseInt(tokenizer.nextToken()) >= minor)
                return true;
            
        } catch (SecurityException e) {
            System.err.println("Error checking Java version: " + e);
        } catch (NumberFormatException e) {
            System.err.println("Error checking Java version: " + e);
        }
        
        return false;
    }
    
    /**
     * Try to enable antialiasing if supported by Java VM.
     */
    private static void setAA(Graphics g, final boolean textOnly) {
        if (!antialias) {
            return;
        }

        // Java 1.1 compatibility hack.
        try {
            (new Object() {
                void setAA(Graphics g) throws Throwable {
                    ((Graphics2D) g).addRenderingHints(
                            new RenderingHints(
                            RenderingHints.KEY_TEXT_ANTIALIASING,
                            RenderingHints.VALUE_TEXT_ANTIALIAS_ON));
                    
                    if (textOnly) {
                        ((Graphics2D) g).addRenderingHints(
                                new RenderingHints(
                                RenderingHints.KEY_ANTIALIASING,
                                RenderingHints.VALUE_ANTIALIAS_DEFAULT));
                    } else {
                        ((Graphics2D) g).addRenderingHints(
                                new RenderingHints(
                                RenderingHints.KEY_ANTIALIASING,
                                RenderingHints.VALUE_ANTIALIAS_ON));
                    }
                }
            }).setAA(g);
        } catch (Throwable e) {
            // Requred API is not available?
            System.err.println(e);
            antialias = false;
        }
    }
    
    /*
     * JRE bug workaround: create non-accelerated offscreen images.
     */
    public Image createImage(final int width, final int height) {
        if (!checkJavaVersion(1, 3))
            return super.createImage(width, height);

        // Java compatibility hack.
        try {
            return (new Object() {
                Image createImage() throws Throwable {
                    ColorModel cm =
                            getGraphicsConfiguration().getColorModel(
                            Transparency.OPAQUE);

                    return new BufferedImage(
                            cm,
                            cm.createCompatibleWritableRaster(width, height),
                            cm.isAlphaPremultiplied(), null);
                }
            }
            ).createImage();
        } catch (Throwable e) {
            // Requred API is not available?
            System.err.println(e);
            return super.createImage(width, height);
        }
    }
    
    public void update(Graphics g) {
        if (offScreenImage == null) {
            offScreenImage = createImage(WIDTH, HEIGHT);
            offScreenGraphics = offScreenImage.getGraphics();
        }

        
        offScreenGraphics.setClip(g.getClip());
        paint(offScreenGraphics);
        g.drawImage(offScreenImage, 0, 0, this);
    }
    
    private void paintLink(Graphics g) {
        int baseLine;
        
        if (DISPLAY_URL) {
            g.setColor(skin.linkColor);
            g.setFont(skin.linkFont);
            FontMetrics fm = g.getFontMetrics();
            urlArea.width = fm.stringWidth(HOME_PAGE);
            baseLine = urlArea.y + (urlArea.height - fm.getHeight()) / 2 +
                    fm.getAscent();
            g.drawString(HOME_PAGE, urlArea.x, baseLine);
        }
        
        if (DISPLAY_URL && titleHover && skin.activeTitle != null) {
            g.drawImage(skin.activeTitle, 0, 0, this);
            g.drawLine(urlArea.x, baseLine + 1,
                    urlArea.width + urlArea.x - 1, baseLine + 1);
        } else if (skin.title != null) {
            g.drawImage(skin.title, 0, 0, this);
        }
    }
    
    private void paintStatus(Graphics g) {
        synchronized (statusLock) {
            if (statusCenter) {
                Font f1 = skin.messageFont1;
                Font f2 = skin.messageFont2;
                FontMetrics fm1 = g.getFontMetrics(f1);
                FontMetrics fm2 = g.getFontMetrics(f2);
                
                String s2 = this.status2;
                int s2length = s2.length();
                int s2width;
                while ((s2width = fm2.stringWidth(s2)) > WIDTH - 40) {
                    s2length--;
                    s2 = status2.substring(0, s2length) + "...";
                }
                status2 = s2;
                
                int y1 = (HEIGHT - fm1.getHeight() - fm2.getHeight() - 5) / 2;
                int y2 = y1 + 5 + fm1.getHeight();
                int x1 = (WIDTH - fm1.stringWidth(status1)) / 2;
                int x2 = (WIDTH - s2width) / 2;
                int x3 = (x1 < x2) ? x1 : x2;
                int y3 = (y1 < y2) ? y1 : y2;
                
                g.setColor(skin.messageBackColor);
                
                g.fillRoundRect(x3 - 15, y3 - 15,
                        WIDTH + 30 - x3 * 2, HEIGHT + 30 - y3 * 2, 40, 40);
                g.setColor(skin.messageShadowColor);
                g.drawRoundRect(x3 - 14, y3 - 14,
                        WIDTH + 30 - x3 * 2, HEIGHT + 30 - y3 * 2, 40, 40);
                g.setColor(skin.messageBorderColor);
                g.drawRoundRect(x3 - 15, y3 - 15,
                        WIDTH + 30 - x3 * 2, HEIGHT + 30 - y3 * 2, 40, 40);
                
                g.setFont(f1);
                g.setColor(skin.messageShadowColor);
                g.drawString(status1, x1 + 1, y1 + fm1.getAscent() + 1);
                g.setColor(skin.messageTextColor1);
                g.drawString(status1, x1, y1 + fm1.getAscent());
                
                g.setFont(f2);
                g.setColor(skin.messageShadowColor);
                g.drawString(status2, x2 + 1, y2 + fm2.getAscent() + 1);
                g.setColor(skin.messageTextColor2);
                g.drawString(status2, x2, y2 + fm2.getAscent());
            } else {
                g.setFont(skin.statusFont);
                FontMetrics fm = g.getFontMetrics();
                int sh = fm.getHeight();
                int sw1 = fm.stringWidth(status1);
                int sw2 = fm.stringWidth(status2);
                int x1 = (statusArea.width - sw1) / 2 + statusArea.x;
                int x2 = (statusArea.width - sw2) / 2 + statusArea.x;
                int y1 = (statusArea.height / 2 - fm.getHeight()) / 2
                        + statusArea.y;
                int y2 = y1 + statusArea.height / 2;
                
                g.setColor(skin.statusTextColor);
                g.drawString(status1, x1, y1 + fm.getAscent());
                g.drawString(status2, x2, y2 + fm.getAscent());
            }
        }
    }
    
    public void paint(Graphics g) {
        Rectangle clip = g.getClipBounds();
        
        if (clip == null)
            clip = new Rectangle(0, 0, WIDTH, HEIGHT);
        
        synchronized (skin.getSkinLock()) {
            setAA(g, !statusCenter);
            
            if (!rectangleContains(fieldArea, clip)) {
                g.setColor(skin.backColor);
                g.fillRect(0, 0, WIDTH - 1, HEIGHT - 1);
                if (skin.background != null)
                    g.drawImage(skin.background, 0, 0, this);
                
                paintLink(g);
            }
            
            super.paint(g);
            
            if (renderField && fieldArea.intersects(clip)) {
                field.render(g, fieldArea.x, fieldArea.y,
                        fieldArea.width, fieldArea.height);
            }
            
            if (statusArea.intersects(clip) || statusCenter) {
                paintStatus(g);
            }
        }
        
        // TEST
        //g.setColor(Color.red);
        //g.drawRect(clip.x, clip.y, clip.width - 1, clip.height - 1);
    }
    
    
    void repaintField() {
        repaint(fieldArea.x, fieldArea.y, fieldArea.width, fieldArea.height);
    }
    
    void repaintField(int x, int y, int width, int height) {
        repaint(fieldArea.x + x, fieldArea.y + y, width, height);
    }
    
    // MS Java does not implement Rectangle.contains().
    private boolean rectangleContains(Rectangle r1, Rectangle r2) {
        return r2.x >= r1.x && r2.y >= r1.y &&
                r2.x + r2.width <= r1.x + r1.width &&
                r2.y + r2.height <= r1.y + r1.height;
    }
    
    void setStatus(String s1, String s2, boolean center) {
        boolean lsc = statusCenter;
        
        synchronized (statusLock) {
            statusCenter = center;
            if (s1 != null) status1 = s1;
            if (s2 != null) status2 = s2;
        }
        
        if (lsc || center)
            repaint();
        else
            repaint(statusArea.x, statusArea.y,
                    statusArea.width, statusArea.height);
    }
    
    void setStatus(String s1, String s2) {
        setStatus(s1, s2, statusCenter);
    }
    
    void setStatus(String s2) {
        setStatus(null, s2);
    }
    
    private void setTitleHover(boolean atitle) {
        this.titleHover = atitle;
        if (atitle)
            setCursor(new Cursor(Cursor.HAND_CURSOR));
        else
            setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
        repaint(0, 0, skin.title.getWidth(null), skin.title.getHeight(null));
        repaint(urlArea.x, urlArea.y, urlArea.width, urlArea.height);
    }
    
    public void mouseMoved(MouseEvent e) {
        if (!DISPLAY_URL) return;
        
        int x = e.getX();
        int y = e.getY();
        if ((skin.title != null &&
                x <= skin.title.getWidth(null) &&
                y <= skin.title.getHeight(null)) ||
                (x >= urlArea.x &&
                y >= urlArea.y &&
                x <= urlArea.width + urlArea.x &&
                y <= urlArea.height + urlArea.y)) {
            if (titleHover) return;
        } else {
            if (!titleHover) return;
        }
        setTitleHover(!titleHover);
    }
    
    public void mouseExited(MouseEvent e) {
        if (!titleHover) return;
        setTitleHover(false);
    }
    
    public void mouseClicked(MouseEvent e) {
        requestFocus();
        
        if (!titleHover)
            return;
        
        if (application) {
            openInBrowser(HOME_PAGE);
        } else {
            try {
                getAppletContext().showDocument(new URL(HOME_PAGE), "_self");
            } catch (MalformedURLException ex) {
                ex.printStackTrace();
            }
        }
    }
    
    private void openInBrowser(final String address) {
        try {
            (new Object() {
                void browse(String address) throws Exception {
                    Desktop.getDesktop().browse(new URI(address));
                }
            }
            ).browse(address);
        } catch (Throwable e) {
            System.err.println("BM Tron: " + e);
            System.err.println(
                    "         Could not launch external browser.");
        }
    }
    
    public void mouseEntered(MouseEvent e) {}
    public void mousePressed(MouseEvent e) {}
    public void mouseReleased(MouseEvent e) {}
    public void mouseDragged(MouseEvent e) {}
    
    static final Random random = new Random();
    
    static int nextRandom(int lowerbound, int upperbound) {
        return (int)((upperbound - lowerbound + 1) *
                random.nextFloat() + lowerbound);
    }
    
    public Dimension getPreferredSize() {
        return new Dimension(WIDTH, HEIGHT);
    }
    
    public boolean isDoubleBuffered() {
        return true;
    }
}
