package 
{
	import flash.display.Sprite;
	import flash.text.TextField;
	import flash.text.TextFieldAutoSize;
	import flash.text.TextFormat;
	import flash.text.TextFormatAlign;
	import flash.text.TextLineMetrics;
	import flash.events.TextEvent;
	import flash.events.Event;

	public class actionscript_book_NewsLayout_com_example_programmingas3_newslayout_MultiColumnText extends Sprite
	{
		public var fieldArray:Array;
		
		public var numColumns:uint = 2;
		
		public var lineHeight:uint = 16;
		public var linesPerCol:uint = 15;
		
		/**
		 * The space between columns.
		 */
		public var gutter:uint = 10;
		
		public var format:TextFormat;
		public var lastLineFormat:TextFormat;
		public var firstLineFormat:TextFormat;
		
		private var _text:String = "";
		
		public var preferredWidth:Number = 400;
		public var preferredHeight:Number = 100;

		/**
		 * The width of each column.
		 */
		public var colWidth:int = 200;
		
		
		public function actionscript_book_NewsLayout_com_example_programmingas3_newslayout_MultiColumnText(cols:uint = 2, 
			gutter:uint = 10, w:Number = 400, h:Number = 100, 
			tf:TextFormat = null):void
		{
			this.numColumns = Math.max(1, cols);
			this.gutter = Math.max(1, gutter);
			
			this.preferredWidth = w;
			this.preferredHeight = h;
			
			if (tf != null)
            {
				applyTextFormat(tf);
            }
			
			this.setColumnWidth();
			
			this.fieldArray = new Array();
			
			// Create a text field for each column
        	for (var i:int = 0; i < cols; i++)
        	{
        		var field:TextField = new TextField();
				field.multiline = true;
        		field.autoSize = TextFieldAutoSize.NONE;
        		field.wordWrap = true;
        		field.width = this.colWidth;

                if (tf != null)
                {
                    field.setTextFormat(tf);
                }
        		
        		this.fieldArray.push(field);
        		this.addChild(field);
        	}
		}
		
		public function applyTextFormat(tf:TextFormat):void
		{
            this.format = tf;
            
            // The last line format starts as a direct copy
			this.lastLineFormat = new TextFormat(tf.font, tf.size, tf.color, tf.bold, 
                                                  tf.italic, tf.underline, tf.url, 
                                                  tf.target, tf.align, tf.leftMargin, 
                                                  tf.rightMargin, tf.indent, tf.leading);
			this.lastLineFormat["letterSpacing"] = this.format["letterSpacing"];
                                      
            // The first line format removes the indent value
			this.firstLineFormat = new TextFormat(tf.font, tf.size, tf.color, tf.bold, 
                                                  tf.italic, tf.underline, tf.url, 
                                                  tf.target, tf.align, tf.leftMargin, 
                                                  tf.rightMargin, 0, tf.leading);
			this.firstLineFormat["letterSpacing"] = this.format["letterSpacing"];
		}
		
		/**
		 * Spread the text across multiple text fields.
		 */
        public function layoutColumns():void
        {
        	if (this._text == "" || this._text == null)
        	{
        		return;
        	}
        	
    	    var field:TextField = fieldArray[0] as TextField;
    		field.text = this._text;
    		field.setTextFormat(this.format);

        	this.preferredHeight = this.getOptimalHeight(field);
        	
        	var remainder:String = this._text;
        	var fieldText:String = "";
        	var lastLineEndedPara:Boolean = true;
        	
        	var indent:Number = this.format.indent as Number;
        	
        	for (var i:int = 0; i < fieldArray.length; i++)
        	{
        		field = this.fieldArray[i] as TextField;
        		
				// First set the text for the field so we can find out where the
				// line break will be in the last line of the field
        		//field.width = colWidth;
        		field.height = this.preferredHeight;
    		    field.text = remainder;
    		    
    		    // Apply the text format for to get the lines to all wrap as intended
    		    field.setTextFormat(this.format);
    		    
    		    // Remove indents from the start of each new field, unless the new field
    		    // marks the start of a new paragraph
    		    var lineLen:int;
    		    if (indent > 0 && !lastLineEndedPara && field.numLines > 0)
    		    {
    		        lineLen = field.getLineLength(0);
    		        if (lineLen > 0)
    		        {
    		            field.setTextFormat(this.firstLineFormat, 0, lineLen);
    		        }
    		    }
        		
        		field.x = i * (colWidth + gutter);
        		field.y = 0;
        		
        		remainder = "";
        		fieldText = "";
        		
        		var linesRemaining:int = field.numLines;   	
        		var linesVisible:int = Math.min(this.linesPerCol, linesRemaining);

				// Now copy lines from the text into the field for display
				// purposes. Remaining lines will be saved for the next field.
        		for (var j:int = 0; j < linesRemaining; j++)
        		{
        			if (j < linesVisible)
        			{
        				fieldText += field.getLineText(j);
        			}
        			else
        			{
        				remainder +=  field.getLineText(j);
        			}
        		}
        		
        		field.text = fieldText;
        		
        		// Apply the format again, this time to the exactly-fitting text
        		field.setTextFormat(this.format);
        		
        		// Remove the indent again if needed
    		    if (indent > 0 && !lastLineEndedPara)
    		    {
    		        lineLen = field.getLineLength(0);
    		        if (lineLen > 0)
    		        {
    		            field.setTextFormat(this.firstLineFormat, 0, lineLen);
    		        }
    		    }
        		
        		// Check if a paragraph ends on the last line
    		    var lastLine:String = field.getLineText(field.numLines - 1);
        		var lastCharCode:Number = lastLine.charCodeAt(lastLine.length - 1);
        		
        		if (lastCharCode == 10 || lastCharCode == 13)
        		{
        		    lastLineEndedPara = true;
        		}
        		else
        		{
        		    lastLineEndedPara = false;
        		}
        		
        		// If the last line doesn't end a paragraph, manually justify itif needed
        		if ((this.format.align == TextFormatAlign.JUSTIFY) && (i < fieldArray.length - 1))
        		{
            		if (!lastLineEndedPara)
            		{
                        justifyLastLine(field, lastLine);
            		}
                }
        	}
        }
        
		/**
		 * If the text alignment style is set to "justify" it will not justify the final
		 * line in a paragraph. It also assumes that the last line in a field is the 
		 * final line in a paragraph. Of course when we split text across fields, the 
		 * last line in a field might be in the middle of a paragraph.
		 *
		 * This method "manually" justfies the last line in a text field by changing the 
		 * letter spacing style and applying it to space characters in the line. The effect
		 * is to add additional pixels to each space until the line width matches the 
		 * maximum text width for the field.
		 */
        public function justifyLastLine(field:TextField, lastLine:String):void
        {
         	// boost letter spacing to widen the last line
		    var metrics:TextLineMetrics = field.getLineMetrics(field.numLines - 1);
		    
		    // get the original letter spacing
		    var spacing:Number = this.format.letterSpacing as Number;
		    
		    var maxTextWidth:Number = field.width - 4 - (this.lastLineFormat.leftMargin as Number) - (this.lastLineFormat.rightMargin as Number);
		    
		    // adjust for paragraph indent value
		    var indent:Number = (this.lastLineFormat.indent as Number);
		    if (indent > 0)
		    {
		        var secondToLastLine:String = field.getLineText(field.numLines - 2);
            	var lastCharCode:int = secondToLastLine.charCodeAt(secondToLastLine.length - 1);
        		if (lastCharCode == 10 || lastCharCode == 13)
        		{
                    // previous line was the end of a paragraph, 
                    // so subtract the indent value from this line's max width
                    maxTextWidth -= indent;
        		}
		    }
		    var extraWidth:Number = maxTextWidth - metrics.width;
		    
		    // remove trailing spaces
		    while (lastLine.charAt(lastLine.length - 1) == " ")
		    {
		        lastLine = lastLine.substr(0, lastLine.length - 1);
		    }
		    
		    var wordArray:Array = lastLine.split(" ");
		    var numSpaces:int = wordArray.length - 1;
			// if there aren't any spaces, give up
		    if (numSpaces < 1)
		    {
		        return;
		    }
		    var spaceSize:int = Math.floor(extraWidth / numSpaces) + spacing;
		    this.lastLineFormat.letterSpacing = spaceSize;
            		    
		    var remainingPixels:int = extraWidth % spaceSize;
		    
		    var lastChars:String = lastLine;
		    var lastlineOffset:Number = field.getLineOffset(field.numLines - 1);
		    
		    var sPos:int;
		    var counter:int = -1;
		    
		    for (var i:int = 0; i < numSpaces; i++)
		    {
				// later in the loop add an extra pixel to the spacing
				// to account for the remaining pixels
		        if ((numSpaces - i) == remainingPixels)
		        {
		            this.lastLineFormat.letterSpacing = spaceSize + 1;
		        }
		        
		        sPos = lastChars.indexOf(" ");
		        counter += sPos + 1;
		        field.setTextFormat(this.lastLineFormat, lastlineOffset + counter, lastlineOffset + counter + 1); 
		        lastChars = lastChars.substring(sPos + 1);
		    }
        }
		
		public function set text(str:String):void
		{
			this._text = str;
			layoutColumns();
		}
		
		public function get text():String
		{
			return this._text;
		}

		public function set textFormat(tf:TextFormat):void
		{
			applyTextFormat(tf);
			layoutColumns();
		}
		
		public function get textFormat():TextFormat
		{
			return this.format;
		}
		
		public function setColumnWidth():void
		{
    		this.colWidth = Math.floor( (this.preferredWidth - 
    			((this.numColumns - 1) * this.gutter)) / this.numColumns);    
		}
		
        public function getOptimalHeight(field:TextField):int
        {
        	if (field.text == "" || field.text == null)
        	{
        		return this.preferredHeight;
        	}
        	else
        	{
        		this.linesPerCol = Math.ceil(field.numLines / this.numColumns);
        		
        		// Get the line height based on the font and size being used
        		var metrics:TextLineMetrics = field.getLineMetrics(0);
        		this.lineHeight = metrics.height;
        		var prefHeight:int = linesPerCol * this.lineHeight;

        		// Add 4 pixels as a buffer to account for the standard
        		// 2 pixel TextField border
        		return prefHeight + 4;
        	}
        }
	}
}