/**
 * 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.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;

import javax.swing.JLabel;
import javax.swing.border.Border;

/**
 * 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 variant scrolls the characters based on a single character movement
 * which can lead to flashing when the screen is not double buffered.  As a
 * result its alternate variant should be used in that circumstance which 
 * works better on such displays.
 * <p>
 * @author Martin Foster
 * @version 1.0.4, %Revision, 99%, %LastChangedDate, 17/03/11 8:17PM%
 * @see TickerSimpleLabel
 */
public class TickerBufferedLabel extends JLabel implements Runnable {

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

	/*
	 * INTERFACE ELEMENTS
	 */
	
	// 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 TickerBufferedLabel() { 
		
		// Super class processing
		super();
		
		// Force Buffering
		this.setDoubleBuffered(true);
		
		// 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 TickerBufferedLabel(String msg) {
		
		// Super class processing
		super(msg);
		
		// Force Buffering
		this.setDoubleBuffered(true);
		
		// 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 TickerBufferedLabel(String msg, Font font) {
		
		// Super Call
		super(msg);

		// Force Buffering
		this.setDoubleBuffered(true);
		
		// Assign font
		super.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 TickerBufferedLabel(String msg, Font font, int limit) {
		
		// Super class processing
		super(msg);
		
		// Force Buffering
		this.setDoubleBuffered(true);
		
		// Pass up
		super.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 TickerBufferedLabel(String msg, int limit) {

		// Super class processing
		super(msg);
		
		// Force Buffering
		this.setDoubleBuffered(true);
		
		// 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) {
	
		// Copy padding
		Insets padding = this.getInsets();
		
	    // Calculate decent
	    myDescent = metrics.getDescent();
	    
	    // Get the height of a line of text in this font and render context
	    myHeight = metrics.getHeight()
	             + padding.bottom
	             + padding.top;

	    // Get the advance of my text in this font and render context
	    myWidth  = metrics.stringWidth(super.getText())
	             + padding.left
                 + padding.right
                 + 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) {
	
		// Extract insets
		Insets padding = super.getInsets();
		
		// 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(
			super.getText(), 
			padding.left + BUFFER, 
			myHeight - padding.bottom - 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
	
	
	/* (non-Javadoc)
	 * @see javax.swing.JLabel#setText(java.lang.String)
	 */
	public void setText(String msg) {
		
		// Order up re-processing
		Reprocess = true;
		
		// Pass up
		super.setText(msg);
		
		// Order a repaint
		repaint();
	}
	// End of setText Method
	
	
	/* (non-Javadoc)
	 * @see javax.swing.JComponent#setBorder(javax.swing.border.Border)
	 */
	public void setBorder(Border border) {
		
		// Order up re-processing
		Reprocess = true;
		
		// Pass up
		super.setBorder(border);
	}
	// End of setBorder 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) {
		
		/*
		 * We have to ensure that we know all of the metrics are known prior
		 * to doing any actual drawing.
		 */
		if (Reprocess) {
			
			// 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) {
			
			// Recast
			Graphics2D g2 = (Graphics2D)this.getGraphics();
			
			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
						));
					
					// Draw that cropped Source image out
					g2.drawImage(getCrop(), null, 0, 0);
					
					// Dispose
					g2.dispose();
				}
			} 
			
			/*
			 * In case we get interrupted.  However this does essentially
			 * nothing since we expect to get them.
			 */
			catch (InterruptedException e) {}
		}
	}
	// End of run Method
	    
}
