/** 
 *  BlackHole Source File
 *
 *  GNU Copyright (C) 2008  Gaspar Sinai gaspar(at)adys.org 
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License, version 2,
 *  dated June 1991. See file COPYYING for details.
 *
 *  This program 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 this program; if not, write to the Free Software
 *  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

package sinai.gaspar.blackhole;

import java.awt.Panel;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Image;

/**
 * This is the panel where the animated Ulam Spiral is drawn.
 * This is used from BlackHole.java applet, but it could be
 * used stand-alone.
 *
 * @author Gaspar Sinai
 * @version 2008-04-30
 */
public class BoardPanel extends Panel implements IOptions {

    private int cells = 1;
    private int origCells = 1;
    private int type = IOptions.TYPE_SPIRAL;

    private CoordConverter coordConverter = new CoordConverter (type, cells);

    private int cellSize = 4;
    private int origCellSize = 4;

    private boolean showCounter = true;

    private long delay = 100L; // ms
    private long origDelay = 100L; // ms

    private PrimeStack primeStack = new PrimeStack (cells);

    Color foreground = BoardPanel.parseColor ("000000");
    Color background = BoardPanel.parseColor ("ffffff");

    TimerThread timerThread = null;
    IChangeListener changeListener = null;

    /**
     * Create a default BoardPanel.
     */
    public BoardPanel() {
        setBackground (background);
        setForeground (foreground);
    }

    public void setChangeListener (IChangeListener changeListener) {
        this.changeListener = changeListener;
        primeStack.setChangeListener (changeListener);
    }

    /**
     * Set the number of cells in a row/column.
     */
    public void setCells (int cells) {
        this.cells = cells;   
        this.origCells = cells;   
        coordConverter = new CoordConverter (type, cells);
        long start = primeStack.getStart ();
        primeStack = new PrimeStack (cells);
        primeStack.setChangeListener (changeListener);
        if (start != primeStack.getStart()) {
            primeStack.setStart (start);
        }
    }

    public long getStart () {
        return primeStack.getStart ();
    }

    /**
     * Set the size of a cell.
     */
    public void setCellSize (int cellSize) {
        this.cellSize = cellSize;   
        this.origCellSize = cellSize;   
    }

    /**
     * Zoom in and out.
     */
    public int zoom (int value) {
        int newCellSize = origCellSize;
        int newCells = origCells;
        int size = origCellSize * origCells;
        // Zoom in
        int count = 0;
        if (value > 0) {
            while (count < value) {
                if (newCellSize == size) break;
                int test = newCellSize+1;
                while (test != size && size % test  != 0) {
                    test++;
                } 
                newCells = size / test;
                newCellSize = test;
                count++;
            }
        } else if (value < 0) { // Zoom-out
            while (count > value) {
                if (newCellSize == 1) break;
                int test = newCellSize - 1;
                while (test > 0 && size % test  != 0) {
                    test--;
                } 
                newCells = size / test;
                newCellSize = test;
                count--;
            }
        }
        int saveOrigCellSize = origCellSize;
        int saveOrigCells = origCells;
        synchronized (timerThread.lock) {
            setCells (newCells);
            setCellSize (newCellSize);
            timerThread.setPrimeStack (primeStack);
        }
        origCellSize = saveOrigCellSize;
        origCells = saveOrigCells;
        repaint();
        return count;
    }

    // Double or half the speed.
    public int speed (int value) {
        long delaySaved = origDelay;
        long newDelay = origDelay;
        int count = 0;
        if (value > 0) {
            while (count < value && newDelay > 10) {
                newDelay = (count % 3 != 1)
                   ? newDelay / 2 : newDelay * 10/ 25  ;
                count++;
            }
            if (newDelay < 10) newDelay = 10;
        } else  if (value < 0) {
            while (count > value && newDelay <= 10000) {
                newDelay = (-count % 3 != 1) ? newDelay * 2 
                   : newDelay * 25 / 10;
                count--;
            }
            if (newDelay > 10000) newDelay = Long.MAX_VALUE;
        }
        setDelay (newDelay);
        origDelay = delaySaved;
        return count;
    }

    public boolean isDelayLimited () {
        if (timerThread == null) return false;
        long ret = timerThread.getDelay ();
        return (ret <= 10L || ret == Long.MAX_VALUE);
    }

    public boolean isZoomLimited () {
        return (cells == 1 || cellSize == 1);
    }

    /**
     * Move back one frame.
     */
    public void back () {
        primeStack.back();
        repaint();
    }

    /**
     * Move forward one frame.
     */
    public void forward () {
        primeStack.forward();
        repaint();
    }

    /**
     * Set the size of a cell.
     */
    public void setDelay (long delay) {
        this.delay = delay;   
        this.origDelay = delay;   
        if (timerThread != null) {
            timerThread.setDelay (delay);
        }
    }

    /**
     * Set the size of a cell.
     */
    public void setStart (long start) {
        primeStack.setStart (start);
        repaint ();
    }

    /**
     * Set the background color 
     */
    public void setBackground (Color background) {
        this.background = background;
        super.setBackground (background);
    }
    /**
     * Set the foreground color 
     */
    public void setForeground (Color foreground) {
        this.foreground = foreground;
        super.setForeground (foreground);
    }
    /**
     * Toggle counter at the right-bottom edge of the screen.
     * @param showCounter is true if couter is shown.
     */
    public void setShowCounter (boolean showCounter) {
        this.showCounter = showCounter;   
    }

    /**
     * Set the type of the diagram.
     * @see IOptions#setType
     */
    public void setType (int type) {
        coordConverter = new CoordConverter (type, cells);
        this.type = type;
        repaint();
    }

    /**
     * Start animation.
     */
    public void start () {
        // do a shift and repaint();
        if (timerThread == null) {
            timerThread = new TimerThread (delay, primeStack, this);
            timerThread.th_start ();
        } else {
             timerThread.th_resume ();
        }
    }

    /**
     * Stop animation.
     */
    public void stop () {
        // do a shift and repaint();
        timerThread.th_stop ();
    }

    /**
     * Stop animation.
     */
    public void destroy () {
        // do a shift and repaint();
        timerThread.th_destroy ();
    }

    /**
     * In awt this is called first, and paint is called from here.
     * We implemented a double buffer here.
     */
    public void update (Graphics graphics) {
        Image db = createImage (cells*cellSize, cells*cellSize);
        if (db == null) {
            return;
        }
        Graphics dbg = db.getGraphics();
        paint (dbg);
        graphics.drawImage (db, 0, 0, this);
    }

    /**
     * Paint ourself.
     */
    public void paint (Graphics graphics) {
        // Clear the area
        graphics.setColor (background);
        graphics.fillRect(0, 0, cells*cellSize, cells*cellSize);
        graphics.setColor (foreground);
        // The real center.
        long maxNum = cells * cells  + primeStack.getStart();
        int digits = countDigits (maxNum);
        boolean showNumber = showCounter && (digits * 8 + 8 < cellSize);
        for (int y=0; y<cells; y++) {
           for (int x=0; x<cells; x++) {
                int index = coordConverter.convert(x, y);
                if (index < 0) continue;
                int cx = x*(int)cellSize;
                int cy = y*(int)cellSize;
                if (primeStack.isPrimeAt (index)) {
                    // showNumber overwrites out foreground
                    if (showNumber) graphics.setColor (foreground);
                    graphics.fillRect (cx, cy, cellSize, cellSize);
                    if (showNumber) {
                        printNum (graphics, 
                            cx + (cellSize + digits * 8)/2-8, 
                            cy + (cellSize/2) - 8, 
                            primeStack.getStart() + (long) index, background);
                    }
                } else {
                    if (showNumber) {
                        printNum (graphics, 
                            cx + (cellSize + digits * 8)/2-8, 
                            cy + (cellSize/2) - 8, 
                            primeStack.getStart() + (long) index, foreground);
                    }
                }
            }
        }
        if (showCounter && !showNumber) {
            printNum (graphics, cells*cellSize - 8, 
                cells*cellSize - 16, primeStack.getStart(), null);
        }
    }

    private static int countDigits (long num) {
        int ret = 1;
        while (num/10 != 0) {
            num = num / 10;
            ret++;
        }
        return ret;
    }

    public void print (Graphics graphics) {
        paint (graphics);
    }

    /**
     * @param color is RRGGBB
     */
    public static Color parseColor (String color) {
        try {
            String r = color.substring(0,2);
            String g = color.substring(2,4);
            String b = color.substring(4,6);
            return new Color (Integer.parseInt(r, 16), 
                Integer.parseInt(g, 16), Integer.parseInt (b, 16));
        } catch (Exception e) {
        }
        return Color.BLACK;
    }

    /**
     * From GNU Unifont. 0..9
     * http://en.wikipedia.org/wiki/GNU_Unifont
     */
    static String FONT[] = { 
            "00000000182442424242424224180000",
            "000000001030501010101010107C0000",
            "000000003C4242020C102040407E0000",
            "000000003C4242021C020242423C0000",
            "00000000040C142444447E0404040000",
            "000000007E4040407C020202423C0000",
            "000000001C2040407C424242423C0000",
            "000000007E0202040404080808080000",
            "000000003C4242423C424242423C0000",
            "000000003C4242423E02020204380000"
    };

    /**
     * Print a right-alinged number in a 8x16 area.
     * If fg is numm clear the rect.
     */
    public void printNum (Graphics graphics, int x, int y, long num, Color fg) {
        if (num >= 10) printNum (graphics, x-8, y, num/10, fg);
        num = num%10;
        String f = FONT[(int)num];
        if (fg == null) {
            graphics.setColor (background);
            graphics.fillRect (x, y, 8, 16);
            graphics.setColor (foreground);
        } else {
            graphics.setColor (fg);
        }
        for (int row=0; row<16; row++) {
           int b = Integer.parseInt (f.substring(row*2, row*2+2), 16);
           for (int column=0; column<8; column++) {
               if ((b & 0x80) == 0x80) {
                   graphics.fillRect (x+column, y+row, 1, 1);
               }
               b = b << 1;
            }
        }
    }
}
