// Copyright 1998-2000 Omni Development, Inc.  All rights reserved.
//
// This software may only be used and reproduced according to the
// terms in the file OmniSourceLicense.html, which should be
// distributed with this project and can also be found at
// http://www.omnigroup.com/DeveloperResources/OmniSourceLicense.html.

#import "OHLine.h"

#import <Foundation/Foundation.h>
#import <AppKit/AppKit.h>
#import <OmniBase/OmniBase.h>
#import <OmniFoundation/OmniFoundation.h>

#import "OHBasicCell.h"
#import "OHLay.h"
#import "OHHTMLView.h"
#import "OHParagraph.h"
#import "OHWord.h"

RCS_ID("$Header: /Network/Source/CVS/OmniGroup/Frameworks/OmniHTML/View.subproj/OHLine.m,v 1.17 2000/01/20 00:28:05 kc Exp $")

@interface OHLine (Private)
- (void)drawSelectionRectForGlyphRange:(NSRange)selectedGlyphRange inHTMLView:(OHHTMLView *)htmlView;
@end

@implementation OHLine

// Init and dealloc

- (id)initFromParagraph:(OHParagraph *)newParagraph wordIndex:(unsigned int *)firstWordIndex lastWordIndex:(unsigned int)lastWordIndex maxX:(float *)maxX bounds:(NSRect)maxBounds alignment:(NSTextAlignment)alignment attributes:(NSDictionary **)attributes attributesRange:(NSRange *)attributesRange inHTMLView:(OHHTMLView *)htmlView;
{
    OFStaticArray *words;
    float lineWidthWithoutTrailingSpaces = 0.0;
    OFStaticArray *lays;
    OHLay *lay = nil;
    BOOL laidOutAttachment = NO;

    paragraph = newParagraph;
    wordRange = NSMakeRange(*firstWordIndex, 0);
    bounds.origin.y = NSMinY(maxBounds);

    lays = [htmlView lays];
    layRange = NSMakeRange([lays count], 0);
    
    // Always lay out a single word, even if there's no room
    words = [htmlView words];

    // Add words until they don't fit or we run out of words in the source paragraph
    while (1) {
        OHWord *word;
        OHBasicCell *attachmentCell;
        float wordWidth;
        BOOL wordRequiresItsOwnLay;

        word = [words objectAtIndex:NSMaxRange(wordRange)];
        
        attachmentCell = [word attachmentCellInHTMLView:htmlView];
        if (attachmentCell)
            [attachmentCell setLayoutBounds:maxBounds];

        if (word->type == OHWordTypeTab) {
            wordWidth = [paragraph positionOfTabStopFollowingPosition:NSMaxX(bounds)] - NSMaxX(bounds);
            [word setTabWidth:wordWidth];
        } else {
            wordWidth = [word widthInHTMLView:htmlView];
        }

        // STOP if we've already laid out a word, and this word won't fit
        if (wordRange.length // Every line must have at least one word
            && word->type != OHWordTypeSpaces // spaces at the end of a line don't take up space, so always add them
            && word->type != OHWordTypeTab // a final tab which takes us to the beginning of the next line is laid out on this line; we'll stop laying out this line after adding it (below)
            && word->type != OHWordTypeReturn // returns belong on the end of a line
            && (wordWidth > NSWidth(maxBounds) - NSWidth(bounds))) // word is too wide to fit
            break; // STOP laying out this line

        laidOutAttachment |= (attachmentCell != nil);

        // Attachments and tabs are placed in their own lays
        wordRequiresItsOwnLay = attachmentCell != nil || word->type == OHWordTypeTab;
        if (!lay || wordRequiresItsOwnLay) {
            lay = [lays newObject];
            lay->wordRange.location = NSMaxRange(wordRange);
            lay->x = bounds.size.width;
            lay->firstWordXLocationInLayoutManager = word->x;

            layRange.length++;
        }

        bounds.size.width += wordWidth;
        lay->width += wordWidth;

        if (word->type != OHWordTypeSpaces && word->type != OHWordTypeTab && word->type != OHWordTypeReturn)
            lineWidthWithoutTrailingSpaces = NSWidth(bounds);
        wordRange.length++;
        lay->wordRange.length++;

        if (word->type == OHWordTypeTab && NSWidth(bounds) > NSWidth(maxBounds))
            break; // This final tab takes us to the next line

        if (NSMaxRange(wordRange) > lastWordIndex)
            break; // Source paragraph out of words (we're at EOL)

        if (wordRequiresItsOwnLay)
            lay = nil; // Force start of new lay

        word = nil; // Force fetch of new word, above
    }

    // Justification
    if (lineWidthWithoutTrailingSpaces >= NSWidth(maxBounds))
        // Don't attempt to center or right-justify lines that are too long to fit, or they'll start to the left of the window.
        bounds.origin.x = NSMinX(maxBounds);
    else {
        switch (alignment) {
            default:
                bounds.origin.x = NSMinX(maxBounds);
                break;
            case NSCenterTextAlignment:
                bounds.origin.x = NSMinX(maxBounds) + floor((NSWidth(maxBounds) - lineWidthWithoutTrailingSpaces) / 2.0);
                break;
            case NSRightTextAlignment:
                bounds.origin.x = NSMinX(maxBounds) + NSWidth(maxBounds) - lineWidthWithoutTrailingSpaces;
                break;
        }
    }

    [self getLineHeightAndAscentForAttributes:attributes attributesRange:attributesRange inHTMLView:htmlView];

    // If we laid out any cells (non-marginal), we need to go through and tell them where their origins are, now that we know our baseline and x offset.
    if (laidOutAttachment) {
        unsigned int layIndex;

        for (layIndex = layRange.location; layIndex < NSMaxRange(layRange); layIndex++) {
            OHLay *lay;
            unsigned int wordIndex;

            lay = [lays objectAtIndex:layIndex];
            for (wordIndex = lay->wordRange.location; wordIndex < NSMaxRange(lay->wordRange); wordIndex++) {
                OHBasicCell *attachmentCell;

                attachmentCell = [[words objectAtIndex:wordIndex] attachmentCellInHTMLView:htmlView];
                if (!attachmentCell)
                    continue;

                // tell each attachment its new origin.
                switch ([attachmentCell verticalAlignment]) {
                    case OHCellAlignVerticalTop:
                        // Position the cell at the top of the line
                        [attachmentCell setOrigin:NSMakePoint(NSMinX(bounds) + lay->x, NSMinY(bounds))];
                        break;
                    default:
                        // Position the cell relative to its baseline offset
                        [attachmentCell setOrigin:NSMakePoint(NSMinX(bounds) + lay->x, NSMinY(bounds) + maxAscent - NSHeight([attachmentCell cellFrame]) - [attachmentCell baselineOffset].y)];
                        break;
                }
            }
        }
    }
    
    *firstWordIndex += wordRange.length;
    *maxX = MAX(*maxX, NSMinX(bounds) + lineWidthWithoutTrailingSpaces);

    return self;
}


// API

- (NSRect)bounds;
{
    return bounds;
}

- (BOOL)drawRect:(NSRect)rect selectedGlyphRange:(NSRange)selectedGlyphRange inHTMLView:(OHHTMLView *)htmlView;
{
    OFStaticArray *lays;
    unsigned int layIndex;

    // If this line is below rect, stop drawing.
    if (NSMinY(bounds) >= NSMaxY(rect))
        return NO;

    if (selectedGlyphRange.length)
        [self drawSelectionRectForGlyphRange:selectedGlyphRange inHTMLView:htmlView];

    // Draw text
    lays = [htmlView lays];
    for (layIndex = layRange.location; layIndex < NSMaxRange(layRange); layIndex++) {
        OHLay *lay;

        lay = [lays objectAtIndex:layIndex];

        [lay drawInLineBounds:bounds maxAscent:maxAscent paragraph:paragraph selectedGlyphRange:selectedGlyphRange inHTMLView:htmlView];
    }

    return YES;
}


- (NSComparisonResult)compareToY:(float)testY;
{
    if (testY < NSMinY(bounds))
        return NSOrderedDescending;
    if (testY >=  NSMaxY(bounds))
        return NSOrderedAscending;
    return NSOrderedSame;
}


- (unsigned int)glyphIndexForPoint:(NSPoint)point fractionOfDistanceThroughGlyph:(float *)fraction isReallyOnGlyph:(BOOL *)isReallyOnGlyph inHTMLView:(OHHTMLView *)htmlView;
{
    unsigned int glyphIndex;
    OFStaticArray *lays;
    unsigned int layIndex;

    if (isReallyOnGlyph)
        *isReallyOnGlyph = NO;
    
    lays = [htmlView lays];
    for (layIndex = layRange.location; layIndex < NSMaxRange(layRange); layIndex++) {
        OHLay *lay;
        NSComparisonResult comparisonResult;

        lay = [lays objectAtIndex:layIndex];

        comparisonResult = [lay compareToPoint:point glyphIndex:&glyphIndex fractionOfDistanceThroughGlyph:fraction lineBounds:bounds paragraph:paragraph inHTMLView:htmlView];

        if (comparisonResult == NSOrderedSame) {
            if (isReallyOnGlyph)
                *isReallyOnGlyph = YES;
            break;
        }

        if (comparisonResult == NSOrderedDescending && layIndex == 0)
            break;
    }

    return glyphIndex;
}

- (NSRect)rectForContainedLay:(OHLay *)lay;
{
    return NSMakeRect(NSMinX(bounds) + lay->x, NSMinY(bounds), lay->width, NSHeight(bounds));
}

- (NSRect)rectForContainedLay:(OHLay *)lay fromCharacterAtIndex:(unsigned int)characterIndex inHTMLView:(OHHTMLView *)htmlView;
{
    float xPositionOfCharacter;

    xPositionOfCharacter = [lay xPositionOfCharacterAtIndex:characterIndex inHTMLView:htmlView];
    return NSMakeRect(NSMinX(bounds) + xPositionOfCharacter, NSMinY(bounds), lay->x + lay->width - xPositionOfCharacter, NSHeight(bounds));
}

- (NSRect)rectForContainedLay:(OHLay *)lay toCharacterAtIndex:(unsigned int)characterIndex inHTMLView:(OHHTMLView *)htmlView;
{
    float xPositionOfCharacter;

    xPositionOfCharacter = [lay xPositionOfCharacterAtIndex:characterIndex inHTMLView:htmlView];
    return NSMakeRect(NSMinX(bounds) + lay->x, NSMinY(bounds), xPositionOfCharacter - lay->x, NSHeight(bounds));
}

- (NSRect)rectForCharacterRange:(NSRange)characterRange inContainedLay:(OHLay *)lay inHTMLView:(OHHTMLView *)htmlView;
{
    float leftXPosition, rightXPosition;

    leftXPosition = [lay xPositionOfCharacterAtIndex:characterRange.location inHTMLView:htmlView];
    rightXPosition = [lay xPositionOfCharacterAtIndex:NSMaxRange(characterRange) inHTMLView:htmlView];
    return NSMakeRect(NSMinX(bounds) + leftXPosition, NSMinY(bounds), rightXPosition - leftXPosition, NSHeight(bounds));
}

@end


@implementation OHLine (SubclassesOnly)

- (void)getMaxAscent:(float *)ascent andMaxDescent:(float *)descent forAttachmentCell:(OHBasicCell *)attachmentCell baselineOffset:(float)baselineOffset attributes:(NSDictionary *)attributes;
{
    NSSize cellSize;
    OHCellAlignment verticalAlignment;
    NSPoint cellBaselineOffset;
    NSFont *font;
    NSRect boundingBox;
    float currentFontAscent, currentFontDescent;

    cellSize = [attachmentCell cellFrame].size;
    verticalAlignment = [attachmentCell verticalAlignment];
    cellBaselineOffset.x = 0;

    font = [attachmentCell font];
    boundingBox = NSIntegralRect([font boundingRectForFont]);
    currentFontAscent = NSMaxY(boundingBox) + baselineOffset;
    currentFontDescent = NSMinY(boundingBox) + baselineOffset;
    
    switch (verticalAlignment) {
        case OHCellAlignVerticalTop:
            // Image top is top of the current font or highest ascent so far
            cellBaselineOffset.y = MAX(currentFontAscent, *ascent) - cellSize.height;
            break;
        case OHCellAlignVerticalTextTop:
            // Image top is top of the current font
            cellBaselineOffset.y = currentFontAscent - cellSize.height;
            break;
        case OHCellAlignVerticalMiddle:
            // Image middle is equal to baseline
            cellBaselineOffset.y = floor(-cellSize.height / 2.0) + baselineOffset;
            break;
        case OHCellAlignVerticalAbsoluteMiddle:
            // Image middle is middle of current ascent plus descent so far
            cellBaselineOffset.y = floor((MAX(currentFontAscent, *ascent) +  MIN(currentFontDescent, *descent) - cellSize.height) / 2.0); 
            break;
        default:
        case OHCellAlignVerticalBaseline:
            // Bottom of cell is on baseline
            cellBaselineOffset.y = baselineOffset;
            break;
        case OHCellAlignVerticalAbsoluteBottom:
            // Bottom of cell is on lowest ascent so far
            cellBaselineOffset.y = MIN(currentFontDescent, *descent);
            break;
    }

    [attachmentCell setBaselineOffset:cellBaselineOffset];
    
    *ascent = MAX(*ascent, cellSize.height + cellBaselineOffset.y);
    *descent = MIN(*descent, cellBaselineOffset.y);
}

@end


@implementation OHLine (Private)

- (void)drawSelectionRectForGlyphRange:(NSRange)selectedGlyphRange inHTMLView:(OHHTMLView *)htmlView;
{
    NSRange glyphRange, lineSelectedGlyphRange;
    float minX = 1e10, maxX = 0.0;
    BOOL selectedRangeStartsInLine, selectedRangeEndsInLine;
    NSRect layoutBounds;
    NSSize textContainerInset;

    glyphRange = [htmlView glyphRangeForWordRange:wordRange];

    lineSelectedGlyphRange = NSIntersectionRange(selectedGlyphRange, glyphRange);
    if (lineSelectedGlyphRange.length == 0)
        return;

    selectedRangeStartsInLine = selectedGlyphRange.location >= lineSelectedGlyphRange.location;
    selectedRangeEndsInLine = NSMaxRange(selectedGlyphRange) <= NSMaxRange(lineSelectedGlyphRange);

    textContainerInset = [htmlView textContainerInset];
    layoutBounds = NSInsetRect([htmlView bounds], textContainerInset.width, textContainerInset.height);

    if (!selectedRangeStartsInLine)
        minX = NSMinX(layoutBounds);
    if (!selectedRangeEndsInLine)
        maxX = NSMaxX(layoutBounds);

    if (selectedRangeEndsInLine || selectedRangeStartsInLine) {
        OFStaticArray *lays;
        unsigned int layIndex;

        lays = [htmlView lays];
        // Draw selection rectangle
        for (layIndex = layRange.location; layIndex < NSMaxRange(layRange); layIndex++) {
            OHLay *lay;

            lay = [lays objectAtIndex:layIndex];

            [lay getMinX:&minX maxX:&maxX forSelectedGlyphRange:selectedGlyphRange lineBounds:bounds inHTMLView:htmlView];
        }
    }

    maxX = MIN(maxX, NSMaxX(layoutBounds));
    if (maxX <= minX)
        return;

    [[NSColor selectedTextBackgroundColor] set];
    NSRectFill(NSMakeRect(minX, NSMinY(bounds), maxX - minX, NSHeight(bounds)));
}

@end
