/**
 * @(#)Display.java
 *
 * <code>Display</code> class can be considered as the "core" of
 * the application since all the other methods are
 * designed as "helpers" to Display. The class visualizes
 * the main window and also consists (mostly anonymous) KeyListeners,
 * ActionListeners and MouseListeners.
 * It also uses threads.
 * 
 * @author Devrim Sahin
 * @version 1.00 21.12.2009
 */

 package bin;
 
import java.awt.Image;
import java.awt.image.BufferStrategy;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Font;
import java.awt.RenderingHints;
import java.awt.FontMetrics;
import java.awt.Point;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import javax.swing.JFrame;
import javax.swing.JFileChooser;
import javax.swing.filechooser.FileNameExtensionFilter;
import javax.imageio.ImageIO;
import java.io.File;
import java.util.StringTokenizer;

public class Display extends JFrame implements MouseListener, MouseMotionListener {
	
	/**
	 * <code>serialVersionUID</code><br>
	 * Created by Eclipse.
	 */
	private static final long serialVersionUID = -2504741879991406219L;

	/**
	 * <code>relaxLoop</code><br>
	 * The thread managing the relaxation loop.
	 */
	private Thread relaxLoop;
	
	/**
	 * <code>fntNotify</code><br>
	 * The AnimatedNotifier object used when the font is being changed.
	 */
    private static final AnimatedNotifier fntNotify = new AnimatedNotifier("fnt");
	
	/**
	 * <code>readNotify</code><br>
	 * The AnimatedNotifier object used when a file is being read.
	 */
    private static final AnimatedNotifier readNotify = new AnimatedNotifier("readin");
		
	/**
	 * <code>PAGEWIDTH</code><br>
	 * Width of the "page" (500 pixels).
	 */
    public static final int PAGEWIDTH = 500;

	/**
	 * <code>PAGEHEIGHT</code><br>
	 * Height of the "page" (700 pixels).
	 */
    public static final int PAGEHEIGHT = 700;

    /**
	 * <code>marginH</code><br>
	 * Horizontal margins (50 pixels).
	 */
    private static final int marginH = 50;
    
    /**
	 * <code>marginV</code><br>
	 * Vertical margins (70 pixels).
	 */
    private static final int marginV = 70;

    /**
	 * <code>cf</code><br>
	 * Configurations object. Holds every 
	 * preference that should be saved.
	 * @see Configurations
	 */
    private Configurations cf;
	
    /**
	 * <code>BGImage</code><br>
	 * Background image (read from Configurations object).
	 */	
    private Image BGImage;
    
    /**
	 * <code>bs</code><br>
	 * BufferStrategy object.
	 */	
    private BufferStrategy bs = null;

    /**
	 * <code>paginator</code><br>
	 * Paginator object.
	 * @see Paginator
	 */
    private Paginator paginator = null;

    /**
	 * <code>cont</code><br>
	 * cont String holds all the content of the book.
	 * This may seem like an unnecessary waste of memory.
	 * However, holding a copy of the book at the memory
	 * prevents the reader from having some problems when
	 * the book is deleted/damaged while being read.
	 */
    private String cont;
    
    /**
     * <code>menu</code><br>
     * The reference object to the Menu object.
	 * @see Menu
     */
    private Menu menu;
    
    /**
	 * <code>currentPage</code><br>
	 * Holds the current page.
	 */
    private String currentPage;
    
    /**
	 * <code>location</code><br>
	 * A <code>Point</code> object used in dragging events.
	 */
	private Point location;
	
    /**
	 * <code>pressed</code><br>
	 * A <code>MouseEvent</code> object used in dragging events.
	 */
	private MouseEvent pressed;
    
    /**
	 * <code>relaxVal</code><br>
	 * The transparency value for "Eye Relaxing Mode".<br>
	 * Initially 128.
	 */
    private int relaxVal = 128;

    /**
	 * Default constructor. Creates a Display object
	 * which is undecorated, unresizable, centered 
	 * with size <code>PAGEWIDTH</code> by <code>PAGEHEIGHT</code>.
	 * Also adds Mouse, MouseMotion and Key Listeners; loads images,
	 * configurations, book itself and font, starts the "relaxation"
	 * thread, creates the menu ...
	 */
    public Display() {
    	// Set the title (Which will only be shown in the taskbar)
    	super(Language.getText(4));
    	// Set the size to PAGEWIDTH x PAGEHEIGHT
	    setSize(PAGEWIDTH,PAGEHEIGHT);
	    // Make unresizable
	    setResizable(false);
	    // Make undecorated
	    setUndecorated(true);
	    // Center to the screen
	    setLocationRelativeTo(null);
	    // When closed, exit
		setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		// Add this as a mouse listener...
	    addMouseListener( this );
	    // ...and mouse motion listener
		addMouseMotionListener( this );
		
		// Start the notifier
		readNotify.start();
		
		// Create a new Configurations object
		cf = new Configurations();
		// Try to read configurations from a file
		cf.loadConfigurations();
		
		// Create the menu and assign it
		menu = new Menu(this,cf);
		
		// Create an anonymous KeyAdapter and add it as a KeyListener
	    addKeyListener(new KeyAdapter() {
		    // Determine what to do when a key is released
			public void keyReleased(KeyEvent ev) {
				// Call the procedure pressKey to do some stuff
				pressKey(ev);
			}
	    });
	    
		// Create the relaxation loop
		createRelaxLoop();
		
		// Try to ...
		try {
			// ... load the background image
	        BGImage = ImageIO.read(new File(cf.getBGImagePath()));
	    // If an exception is thrown
		} catch(Exception ex) {
			// Set BGImage to null
			BGImage = null;
		}
		
		// Try to ...
		try {
			// ... read the file using ReadFiles class
			cont = ReadFiles.readPlainFile(cf.getBookPath());
		// If an error occurs
		} catch(Exception e) {
			// Show the manual
			cont = Language.getHelp();
		}
		
		// All files are loaded, stop the animation
    	readNotify.stop();
    	
		// Apply the font stored in Configurations object
		changeFont(cf.getFont());
    }

	/*
	 * <code>createRelaxLoop</code>
	 * Recreates the relaxationLoop to prevent 
	 * a ThreadStateException (which is caused by
	 * an already-used relaxLoop)
	 */
	public void createRelaxLoop() {
		// Create a thread ...
		relaxLoop = new Thread () {
			// ... which will ...
			public void run() {
				// ... continuously ...
				while (isVisible()) {
					// ... check if "relaxation mode" is open ...
					if (cf.getRelax()) {
						// ... and update relaxVal if so. The point is
						// to make relaxVal increase up to 255, and to
						// make it 0 again, and continue increasing.
						// It can be solved this way:
						// relaxVal = (relaxVal+1) % 256;
						// But remainder is a slow operation, therefore
						// we used bitwise AND operator (&):
						// Of course, no change is observed.
						// Bitwise rules anyway.
						relaxVal = (relaxVal + 1) & 255;
						// Repaint
						repaint();
					}
					
					// Wait for 50 milliseconds, to not "choke"
					// the processor(s).
					AnimatedNotifier.wait(50);
				}
			}
		};
	}


	/**
	 * <code>setVisible</code><br>
	 * This method is an extended version of the standard 
	 * setVisible() method. If the window is about to appear 
	 * on the monitor, this method starts the relaxation loop.
	 * @param b The boolean to set the frame visible or invisible.
	 */
	public void setVisible(boolean b) {
		// Do its real job
		super.setVisible(b);
		// If trying to set the frame visible
		if (b && !relaxLoop.isAlive()) {
			// Recreate the relaxation loop
			createRelaxLoop();
			// Start the relaxation loop
			relaxLoop.start();
		}
	}

    /**
     * <code> mousePressed </code><br>
     * Called when mouse is pressed. Used for 
     * dragging feature. Do not call directly.
     * @param me the MouseEvent object.
     */
	public void mousePressed(MouseEvent me) {
		// Store the event object in pressed.
		pressed = me;
	}

    /**
     * <code> mouseDragged </code><br>
     * Called when mouse is dragged. Used for 
     * dragging feature. Do not call directly.
     * @param me the MouseEvent object.
     */
	public void mouseDragged(MouseEvent me) {
		// Get the current location of the window
		location = getLocation(location);
		// If not on a menu item
		if(menu.isIdle(pressed)) {
			// Drag in x-axis
			int x = location.x - pressed.getX() + me.getX();
			// Drag in y-axis
			int y = location.y - pressed.getY() + me.getY();
			// Set the new location
			setLocation(x, y);
		}
		// Consume the event so that it is not used again
		me.consume();
	}
	
    /**
     * <code> mouseReleased </code><br>
     * Called when mouse is released. Used for 
     * toolbar feature. Do not call directly.
     * @param e the MouseEvent object.
     */
	public void mouseReleased(MouseEvent e) {
		// Call menu's mouseReleased method
		menu.mouseReleased(e);
	}
	
    /**
     * <code> mouseClicked </code><br>
     * Called when mouse is clicked. 
     * Not used. Do not call directly.
     * @param e the MouseEvent object.
     */
	public void mouseClicked(MouseEvent e) {}

    /**
     * <code> mouseMoved </code><br>
     * Called when mouse is moved. Used for 
     * toolbar feature. Do not call directly.
     * @param e the MouseEvent object.
     */
    public void mouseMoved(MouseEvent e) {
    	// Call menu's mouseMoved method
    	menu.mouseMoved(e);
    }
	
    /**
     * <code> mouseEntered </code><br>
     * Called when mouse entered somewhere.
     * Not used. Do not call directly.
     * @param e the MouseEvent object.
     */
	public void mouseEntered(MouseEvent e) {}

    /**
     * <code> mouseExited </code><br>
     * Called when mouse exited from somewhere.
     * Not used. Do not call directly.
     * @param e the MouseEvent object.
     */
	public void mouseExited(MouseEvent e) {}
	
    /**
     * <code> paint </code><br>
     * Paint event with BufferStrategy.
     * Do not call directly.
     * @param gee Graphics object.
     */	
	public void paint (Graphics gee) {
		try {
			// Get BufferStrategy from the window
			bs = getBufferStrategy();
			// Get Graphics from bs
			Graphics g = bs.getDrawGraphics();
			// Draw to the Graphics object
			draw(g);
			// Dispose the Graphics object
			g.dispose();
			// Show the BufferStrategy object
			bs.show();
		// At any error
		} catch (Exception e) {
			// This should never happen.
			// Just in case, draw to the 
			// graphics object which is 
			// taken as a parameter
			draw (gee);
		}
	}
	
    /**
     * <code> draw </code><br>
     * Does all the drawing job to the Graphics object
     * taken. Uses Text Antialiasing.
     * Note: This method acknowledges that the Graphics
     * object is an instance of Graphics2D class!
     * @param g Graphics object.
     */	
    private void draw(Graphics g) {
    	// Cast the Graphics object to Graphics2D
        Graphics2D g2d = (Graphics2D) g;
        // Use text antialiasing
        g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
		// Set the background color
		g2d.setColor(cf.getBackColor());
		// Fill the window
		g2d.fillRect(0,0,PAGEWIDTH,PAGEHEIGHT);
		
		// If using a bgImage is both possible and allowed
		if (!(BGImage == null) && cf.isUseBGImage() ) {
			// Draw the image; if it is not at the same size with the window, stretch it
			g2d.drawImage(BGImage,0,0,PAGEWIDTH,PAGEHEIGHT,null);
		}
		
		// Print the page
		printPage(g2d,currentPage);
        
        // If "Eye Relaxation Mode" is on
        if (cf.getRelax()) {
        	// Create a black color with alpha value of
        	// Math.abs(relaxVal-128)
        	// Instead of calling Math.abs(), we used a 
        	// simple if statement
        	g2d.setColor(new Color (0,0,0,(relaxVal>128)?relaxVal-128:128-relaxVal));
        	// Fill the screen
        	g2d.fillRect(0,0,PAGEWIDTH,PAGEHEIGHT);
        }
        // Render the menu
        menu.render(g2d);
    }
   
    /**
     * <code> changeFont </code><br>
     * Doesn't do anything but calling the actual (and 
     * private) changeFont() method with two parameters:
     * font and content String. The aim of this method is
     * to allow FontChooser class change the font without
     * needing the content of the book.
     * @param font Font object.
     */	
	public void changeFont (Font font) {
		// Call the actual method
		changeFont(cont,font);
	}

    /**
     * <code> changeFont </code><br>
     * The real changeFont method. Renews the Pagination
     * object using the content String, which is actually
     * the book itself.
     * @param content String object.
     * @param font Font object.
     */
	private void changeFont(final String content, final Font font){
		// If font isn't changed
		if (font.equals(getFont()))
			// Don't bother doing anything
			return;
		
		// Start the notifier
		fntNotify.start();
		
		// Hide the window, because the process of changing seems a bit ugly
		setVisible(false);
		
		// Set the font
		setFont(font);
		
		// Update the config
		cf.setFont(font);
		
		// Update the config file
		updateConfig();
		
		// Create a new Thread ...
		new Thread() {
			// ... which will ...
			public void run() {
				// ... create a new FontMetrics object
				FontMetrics metrics = getFontMetrics(font);
				// Renew the paginator
        		paginator = new Paginator(content,metrics, PAGEWIDTH - (2*marginH), PAGEHEIGHT-(2*marginV));
        		// Go to the current page
				currentPage = paginator.getPage(cf.getPageNum());
				// Stop the notifier
        		fntNotify.stop();
        		// Make the window visible again
    			setVisible(true);
        		// Create 2 buffers
				createBufferStrategy(2);
				// Repaint the window
				repaint();
			}
		// Start it
		}.start();
	}

    /**
     * <code> printPage </code><br>
     * The printPage algorithm was designed to print a page
     * without a need for window's sizes. To accomplish such
     * a thing, bufferText should be splitted at the line ends.
     * In other words, bufferText should be already formatted 
     * and shaped in such a way that it will seem good when printed.
     * Paginator class handles it by storing the book in pages 
     * which has line breaking characters at the end of each
     * line. This way, the book is "shaped" only once and users can 
     * navigate through the pages rapidly.
     * @param g2d Graphics2D object.
     * @param bufferText String object.
     */
	private void printPage(Graphics2D g2d,String bufferText) {
		// Create a FontMetrics object
		FontMetrics metrics = getFontMetrics(getFont());
		// Set g2d's font ...
		g2d.setFont(getFont());
		// ...and color.
		g2d.setColor(cf.getFontColor());
		// Initialize spaceRemaining
		int spaceRemaining = PAGEWIDTH-(2*marginH);
		// Initialize h
        int h=metrics.getAscent();
        // Tokenize bufferText according to \n and \t. Include these characters.
		StringTokenizer sT = new StringTokenizer(bufferText,"\n\t",true);
		
		// Until the end of the page
		while (sT.hasMoreTokens() ) {
			// Read next token
			String nT = sT.nextToken();
			// If next token is \n
			if (nT.equals("\n")) {
				// Reset spaceRemaining
				spaceRemaining = PAGEWIDTH-(2*marginH);
				// Increase h by line height
				h+=metrics.getAscent();
			// Else if it is \t
			} else if (nT.equals("\t")) {
				// Leave some space
				spaceRemaining = ( (spaceRemaining*10-1)/(PAGEWIDTH-(2*marginH)) )*(PAGEWIDTH-(2*marginH))/10;
			} else {
				// Draw the text
				g2d.drawString(nT,PAGEWIDTH-(spaceRemaining+marginH),h+marginV);
				// Decrease spaceRemaining by the width of the token
				spaceRemaining -= metrics.stringWidth(nT);
			}
		}
		// Prepare the page number string
	    String pageNumString = (paginator.getCurrentPageNum()+1) + "/" + paginator.getPageCount();
	    // and print it
	    g2d.drawString(pageNumString,PAGEWIDTH-metrics.stringWidth(pageNumString)-marginH,marginV-metrics.getAscent());
    }
    
    /** 
     * <code>updateConfig</code><br>
     * Updates the font again for safety and saves
     * the configurations.
     */
    public void updateConfig() {
    	// Update the font
    	cf.setFont(getFont());
    	// Save the configurations
    	cf.saveConfigurations();
    }
    
    /**
     * <code>prevPage</code><br>
     * Goes to the previous page. Used by the Menu class.
     */
    public void prevPage() {
		// Go to the previous page
		currentPage = paginator.getPrevPage();
		// Update the page change
		updatePageChange();
    }
    
    /**
     * <code>nextPage</code><br>
     * Goes to the next page. Used by the Menu class.
     */
   public void nextPage() {
		// Go to the previous page
		currentPage = paginator.getNextPage();
		// Update the page change
		updatePageChange();
    }
    
    /**
     * <code>lastPage</code><br>
     * Goes to the last page. Used by the Menu class.
     */
    public void lastPage() {
     	// Go to the last page
		currentPage = paginator.getPage(paginator.getPageCount()-1);
		// Update the page change
		updatePageChange();
    }
    
    /**
     * <code>firstPage</code><br>
     * Goes to the first page. Used by the Menu class.
     */
    public void firstPage() {
     	// Go to the first page
		currentPage = paginator.getPage(0);
		// Update the page change
		updatePageChange();
    }
    
    /**
     * <code>toPage</code><br>
     * Goes to a specified page. Used by the Menu class.
     * @param i Index of the page number.
     */
    public void toPage(int i) {
     	// Go to the specified page
		currentPage = paginator.getPage(i);
		// Update the page change
		updatePageChange();
    }
    
    /**
     * <code>updatePageChange</code><br>
     * Updates the Configurations class and repaints the screen.
     * Used by the Menu class.
     */
    private void updatePageChange() {
		// Repaint the screen
		repaint();
		// Apply the change to the Configurations object
		cf.setPageNum(paginator.getCurrentPageNum());
		// Update config file
		updateConfig(); 
	}
    
    /**
     * <code>changeBGImage</code><br>
     * Opens the "Choose BG Image" dialog and applies the chosen BG 
     * Image by calling setBGImage. Used by the Menu class.
     */
    public void changeBGImage() {
		// Set returnVal to -1
		int returnVal = -1;
		// Create a new JFileChooser object. Default Path : /images/backgrounds/
		JFileChooser chooser = new JFileChooser("./images/backgrounds/");
		
		// Restrict(filter) the files
		chooser.setFileFilter(new FileNameExtensionFilter(Language.getText(1)+" (Gif, Png, Jpg, Png)", "gif","jpg","bmp","png"));
		
		// show the open dialog, make it modal
		returnVal = chooser.showOpenDialog(this);
		// If selected option is not Open
		if (!(returnVal == JFileChooser.APPROVE_OPTION))
			// Don't do anything, just break
			return;
		// Set the background image
		setBGImage(chooser.getSelectedFile().getPath());
		// Repaint the screen
		repaint();
		// Update config file
		updateConfig();
     }


	/**
     * <code>setBGImage</code><br>
     * Sets the background image to the specified one.
     * @param tempPath The file path of the background image.
     */
	public void setBGImage(String tempPath) {
		// Try to load the new image...
		try {
			// ... and reading the file from that path.
	        BGImage = ImageIO.read(new File(tempPath));
	        // Then update the config
	        cf.setBGImagePath(tempPath);
	    // If an error occurs
		} catch(Exception ex) {
			// Don't do anything
		}
    }
    
    /**
     * <code>pressKey</code>
     * Used for doing some duties when a button is pressed.
     * Defined as a separate function to allow the Menu class
     * to have access to some events.
     * @param ev The KeyEvent object.
     */
    public void pressKey (KeyEvent ev) {
		// If LEFT or UP arrows are pressed
		if (ev.getKeyCode() == KeyEvent.VK_LEFT || ev.getKeyCode() == KeyEvent.VK_UP ) {
			// Go to the previous page
			prevPage();
		// If RIGHT or DOWN arrows are pressed
		} else if (ev.getKeyCode() == KeyEvent.VK_RIGHT || ev.getKeyCode() == KeyEvent.VK_DOWN ) {
			// Go to the next page
			nextPage();
		// If ESC is pressed
		} else if (ev.getKeyCode() == KeyEvent.VK_ESCAPE ) {
			// Close the application
			System.exit(0);
		// If R key is pressed
		} else if (ev.getKeyCode() == KeyEvent.VK_R ) {
			// Reset relaxVal
			relaxVal = 128;
			// Toggle Relaxation Mode
			cf.setRelax (!cf.getRelax());
			// Repaint the screen
			repaint();
			// Update config file
			updateConfig();
		}
	}
}