/**
 * Copyright (c) 2011, Martin Foster <martin@ethereal-realms.org>
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without 
 * modification, are permitted provided that the following conditions are met:
 * 
 *     * Redistributions of source code must retain the above copyright notice, 
 *       this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright 
 *       notice, this list of conditions and the following disclaimer in the 
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of the Ethereal-Realms.org nor the names of its
 *       contributors may be used to endorse or promote products derived
 *       from this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package org.lyra.Widgets;

import java.awt.Canvas;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferStrategy;
import java.awt.image.BufferedImage;

/**
 * Provides a ticker tape capability to the javax.swing.JLabel display in order
 * to permit them to display larger strings then they are physically able to
 * display.
 * <p>
 * This is an alternative to the TickerLabel method which makes use of the 
 * Swing methods directly.   Due to the ability of controlling buffering directly
 * it was hoped to have a smoother look, but seems to be on par with the swing
 * variant.
 * <p>
 * Currently this variant is to remain in case it can be worked out.  However at
 * the moment there is no immediate requirement for it, since mixing of AWT and
 * Swing components can lead to unexpected behaviours.
 * <p>
 * @author Martin Foster
 * @version 1.0.1, %Revision, 99%, %LastChangedDate, 17/03/11 8:17PM%
 */
public class TickerCanvas extends Canvas implements Runnable {

	// Serial Requirement
	private static final long serialVersionUID = 1L;

	/*
	 * INTERFACE ELEMENTS
	 */
	
	// Buffer Strategy
	private BufferStrategy myBuffer = null;
	
	// Stored Message
	private String myMessage;
	
	// Individual Elements
	private int myHeight  = 0;
	private int myWidth   = 0;
	private int myLimit   = 200;
	private int myDescent = 0;

	// Display area
	private Dimension mySize;
	
	// Need for re-processing
	private boolean Reprocess = true;
	private boolean Redraw    = true;
	private boolean Loop      = true;
	
	// Current Position
	private int myPosition = 0;

	// Storage of image
	private BufferedImage mySource;
	private BufferedImage myCrop;

	// Running thread
	private Thread myThread = null;
	
	/*
	 * CONSTANTS
	 */
	private static final int BUFFER = 1;
	
	
	/**
	 * Base constructor which initialises none of the elements.  
	 */
	public TickerCanvas() { 
		
		// Super class processing
		super();
		
		// Adjust metrics
		this.fetchMetrics();
	}
	// End of Constructor Class

	
	/**
	 * Constructor which takes the initial message as the base.
	 * <p>
	 * @param msg String message for display in ticker format.
	 */
	public TickerCanvas(String msg) {
		
		// Super class processing
		super();
		
		// Pass up
		this.myMessage = msg;
		
		// Adjust metrics
		this.fetchMetrics();
	}
	// End of Constructor Method
	
	
	/**
	 * Constructor which takes the initial message and font for use as the
	 * base.
	 * <p>
	 * @param msg String message for display in ticker format.
	 * @param font Font for display.
	 */
	public TickerCanvas(String msg, Font font) {
		
		// Super Call
		super();

		// Pass up
		this.myMessage = msg;
		
		// Adjust Font
		this.setFont(font);
		
		// Adjust metrics
		this.fetchMetrics();
	}
	// End of Constructor Method
	
	
	/**
	 * Constructor which takes the initial message, font and limit for use as 
	 * the base.
	 * <p>
	 * @param msg String message for display in ticker format.
	 * @param font Font for display.
	 * @param limit Maximum width which will lead to a ticker incident.
	 */
	public TickerCanvas(String msg, Font font, int limit) {
		
		// Super class processing
		super();
		
		// Pass up
		this.myMessage = msg;
		
		// Adjust Elements
		this.setFont(font);
		this.setLimit(limit);	
		
		// Adjust metrics
		this.fetchMetrics();
	}
	// End of Constructor Method

	
	/**
	 * Constructor which takes the initial message and limit for use as the
	 * base.
	 * <p>
	 * @param msg String message for display in ticker format.
	 * @param limit Maximum width which will lead to a ticker incident.
	 */
	public TickerCanvas(String msg, int limit) {

		// Super class processing
		super();
		
		// Pass up
		this.myMessage = msg;
		
		// Pass up
		this.setLimit(limit);
		
		// Adjust metrics
		this.fetchMetrics();
	}
	// End of Constructor Method
	
	
	/**
	 * Makes use of the FontMetrics methods to establish sizing elements in
	 * order to determine the needs for ticker functionality.
	 * <p>
	 * @param metrics FontMetrics handle for information.
	 */
	private synchronized void fetchMetrics(FontMetrics metrics) {
		
	    // Calculate decent
	    myDescent = metrics.getDescent();
	    
	    // Get the height of a line of text in this font and render context
	    myHeight = metrics.getHeight();
	    
	    // Get the advance of my text in this font and render context
	    myWidth  = metrics.stringWidth(myMessage)
                 + BUFFER;

	    // Calculate the size of a box to hold the text with some padding.
	    mySize = new Dimension(myWidth, myHeight);
	    
	    
	    /*
	     * Now that we have an idea on how big we need to be then we will
	     * establish these norms in order to prevent blatant resizing.
	     */
	    Dimension limits = new Dimension(myLimit, myHeight);
	    
	    // Force elements to see things out way
	    super.setMinimumSize(limits);
	    super.setPreferredSize(limits);
	    super.setMaximumSize(limits);
	}
	// End of fetchMetrics Method
	
	
	/**
	 * Makes use of the FontMetrics methods to establish sizing elements in
	 * order to determine the needs for ticker functionality.
	 */
	private void fetchMetrics() {
		
		// Extract metrics from Graphics
	    FontMetrics metrics = 
	    	super.getFontMetrics(this.getFont());
	    
	    // Process using Font metrics.
	    fetchMetrics(metrics);
	}
	// End of fontMetrics Method
	
	
	/**
	 * Makes use of the FontMetrics methods to establish sizing elements in
	 * order to determine the needs for ticker functionality.
	 * <p>
	 * @param g java.awt.Graphics handle for display.
	 */
	private void fetchMetrics(Graphics g) {
		
		// Cast for use
		Graphics2D g2 = (Graphics2D)g;
		
		// Adjust rendering hints
	    g2.setRenderingHint(
		    	RenderingHints.KEY_TEXT_ANTIALIASING,
	            RenderingHints.VALUE_TEXT_ANTIALIAS_GASP
	          );
	    
	    // Call primary method
	    fetchMetrics(g2.getFontMetrics());
	}
	// End of fetchMetrics Method
	
	
	/**
	 * Creates a buffered image for use.  This will enable us to avoid having
	 * to generate the same elements over and over.
	 */
	private synchronized void fetchSource(Graphics g) {
	
		// Generate a new source image
		mySource = new BufferedImage(
				myWidth + myLimit, 
				myHeight, 
				BufferedImage.TRANSLUCENT);
		
		// Cast as Graphics2D
		Graphics2D g2 = (Graphics2D)mySource.getGraphics();
		
		// Adjust rendering hints
	    g2.setRenderingHint(
		    RenderingHints.KEY_TEXT_ANTIALIASING,
	        RenderingHints.VALUE_TEXT_ANTIALIAS_GASP
	     );
		
		// Establish Font
		g2.setColor(super.getBackground());
		
		// Opaque handling
		if (true)
			g2.fillRect(
				0, 
				0, 
				(int)mySize.getWidth(), 
				(int)mySize.getHeight()
			  );
		
		// Establish Font
		g2.setFont(super.getFont());
		g2.setColor(super.getForeground());
		
		// Draw out the element
		g2.drawString(
			myMessage, 
			BUFFER, 
			myHeight - myDescent
		  );
		
		// Cleanup
		g2.dispose();
		
		
		/*
		 * Now that we have generated the appropriate image, we automatically
		 * grab a cropped copy for use.  Since it will be used as the initial
		 * set or displayed until replaced.
		 */
		setCrop(mySource.getSubimage(
				0, 
				0, 
				myLimit, 
				myHeight
			));
	}
	// End of fetchBuffer Method
	
	
	/**
	 * Assigns the cropped image for use.  
	 * <p>
	 * @param img BufferedImage containing cropped section for display.
	 */
	private synchronized void setCrop(BufferedImage img) {
		
		// Direct Assignment
		myCrop = img;
	}
	// End of setCrop Method
	
	
	/**
	 * Returns the cropped image for use.  
	 * <p>
	 * @return BufferedImage containing cropped section for display.
	 */
	private synchronized BufferedImage getCrop() {
		
		// Direct Pass
		return myCrop;
	}
	// End of getCrop Method
	
	

	/**
	 * Establishes the message displayed in ticker fashion.
	 * <p>
	 * @param msg String containing currently displayed text.
	 */
	public void setText(String msg) {
		
		// Order up re-processing
		Reprocess = true;
		
		// Pass up
		myMessage = msg;
		
		// Order a repaint
		repaint();
	}
	// End of setText Method
	
	
	/**
	 * Returns the message displayed in ticker fashion.
	 * <p>
	 * @return String containing currently displayed text.
	 */
	public String getText() {
		
		// Direct Pass
		return myMessage;
	}
	// End of getText Method
	
	
	
	/* (non-Javadoc)
	 * @see javax.swing.JComponent#setFont(java.awt.Font)
	 */
	public void setFont(Font font) {
		
		// Order up re-processing
		Reprocess = true;
		
		// Pass up
		super.setFont(font);
		
		// Order a repaint
		repaint();
	}
	// End of setFont Method
	

	/**
	 * Establishes the limit for message display.  Any item sized less then
	 * the message will not be displayed.
	 * <p>
	 * @param limit Display limit for the message.
	 */
	public void setLimit(int limit) {
		
		// Order up re-processing
		Reprocess = true;
		
		// Pass up
		myLimit = limit;
		
		// Order a repaint
		repaint();
	}
	// End of setLimit Method
	
	
	/**
	 * Returns the established limit for message displays.   Anything under
	 * the limit will not shift in position.
	 * <p>
	 * @return Integer containing the display limit for the message.
	 */
	public int getLimit() {
		
		// Return Instance
		return myLimit;
	}
	// End of getLimit Method
	
	
	/* (non-Javadoc)
	 * @see javax.swing.JComponent#paint(java.awt.Graphics)
	 */
	public void paint(Graphics g) {
		
		/*
		 * Establishes the buffer strategy for the display.  Since it would be
		 * normal to expect that paint is the first thing called once we have
		 * write capability, we can establish the strategy here.
		 */
		if (myBuffer == null) {
			
			// Enable Triple Buffering
			this.createBufferStrategy(3);
			myBuffer = this.getBufferStrategy();
		}
		
		
		/*
		 * We have to ensure that we know all of the metrics are known prior
		 * to doing any actual drawing.
		 */
		if ((Reprocess) 
		 && (myBuffer != null)) {
			
			// Extract all necessary information
			fetchMetrics(g);
			fetchSource(g);
			
			// Determine requirement based on fit
			Redraw = (mySize.getWidth() < (myLimit))
				? false
				: true;
			
			// Re-adjust position
			myPosition = 0;
			
			// No longer need to re-process
			Reprocess = false;
			
			
			/*
			 * The initial paint call will trigger the Thread creation and
			 * subsequent spawning of information.
			 */
			if ((myThread == null)
			 && (Redraw)) {
				
				// Enable Loop
				Loop = true;
				
				// Thread creation and start
				myThread = new Thread(this);
				myThread.start();
			}
			
			/*
			 * If we have no need to redraw why waste the resources, kill
			 * the thread and deal with things here.
			 */
			else if ((myThread != null)
				&& (!Redraw)) {
				
				// Disable Loops
				Loop = false;
				
				// Interrupt Thread
				myThread.interrupt();
				
				// Reset
				myThread = null;
			}
		}
		
		// Recast
		Graphics2D g2 = (Graphics2D)g;
			
		// Draw that cropped Source image out
		g2.drawImage(getCrop(), null, 0, 0);
			
		// Dispose
		g2.dispose();
	}
	// End of paintIcon Method

	
	/* (non-Javadoc)
	 * @see javax.swing.JComponent#getWidth()
	 */
	public int getWidth() {
		
		// Limit is always returned
		return myLimit;
	}
	// End of getWidth Method
	
	
	/* (non-Javadoc)
	 * @see javax.swing.JComponent#getHeight()
	 */
	public int getHeight() {
		
		// Calculated height used
		return myHeight;
	}
	// End of getHeight Method
	
	/*
	 * The following elements have no effect on the operation of the class
	 * since we must strictly control out limits and controls.
	 */

	/* (non-Javadoc)
	 * @see javax.swing.JComponent#setMinimumSize(java.awt.Dimension)
	 */
	public void setMinimumSize(Dimension minimumSize) {}
	
	
	/* (non-Javadoc)
	 * @see javax.swing.JComponent#setPreferredSize(java.awt.Dimension)
	 */
	public void setPreferredSize(Dimension preferredSize) {}
	
	
	/* (non-Javadoc)
	 * @see javax.swing.JComponent#setMaximumSize(java.awt.Dimension)
	 */
	public void setMaximumSize(Dimension maximumSize) {} 
	
	
	
	/*
	 * RUNNABLE IMPLEMENTATION
	 */
	
	/* (non-Javadoc)
	 * @see java.lang.Runnable#run()
	 */
	public void run() {
		
		/*
		 * In order to distinguish between interrupts from us, or from outside
		 * sources we will enforce loops on our side.
		 */
		while (Loop) {
			
			try {
				
				/*
				 * These elements are only processed if there is a requirement
				 * to redraw.  Otherwise we ignore.
				 */
				if (Redraw) {
					
					/*
					 * We will always choose to sleep no matter the event due to
					 * the fact that the pause will reduce processing time.
					 */
					int sleep = (this.myPosition < 1)
						? 1000
						: 65;
					
					// Sleep for designated time
					Thread.sleep(sleep);

					
					/*
					 * We deal with the cropping of the image in this area so that
					 * the paint() method will not have to process this information
					 * this also has the advantage of making this area the
					 * control for image updates.
					 */
					
					// Update position
					myPosition = (myPosition + 1 < mySize.getWidth())
						? myPosition + 1
						: 0;
					
					// Crop the image
					setCrop(mySource.getSubimage(
							myPosition, 
							0, 
							myLimit, 
							myHeight
						));
					
					// No need to write if we have nothing to write to
					if (myBuffer.contentsLost())
						continue;
					
					// Recast
					Graphics2D g2 = (Graphics2D)myBuffer.getDrawGraphics();
					
					// Draw that cropped Source image out
					g2.drawImage(getCrop(), null, 0, 0);
					
					// Dispose
					g2.dispose();
					
					// Display the element
					myBuffer.show();
				}
			} 
			
			/*
			 * In case we get interrupted.  However this does essentially
			 * nothing since we expect to get them.
			 */
			catch (InterruptedException e) {}
		}
	}
	// End of run Method
}

