/**
 * 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.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JLabel;
import javax.swing.Timer;
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 is more efficient in situations where the display is not double
 * buffered.  However it will make its movements based on characters which may
 * be more jarring an effect.
 * <p>
 * @author Martin Foster
 * @version 1.0.1, %Revision, 99%, %LastChangedDate, 17/03/11 8:17PM%
 * @see TickerBufferedLabel
 */
public class TickerSimpleLabel extends JLabel implements ActionListener {

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

	/*
	 * INTERFACE ELEMENTS
	 */
	
	// Temporary String
	private String tempString;
	
	// Character string containing text
	private char[] myString;
	private int[] myCounts;
	
	// Individual Elements
	private int myHeight  = 0;
	private int myWidth   = 0;
	private int myLimit   = 200;
	
	// Current Position
	private int myPosition = 0;
	
	// Re-processing flag
	private boolean Reprocess = true;
	
	// Timer element
	private Timer myTimer = null;
	
	// Separator element
	private String mySeparator = " - ";

	/*
	 * CONSTANTS
	 */
	private static final int BUFFER = 1;
	private static final int INITIAL_DELAY = 2500;
	private static final int NORMAL_DELAY = 250;
	
	
	/**
	 * Base constructor which initialises none of the elements.  
	 */
	public TickerSimpleLabel() { 
		
		// Super class processing
		super("");
		
		// Adjust metrics
		this.fetchSizes();
		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 TickerSimpleLabel(String msg) {
		
		// Super class processing
		super("");
		
		// Pass up
		this.setText(msg);
		
		// Adjust metrics
		this.fetchSizes();
		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 TickerSimpleLabel(String msg, Font font) {
		
		// Super Call
		super("");

		// Pass up
		this.setFont(font);
		this.setText(msg);
		
		// Adjust metrics
		this.fetchSizes();
		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 TickerSimpleLabel(String msg, Font font, int limit) {
	
		// Supreme processing
		super("");
		
		// Pass up
		this.setFont(font);
		this.setText(msg);
		this.setLimit(limit);

		// Adjust metrics
		this.fetchSizes();
		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 TickerSimpleLabel(String msg, int limit) {
		
		// Super processing
		super("");
		
		// Pass up
		this.setLimit(limit);
		this.setText(msg);
		
		// Adjust metrics
		this.fetchSizes();
		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();
	
	    // 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(new String(myString))
	             + padding.left
                 + padding.right
                 + BUFFER;

	    
	    /*
	     * 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
	    this.setMinimumSize(limits);
	    this.setPreferredSize(limits);
	    this.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 = 
	    	getFontMetrics(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
	
	
	/**
	 * Extracts the size of every character in the text to display in order
	 * to determine exactly how many characters can be displayed on the
	 * available space.
	 * <p>
	 * @param metrics Metrics processing capability to use for the calculation.
	 */
	private synchronized void fetchSizes(FontMetrics metrics) {
		
		// Quick assign
		String msg = tempString;
		
	    // Introduce separator character(s)
	    if (metrics.stringWidth(msg) > myLimit)
	    	msg += mySeparator;
		
		// We only care about the individual characters
		myString = msg.toCharArray();
		
		// Create a matching array
		myCounts = new int[myString.length];
		

		/*
		 * We cycle through each and every element and determine their metrics
		 * in order to figure out how much size are we dealing with.
		 */
	    
	    // Running totals
	    int length = 0;
	    int size   = 0;
	    
	    // Rotate through and get a complete list
		for (int count=0; count < myString.length; count++) {
			
			// Each individual characters width
			myCounts[count] = metrics.charWidth(myString[count]);
			
			// Append size
			size += myCounts[count];
			
			// Determine max position
			if (size < myLimit)
				length = count;
		}
		
		// Force the new string into place
		super.setText(
			msg.substring(0, length+1));
	}
	// End of fetchSizes Method
	
	/**
	 * Extracts the size of every character in the text to display in order
	 * to determine exactly how many characters can be displayed on the
	 * available space.
	 */
	private void fetchSizes() {
		
		// Extract metrics from Graphics
	    FontMetrics metrics = 
	    	getFontMetrics(getFont());
	    
	    // Process using Font metrics.
	    fetchSizes(metrics);
	}
	// End of fetchSizes Method
	
	
	/**
	 * Extracts the size of every character in the text to display in order
	 * to determine exactly how many characters can be displayed on the
	 * available space.
	 * <p>
	 * @param g Graphics handle for extracting screen metrics.
	 */
	private void fetchSizes(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
	    fetchSizes(g2.getFontMetrics());
	}
	// End of fetchSizes Method
	
	
	/**
	 * Determines the requirement for the timer process to exist and process
	 * information.
	 */
	private synchronized void fetchRedraw() {
		
		/*
		 * This is where we create the initial instance after establishing the
		 * fact that we will have an initial delay put into place first.
		 */
		if (myTimer == null) {
			
			// Initial instance
			myTimer = new Timer(NORMAL_DELAY, this);
		
			// Initial delay
			myTimer.setInitialDelay(INITIAL_DELAY);
		}
		
		
		/*
		 * The width of the string is greater then what we can realistically
		 * allow.  So we have to start the time and begin the ticker process.
		 */
		if (myWidth > myLimit) {
			
			// Process
			myTimer.restart();
			
		}
		
		/*
		 * We halt the process otherwise since we do not need to waste our
		 * time dealing with updates when none are required.
		 */
		else {
		
			// Pause timer
			myTimer.stop();
		}
	}
	// End of fetchRedraw Method
	
	
	/* (non-Javadoc)
	 * @see javax.swing.JLabel#setText(java.lang.String)
	 */
	public void setText(String msg) {

		// Adjust temporary text
		tempString = msg;
		
		// Require recalculation
		Reprocess = true;
		
		// Order a repaint
		repaint();
	}
	// End of setText Method
	

	/* (non-Javadoc)
	 * @see javax.swing.JLabel#getText()
	 */
	public String getText() {
		
		// Return a string item
		return super.getText();
	}
	// End of getText 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);
		
		// Order a repaint
		repaint();
	}
	// End of setBorder Method
	

	/* (non-Javadoc)
	 * @see javax.swing.JComponent#setFont(java.awt.Font)
	 */
	public void setFont(Font font) {

		// Pass up
		super.setFont(font);
		
		// Order up re-processing
		Reprocess = true;
		
		// 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
	
	
	/**
	 * Sets the separator section used to re-introduce the beginning of the
	 * string.
	 * <p>
	 * @param sep String containing separator section.
	 */
	public void setSeparator(String sep) {
		
		// Pass up
		mySeparator = sep;
		
		// Require a re-processing
		Reprocess = true;
	}
	// End of getSeparator Method
	
	
	/**
	 * Gets the separator section used to re-introduce the beginning of the
	 * string.
	 * <p>
	 * @return String containing separator section.
	 */
	public String getSeparator() {
		
		// Return Separator
		return mySeparator;
	}
	// End of getSeparator 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#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
	
	
	/* (non-Javadoc)
	 * @see javax.swing.JComponent#paintComponent(java.awt.Graphics)
	 */
	public void paintComponent(Graphics g) {
		
		// Fine tune space requirements
		if (Reprocess) {
			
			// Gain a more accurate picture on the size issue
			fetchSizes(g);
			fetchMetrics(g);
			fetchRedraw();
			
			// Re-adjust position
			myPosition = 0;
			
			// Prevent re-processing unless required
			Reprocess = false;
		}
		
		// Have the super class do its thing
		super.paintComponent(g);
	}
	// End of paintComponent Method
	
	
	/*
	 * This action listener is exclusively used for the time element in that
	 * it will determine the length of the string and adjust it accordingly.
	 */
	
	/* (non-Javadoc)
	 * @see java.awt.event.ActionListener#actionPerformed(java.awt.event.ActionEvent)
	 */
	public void actionPerformed(ActionEvent evt) {

		// Safeties
		if (myTimer == null) return;

		// Increment the position
		myPosition = (myPosition + 1 < myString.length)
			? myPosition + 1
			: 0;
		
		/*
		 * We are going to need to determine the length of the string to bring
		 * in since that will determine the amount of characters to drag in.
		 * 
		 * We choose to loop the string over itself and continue with the
	     * ticker process that way.
		 */
		int size = 0;
		int num  = 0;
		int pos  = myPosition;
		
		// Loop through
		do {

			/*
			 * This will loop over back to the beginning of the string for
			 * processing.
			 */
			if (pos >= myString.length)
				pos = 0;
			
			/*
			 * We never want to go past out limit and cannot do that unless
			 * we break out early.
			 */
			if (size + myCounts[pos] > myLimit)
				break;
			
			
			/*
			 * Assuming all of the above was good.  We will process for the
			 * next run through.
			 */
			
			// Increase in size accordingly
			size = size + myCounts[pos];
			
			// Number of characters to display
			pos++;
			num++;
			
		} while (size < myLimit);
		
		
		/*
		 * Now we create the necessary string to pass onto the super class
		 * for display purposes.
		 */
		
		// Re-adjust position
		pos = myPosition;
		
		// Collecting Buffer
		StringBuffer sb = new StringBuffer();
		
		// Loop through and append
		for (int count=0; count < num; count++) {
			
			// Safety
			if (pos >= myString.length)
				pos = 0;
						
			// Append to buffer
			sb.append(myString[pos]);
			
			// Increment
			pos++;
		}
		
		// Pass on the generated string
		super.setText(sb.toString());
	} 
	// End of ActionPerformed 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) {}    
}
