var TextNode = function()
{
    var debug_walkers = 0;

    var _pub = {
        isContiguous: function(textnodes)
        {
            if(!textnodes || !textnodes.length)
                return true;

            //textnodes = Util.sortTextNodes(textnodes);

            var n = 0;
            var walker = textnodes.first();
            while(walker = TextNode.nextLeaf(walker,null,
                'text',{skip_floating:true,skip_brs:true}))
            {
                n++;
                if(n == textnodes.length)
                    return true;
                else if(walker != textnodes[n])
                    return false;
            }
            return true;
        },

        //take a textnode and return an ancestor that is a p, li or heading
        getBlockNode: function(textnode)
        {
            var heading_str = Format.headings().join(',');
            var parent = textnode.parentNode;
            var block = null;
            if(Util.match(parent,'li,p,'+heading_str))
                block = parent;
            else
                block = parent.up('li,p,'+heading_str);

            return block;                
        },

        getSubBlockNode: function(textnode)
        {
            var heading_str_a = Format.headings().join(' > *,');
            var heading_str_b = Format.headings().join(',');
            var parent = Wrapper.ancestor(textnode);
            var sub_block = null;
            if(Util.match(parent,'li,p,'+heading_str_b))
                sub_block = textnode;
            else if(Util.match(parent.parentNode,'li,p,'+heading_str_b))
                sub_block = parent;
            else
                sub_block = parent.up('li > *,p > *,'+heading_str_a+' > *');

            return sub_block;
        },
        
        //walk along leaves to find the next textnode from the one given, but
        //dont leave element 'within'
        nextLeaf: function(node,within,types,extra)
        {
            if(!extra)
                extra = new Object();

            if(types.split(',').indexOf('br') == -1)
                types = types + ',br';
            if(!node)
                return null;

            var result;

            if(!debug_walkers)
                Debug.disableLog();

            Debug.log('');
            Debug.log('nextLeaf('+node.data+','+within+')');
            var walker = node;

            //if we werent given a node of the requested type, walk down until
            //we find a matching node, or a leaf then along as usual
            if(!Util.match(walker,types))
            {
                while(walker.firstChild)
                {
                    walker = walker.firstChild;
                    if(Util.match(walker,types))
                    {
                        Debug.enableLog();
                        return walker;
                    }
                }
            }

            while(1)
            {
                while(!walker.nextSibling)
                {
                    walker = walker.parentNode;
                    if(!walker)
                        break;
                    Debug.log('walked up to ' + walker);
                }
                if(walker)
                {
                    walker = walker.nextSibling;
                    Debug.log('walked along to ' + walker + ' with node type ' + walker.nodeType);
                    
                    while(!Util.match(walker,types))
                    {
                        walker = walker.firstChild;
                        if(!walker)
                            break;
                        Debug.log('walked down to ' + walker);
                    }

                    if(walker)
                    {
                        if(walker.nodeType == 3)
                            Debug.log('resulting node: ' +
                            Debug.summarise(walker));
                        else
                            Debug.log('resulting node: ' + walker.tagName);

                        if(within && !(walker.parentNode == within || walker.parentNode.descendantOf(within)))
                            result = null;
                        else
                            result = walker;
                    }
                    else
                        result = null;
                }
                else
                {
                    Debug.log('ran out of parents');
                    result = null;
                }
                if(result)
                {
                    var f;
                    if(result.nodeType == 3)
                    {
                        if(!extra.accept_empty)
                        {
                            if(result.data.replace(/[ \n\t]*/,'') != '')
                                break;
                        }
                        else
                            break;
                    }
                    else if(result.match('br'))
                    {
                        if(!extra.skip_brs)
                            break;
                    }
                    else
                    {
                        if(f = result.getStyle('float'))
                        {
                            if(f == 'right' || f == 'left')
                            {
                                if(!extra.skip_floating)
                                    break;
                            }
                            else
                                break;
                        }
                    }
                }
                else
                    break;
            }

            Debug.enableLog();
            return result;
        },

        prevLeaf: function(node,within,types,extra)
        {
            if(!extra)
                extra = new Object();

            if(types.split(',').indexOf('br') == -1)
                types = types + ',br';
            if(!node)
                return null;

            var result;

            if(!debug_walkers)
                Debug.disableLog();

            Debug.log('');
            Debug.log('prevLeaf('+node.data+','+within+')');
            var walker = node;

            //if we werent given a node of the requested type, walk down until
            //we find a matching node, or a leaf then along as usual
            if(!Util.match(walker,types))
            {
                while(walker.lastChild)
                {
                    walker = walker.lastChild;
                    if(Util.match(walker,types))
                    {
                        Debug.enableLog();
                        return walker;
                    }
                }
            }

            while(1)
            {
                while(!walker.previousSibling)
                {
                    walker = walker.parentNode;
                    if(!walker)
                        break;
                    Debug.log('walked up to ' + walker);
                }
                if(walker)
                {
                    walker = walker.previousSibling;
                    Debug.log('walked along to ' + walker + ' with node type ' + walker.nodeType);
                    
                    while(!Util.match(walker,types))
                    {
                        walker = walker.lastChild;
                        if(!walker)
                            break;
                        Debug.log('walked down to ' + walker);
                    }

                    if(walker)
                    {
                        if(walker.nodeType == 3)
                            Debug.log('resulting node: ' +
                            Debug.summarise(walker));
                        else
                            Debug.log('resulting node: ' + walker.tagName);

                        if(within && !(walker.parentNode == within || walker.parentNode.descendantOf(within)))
                            result = null;
                        else
                            result = walker;
                    }
                    else
                        result = null;
                }
                else
                {
                    Debug.log('ran out of parents');
                    result = null;
                }
                if(result)
                {
                    var f;
                    if(result.nodeType == 3)
                    {
                        if(!extra.accept_empty)
                        {
                            if(result.data.replace(/[ \n\t]*/,'') != '')
                                break;
                        }
                        else
                            break;
                    }
                    else if(result.match('br'))
                    {
                        if(!extra.skip_brs)
                            break;
                    }
                    else
                    {
                        if(f = result.getStyle('float'))
                        {
                            if(f == 'right' || f == 'left')
                            {
                                if(!extra.skip_floating)
                                    break;
                            }
                            else
                                break;
                        }
                    }
                }
                else
                    break;
            }

            Debug.enableLog();
            return result;
        },

        findTextNode: function(x,y,target,within)
        {
            Debug.disableLog();
            Debug.log('findTextNode(' + x + ',' + y + ',' +
            Debug.summarise(target) + ',' + Debug.summarise(within) + ')');
            var lookin;
            var ret = {invalid:true};
            var in_dropper = false;
            var walker = target;
            var debug = '';
            while(walker && walker.nodeType == 1)
            {
                if(walker.hasClassName('dropper'))
                {
                    in_dropper = true;
                    break;
                }
                walker = walker.parentNode;
            }
            if(in_dropper)
            {
                Debug.log('using within');
                lookin = TextNode.collectTextNodes(within);
            }
            else
            {
                Debug.log('using target');
                lookin = TextNode.collectTextNodes(target);
            }

            Debug.log('searching ' + lookin.length + ' textnodes');

            for(var n = 0;n < lookin.length;n++)
            {
                var tmp = findCursorInTextNode(lookin[n],x,y);
                if(tmp)
                {
                    ret = tmp;
                    if(ret.left_of || ret.inside)
                        break;
                }
            }

            if(ret.left_of)
                Debug.set('where2','left of ' + Debug.summarise(ret.node));
            else if(ret.inside)
                Debug.set('where2','inside ' + Debug.summarise(ret.node));
            else if(ret.right_of)
                Debug.set('where2','right of ' + Debug.summarise(ret.node));
            else
                Debug.set('where2','invalid');

            Debug.enableLog();
            return ret;
        },

        //TODO just use the mouseevents rangeParent and rangeOffset properties
        //(ff only)
        //do a binary search to find the offset closest to x,y
        getOffset: function(textnode,x,y)
        {
            var debug = '';
            var start = 0;
            var finish = textnode.data.length;
            var mid;
            var on_a_line = 0;
        
            var oldstart,oldmid,oldfinish;

            var dir = '';

            debug += x + ' ' + y + '\n';
            if((start-finish) < 1)
            {
                on_a_line = 1;
                mid = 0;
            }
            while(Math.abs(start - finish) > 1)
            {
                mid = start + Math.floor((finish-start)/2);
                oldstart = start;
                oldmid = mid;
                oldfinish = finish;
                debug += start+'-'+mid+'-'+finish+'\n';
                coords = coordsFromOffset(textnode,mid);
                debug += coords.left + ' ' + coords.width + ' ' + coords.top + ' ' + coords.height+'\n';
                if(coords.top <= y && y <= (coords.top + coords.height))
                {
                    on_a_line = 1;
                    if(x <= coords.left)
                    {
                        dir = '<<<';
                        finish = mid;
                    }
                    else
                    {
                        dir = '>>>';
                        start = mid;
                    }
                }
                else if(y <= coords.top)
                {
                    dir = '^^^';
                    finish = mid;
                }
                else
                {
                    dir = 'vvv';
                    start = mid;
                }
                debug += textnode.data.substr(oldstart,oldmid-oldstart)+dir+textnode.data.substr(oldmid,oldfinish-oldmid)+'\n';
            }

            coords = coordsFromOffset(textnode,mid);
            if(x < coords.left)
                mid--;
            var pos1 = coordsFromOffset(textnode,mid);
            var pos2 = coordsFromOffset(textnode,mid+1);
            var midPoint = (pos1.left + pos2.left)/2;
            if(x > midPoint)
                mid++;

            debug += mid;

            //Debug.set('bin_search',debug);
            //Debug.set('offset',mid);
            if(on_a_line)
                return mid;
            else
                return -1;
        },

        //join any adjacent textnodes that are children of parent
        joinChildren: function(parent)
        {
            while(1)
            {
                var children = parent.childNodes;
                var n = 0;
                var changed = 0;
                for(var n = 0;n < children.length-1;n++)
                {
                    var left = children[n];
                    var right = children[n+1];
                    if(left.nodeType == right.nodeType && left.nodeType == 3)
                    {
                        //Debug.log('joined: "' + left.data + '" to "' + right.data + '"');
                        left.data += right.data;
                        right.parentNode.removeChild(right);
                        changed = 1;
                        break;
                    }
                }
                if(!changed)
                    break;
            }
        },

        coordsFromOffset: function(textnode,offset)
        {
            return coordsFromOffset(textnode,offset);
        },

        //recursively collect textnodes from el and return a flat array
        collectTextNodes: function(el)
        {
            var textnodes = new Array();
            var children = el.childNodes;
            for(var n = 0;n < children.length;n++)
            {
                var c = children[n];
                if(c.nodeType == 3)
                    textnodes.push(c);
                else if(!c.hasClassName('dropper'))
                {
                    var descendants = TextNode.collectTextNodes(c);
                    descendants.each(function(d){
                        textnodes.push(d);
                    });
                }
            }
            return textnodes;
        }
    };

    //check if the cursor is somewhere inside this textnode
    var findCursorInTextNode = function(textnode,x,y)
    {
        if(textnode.data.replace(/[ \n\t]*/,'') == '')
            return false;

        Debug.disableLog();
        var ret = null;
        var finish = textnode.data.length;

        var start_coords = coordsFromOffset(textnode,0);
        var end_coords = coordsFromOffset(textnode,finish);
        Debug.log('');
        Debug.log('looking for cursor in ' + Debug.summarise(textnode));
        Debug.log('left ' + Debug.summarise(textnode.previousSibling));
        Debug.log('right ' + Debug.summarise(textnode.nextSibling));
        Debug.log('x ' + x);
        Debug.log('start ' + start_coords.left);
        Debug.log('end ' + end_coords.left);
        if(start_coords.top <= y && y < (end_coords.top + end_coords.height))
        {
            Debug.log('cursor is inside textnode vertically')
            if(start_coords.top == end_coords.top)
            {
                Debug.log('single line');
                //this is a single line textnode
                if(x < start_coords.left)
                {
                    Debug.log('left');
                    ret = {left_of:true,node:textnode};
                }
                else if(start_coords.left <= x && x <= end_coords.left)
                {
                    Debug.log('inside');
                    ret = {inside:true,node:textnode};
                }
                else
                {
                    Debug.log('right');
                    ret = {right_of:true,node:textnode};
                }
            }
            else
            {
                Debug.log('multi line');
                //multiline, look at its parentNode
                if(y < end_coords.top)
                {
                    var parent = textnode.parentNode;
                    if(parent.hasClassName('hilighted'))
                        parent = parent.parentNode;
                    var parent_pos = parent.cumulativeOffset();
                    if(parent_pos.left < x && x < (parent_pos.left +
                        parent.getWidth()))
                        ret = {inside:true,node:textnode};
                }
                else
                {
                    if(x < end_coords.left)
                        ret = {inside:true,node:textnode};
                    else
                        ret = {right_of:true,node:textnode};
                }
            }
        }
        else
            Debug.log('cursor is not inside textnode vertically')

        Debug.enableLog();

        return ret;
    };

    var coordsFromOffset = function(textnode,offset)
    {
        var text = textnode.data;
        var len = text.length;

        var parent = textnode.parentNode;

        var left = document.createTextNode(text.substr(0,offset));
        var right = document.createTextNode(text.substr(offset,len));

        var result = new Object();

        var marker = document.createElement('span');
        marker.appendChild(document.createTextNode('.'));
        //TODO do a browser check and use 'inline-block' for non mozilla
        //browsers
        //as a fallback, do the old method of testing dimensions with content
        //then position without
        marker.style.display = '-moz-inline-box';
        marker.style.width = '0px';

        parent.insertBefore(left,textnode);
        parent.insertBefore(marker,textnode);
        parent.insertBefore(right,textnode);
        parent.removeChild(textnode);
        //get the width and height while marker has content
        var result = {
            width: marker.getWidth(),
            height: marker.getHeight()
        };
        //then remove the content, find the left and top (which will actually be
        //the bottom due to the lack of content)
        //marker.removeChild(marker.firstChild);
        pos = marker.cumulativeOffset();
        result.left = pos.left;
        //result.top = pos.top-result.height;
        result.top = pos.top;
        parent.insertBefore(textnode,left);
        parent.removeChild(left);
        parent.removeChild(marker);
        parent.removeChild(right);

        return result;
    };

    return _pub;
}();
