package org.tinytlf.layout
{
    import flash.display.Sprite;
    import flash.text.engine.TextBlock;
    import flash.text.engine.TextLine;
    import flash.text.engine.TextLineValidity;
    
    public class TextContainer extends Sprite
    {
        private var mHeight:Number = 0;
        
        public function get measuredHeight():Number
        {
            return mHeight;
        }
        
        private var explicitHeight:Number = NaN;
        
        override public function get height():Number
        {
            return explicitHeight;
        }
        
        override public function set height(value:Number):void
        {
            if(value === explicitHeight)
                return;
            
            explicitHeight = value;
        }
        
        private var explicitWidth:Number = NaN;
        
        override public function get width():Number
        {
            return explicitWidth;
        }
        
        override public function set width(value:Number):void
        {
            if(value === explicitWidth)
                return;
            
            explicitWidth = value;
            
            invalidateLines();
        }
        
        private var lines:Vector.<TextLine> = new <TextLine>[];
        
        /**
         * Accepts a TextBlock and last line rendered from the block. Renders
         * as many lines from the TextBlock as will fit within this container,
         * and returns the last line successfully rendered.
         *
         * If all lines are successfully rendered from the TextBlock, this
         * method should return null.
         *
         * If all lines were rendered from the TextBlock, this method will be
         * called a second time with a new TextBlock and the previousLine
         * argument set to null.
         *
         * <ul>
         * <li>If previousLine or the previousLine.nextLine is null, this method
         * should use TextBlock.createTextLine.</li>
         * <li>If previousLine has an invalid nextLine member, this method should
         * attempt to use TextBlock.recreateTextLine.</li>
         * <li>If the previousLine has a valid nextLine member, this method
         * should skip ahead in the linked list until one of the previous two
         * conditions are true.</li>
         * </ul>
         */
        public function layout(block:TextBlock, previousLine:TextLine):TextLine
        {
            _y = measuredHeight;
            
            var line:TextLine = createTextLine(block, previousLine);
            
            while(line)
            {
                addChild(line);
                lines.push(line);
                
                position(line);
                
                if(checkConstraints(line))
                {
                    return line;
                }
                
                line = createTextLine(block, line);
            }
            
            return line;
        }
        
        public function preLayout():void
        {
            _y = 0;
            mHeight = 0;
            removeOrphanedLines();
        }
        
        private function invalidateLines():void
        {
            lines.forEach(function(l:TextLine, ...args):void{
                l.validity = TextLineValidity.INVALID;
            });
        }
        
        /**
         * Creates or recreates a given TextLine.
         */
        private function createTextLine(block:TextBlock, line:TextLine):TextLine
        {
            removeOrphanedLines();
            
            if(line)
            {
                if(line.validity === TextLineValidity.INVALID)
                {
                    return block.recreateTextLine(line, null, width, 0.0, true);
                }
                else if(orphans.length)
                {
                    var orphan:TextLine = getFirstOrphan(line);
                    
                    if(orphan)
                    {
                        return block.recreateTextLine(orphan, line, width, 0.0, true);
                    }
                }
            }
            
            return block.createTextLine(line, width, 0.0, true);
        }
        
        private var _y:Number = 0;
        
        private function position(line:TextLine):void
        {
            _y += line.ascent;
            line.y = _y;
            _y += line.descent;
            mHeight += line.height;
        }
        
        private function checkConstraints(line:TextLine):Boolean
        {
            if(explicitHeight != explicitHeight)
                return false;
            
            return measuredHeight + line.textHeight > explicitHeight;
        }
        
        private function removeOrphanedLines():void
        {
            var line:TextLine;
            var n:int = lines.length;
            
            for(var i:int = 0; i < n; ++i)
            {
                line = lines[i];
                
                if(line.validity === TextLineValidity.VALID)
                    continue;
                
                if(contains(line))
                    removeChild(line);
                
                lines.splice(i, 1);
                orphans.push(line);
                n = lines.length;
            }
        }
        
        private static const orphans:Vector.<TextLine> = new <TextLine>[];
        
        private static function getFirstOrphan(exceptForMe:TextLine):TextLine
        {
            if(orphans.length == 0)
                return null;
            
            var orphan:TextLine = orphans.pop();
            
            while(orphan == exceptForMe)
                orphan = orphans.pop();
            
            while(orphan && orphan.validity == TextLineValidity.VALID)
                orphan = orphans.pop();
            
            return orphan;
        }
    }
}