// Copyright 1997-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 "OHHTMLDisplayProcessor.h"

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

#import "OHImageCell.h"
#import "OWSGMLDTD-OHHTMLDTD.h"
#import "OHTextBuilder.h"

RCS_ID("$Header: /Network/Source/CVS/OmniGroup/Frameworks/OmniHTML/Building.subproj/OHHTMLDisplayProcessor-Lists.m,v 1.8 2000/02/02 05:37:24 kc Exp $")

@implementation OHHTMLDisplayProcessor (Lists)

static OWSGMLTagType *ddTagType;
static OWSGMLTagType *dirTagType;
static OWSGMLTagType *dlTagType;
static OWSGMLTagType *dtTagType;
static OWSGMLTagType *liTagType;
static OWSGMLTagType *menuTagType;
static OWSGMLTagType *olTagType;
static OWSGMLTagType *ulTagType;

static unsigned int liTypeAttributeIndex;
static unsigned int liValueAttributeIndex;
static unsigned int listContinueAttributeIndex;
static unsigned int listPlainAttributeIndex;
static unsigned int listSeqnumAttributeIndex;
static unsigned int listStartAttributeIndex;
static unsigned int listTypeAttributeIndex;

static NSString *discBulletString = nil;
static NSString *circleBulletString;
static NSString *squareBulletString;

+ (void)didLoad;
{
    OWSGMLMethods *methods;
    OWSGMLDTD *dtd;

    dtd = [self dtd];

    dlTagType = [dtd tagTypeNamed:@"dl"];
    dtTagType = [dtd tagTypeNamed:@"dt"];
    ddTagType = [dtd tagTypeNamed:@"dd"];
    menuTagType = [dtd tagTypeNamed:@"menu"];
    dirTagType = [dtd tagTypeNamed:@"dir"];
    olTagType = [dtd tagTypeNamed:@"ol"];
    ulTagType = [dtd tagTypeNamed:@"ul"];
    liTagType = [dtd tagTypeNamed:@"li"];

    listContinueAttributeIndex = [ulTagType addAttributeNamed:@"continue"];
    listPlainAttributeIndex = [ulTagType addAttributeNamed:@"plain"];
    listSeqnumAttributeIndex = [ulTagType addAttributeNamed:@"seqnum"];
    listStartAttributeIndex = [ulTagType addAttributeNamed:@"start"];
    listTypeAttributeIndex = [ulTagType addAttributeNamed:@"type"];

    [dirTagType shareAttributesWithTagType:ulTagType];
    [menuTagType shareAttributesWithTagType:ulTagType];
    [olTagType shareAttributesWithTagType:ulTagType];

    liTypeAttributeIndex = [liTagType addAttributeNamed:@"type"];
    liValueAttributeIndex = [liTagType addAttributeNamed:@"value"];

    methods = [self sgmlMethods];

    [methods registerMethod:@"DefinitionList" forTagName:@"dl"];
    [methods registerMethod:@"DefinitionTerm" forTagName:@"dt"];
    [methods registerMethod:@"DefinitionData" forTagName:@"dd"];

    [methods registerMethod:@"List" forTagName:@"dir"];
    [methods registerMethod:@"List" forTagName:@"menu"];
    [methods registerMethod:@"OrderedList" forTagName:@"ol"];
    [methods registerMethod:@"List" forTagName:@"ul"];
    [methods registerMethod:@"ListItem" forTagName:@"li"];
}

- (void)processDefinitionListTag:(OWSGMLTag *)tag;
{
    float oldLeftIndent, oldFirstIndent;
    float oldHangingIndent;
    float oldDefinitionListIndent;
    
    [textBuilder beginBlock];

    oldLeftIndent = [textBuilder leftIndent];
    oldFirstIndent = [textBuilder firstIndent];
    oldHangingIndent = [textBuilder hangingIndent];
    oldDefinitionListIndent = definitionListIndent;

    if (oldDefinitionListIndent == oldLeftIndent) {
	definitionListIndent = oldLeftIndent + 40.0;
	[textBuilder setLeftIndent:definitionListIndent];
    } else
	definitionListIndent = oldLeftIndent;

    [self processContentForTag:tag];
    [textBuilder endBlock];

    definitionListIndent = oldDefinitionListIndent;
    [textBuilder setHangingIndent:oldHangingIndent];
    [textBuilder setLeftIndent:oldLeftIndent];
    [textBuilder setFirstIndent:oldFirstIndent];
}

- (void)processDefinitionTermTag:(OWSGMLTag *)tag;
{
    [textBuilder ensureAtStartOfLine];
    if (definitionListIndent == -1)
	definitionListIndent = [textBuilder leftIndent];
    [textBuilder setLeftIndent:definitionListIndent];
}

- (void)processDefinitionDataTag:(OWSGMLTag *)tag;
{
    [textBuilder ensureAtStartOfLine];
    if (definitionListIndent == -1)
	definitionListIndent = [textBuilder leftIndent];
    [textBuilder setLeftIndent:definitionListIndent + 40.0];
}


static OHHTMLListLabelType listLabelTypeForString(NSImage **image, NSString *aString)
{
    if (!aString)
	return LABEL_UNSPECIFIED;

    if (*image) {
        [*image release];
        *image = nil;
    }

    if ([aString length] == 1)
        switch ([aString firstCharacter]) {
            case '1':
                return LABEL_NUMBERS;
            case 'I':
                return LABEL_ROMAN;
            case 'i':
                return LABEL_SMALLROMAN;
            case 'A':
                return LABEL_LETTERS;
            case 'a':
                return LABEL_SMALLLETTERS;
            default:
                break;
        }
    
    aString = [aString lowercaseString];
    if ([aString isEqualToString:@"disc"])
	return LABEL_DISC;
    if ([aString isEqualToString:@"circle"])
	return LABEL_CIRCLE;
    if ([aString isEqualToString:@"square"])
	return LABEL_SQUARE;
	
    *image = [NSImage imageNamed:[aString lowercaseString]];
    if (*image)
	return LABEL_IMAGE;
    return LABEL_UNSPECIFIED;
}


- (void)processListTag:(OWSGMLTag *)tag defaultType:(OHHTMLListLabelType)defaultListLabelType;
{
    OHHTMLListLabelType oldListLabelType;
    NSImage *oldBulletImage;
    int oldListIndex;
    float oldLeftIndent, oldFirstIndent;
    float oldHangingIndent;
    
    listRecursionLevel++;

    oldBulletImage = bulletImage; // inherited retain
    oldListLabelType = listLabelType;
    listLabelType = LABEL_UNSPECIFIED;
    bulletImage = nil;
    if (sgmlTagAttributePresentAtIndex(tag, listPlainAttributeIndex))
	listLabelType = LABEL_NONE;
    if (sgmlTagAttributePresentAtIndex(tag, listTypeAttributeIndex))
	listLabelType = listLabelTypeForString(&bulletImage, sgmlTagValueForAttributeAtIndex(tag, listTypeAttributeIndex));
    if (listLabelType == LABEL_UNSPECIFIED)
	listLabelType = defaultListLabelType;
    
    if (listLabelType == LABEL_UNORDERED) {
	switch (listRecursionLevel) {
	case 1:
	    listLabelType = LABEL_DISC;
	    break;
	case 2:
	    listLabelType = LABEL_CIRCLE;
	    break;
	default:
	case 3:
	    listLabelType = LABEL_SQUARE;
	    break;
	}
    }
    
    if (!discBulletString) {
        discBulletString = [[NSString stringWithCharacter:8729 /* used to be 8226 */] retain];
        circleBulletString = [[NSString stringWithCharacter:45 /* 9675, if it weren't ugly */] retain];
        squareBulletString = [[NSString stringWithCharacter:164 /* 9633, if it weren't ugly */] retain];
    }
    
    oldListIndex = listIndex;
    if (sgmlTagAttributePresentAtIndex(tag, listStartAttributeIndex))
	listIndex = [sgmlTagValueForAttributeAtIndex(tag, listStartAttributeIndex) intValue];
    else if (sgmlTagAttributePresentAtIndex(tag, listSeqnumAttributeIndex))
	listIndex = [sgmlTagValueForAttributeAtIndex(tag, listSeqnumAttributeIndex) intValue];
    else if (sgmlTagAttributePresentAtIndex(tag, listContinueAttributeIndex))
	listIndex = lastListFinalIndex;
    else
	listIndex = 1;

    if (listRecursionLevel == 1)
        [textBuilder beginBlock];
    oldLeftIndent = [textBuilder leftIndent];
    oldFirstIndent = [textBuilder firstIndent];
    oldHangingIndent = [textBuilder hangingIndent];
    [textBuilder setLeftIndent:oldLeftIndent + 40.0];

    [self processContentForTag:tag];

    [textBuilder ensureAtStartOfLineIgnoringInfiniteHack];
    if (listRecursionLevel == 1)
	[textBuilder endBlock];

    [textBuilder setHangingIndent:oldHangingIndent];
    [textBuilder setLeftIndent:oldLeftIndent];
    [textBuilder setFirstIndent:oldFirstIndent];
    listRecursionLevel--;

    lastListFinalIndex = listIndex;
    listIndex = oldListIndex;
    listLabelType = oldListLabelType;
    [bulletImage release];
    bulletImage = oldBulletImage; // inherited retain
}

- (void)processOrderedListTag:(OWSGMLTag *)tag;
{
    [self processListTag:tag defaultType:LABEL_NUMBERS];
}

- (void)processListTag:(OWSGMLTag *)tag;
{
    [self processListTag:tag defaultType:LABEL_UNORDERED];
}

typedef struct {
    char character;
    int size;
    int subtraction;
} roman_numeral;

roman_numeral lower_numerals[] = 
    {{'m', 1000, 100},
     {'d',  500, 100},
     {'c',  100,  10},
     {'l',   50,  10},
     {'x',   10,   1},
     {'v',    5,   1},
     {'i',    1,   0},
     {'\0',   0,   0}};

roman_numeral upper_numerals[] = 
    {{'M', 1000, 100},
     {'D',  500, 100},
     {'C',  100,  10},
     {'L',   50,  10},
     {'X',   10,   1},
     {'V',    5,   1},
     {'I',    1,   0},
     {'\0',   0,   0}};

static NSString *list_roman_numeral(int number, BOOL caps)
{
    char buffer[40];
    char *ptr = buffer;
    roman_numeral *numeral_ptr = caps ? upper_numerals : lower_numerals;

    // Surprisingly, I didn't make up this screwed up limit calculation.  It's what Netscape 2.0b5 for the Macintosh does.  Of course, they also have their tag overlap their text, but c'est la vie.
    if (number > 3999)
	number = number % 3999 + 1;

    while (numeral_ptr->size) {
	while (number >= numeral_ptr->size) {
	    *ptr++ = numeral_ptr->character;
	    number -= numeral_ptr->size;
	}
	if (number >= numeral_ptr->size - numeral_ptr->subtraction) {
	    roman_numeral *sub_ptr = numeral_ptr;
	    while (sub_ptr->size != numeral_ptr->subtraction)
		sub_ptr++;
	    *ptr++ = sub_ptr->character;
	    *ptr++ = numeral_ptr->character;
	    number -= (numeral_ptr->size - sub_ptr->size);
	}
	numeral_ptr++;
    }
    *ptr = '\0';
    return [NSString stringWithCString:buffer];
}

static NSString *list_letter(int number, BOOL caps)
{
    char buffer[8];
    char *digit;

    digit = buffer + 19;
    *digit-- = '\0';
    while (digit >= buffer && number > 26) {
        *digit-- = (number - 1) % 26 + (caps ? 'A' : 'a');
        number = (number - 1) / 26;
    }
    if (digit < buffer) // Not likely...INT_MAX is only "fxshrxw"
        return discBulletString;
    *digit = number + (caps ? 'A' : 'a') - 1;
    return [NSString stringWithCString:digit];
}

#if OBOperatingSystemMajorVersion >= 5

// The AppKit is now thread-safe

- (float)computeWidthOfPrefixString:(NSString *)aString inFont:(NSFont *)aFont;
{
    NSDictionary *attributes;
    float width;

    attributes = [[NSDictionary alloc] initWithObjectsAndKeys:aFont, NSFontAttributeName, nil];
    width = [[aString stringByAppendingString:@" "] sizeWithAttributes:attributes].width;
    [attributes release];

    return width;
}

#else

// AppKit is not yet thread-safe, and -[NSString sizeWithAttributes:] calls +[NSRulebook glyphGeneratorForEncoding:language:font:makeSharable:] which checks to make sure it's in the main thread, so we can't even share the same logic.  (Fortunately, -[NSFont widthOfString:] doesn't perform that check.)

- (float)computeWidthOfPrefixString:(NSString *)aString inFont:(NSFont *)aFont;
{
    float width = 20.0;

#warning -computeWidthOfPrefixString:inFont: is slow due to appkit thread locking
    // -[NSFont widthOfString:] has to lock the appkit thread, which introduces a bunch of latency. We should really cache the width for all the font/bullet combinations we've seen.
    // We now cache the well known strings as well as the last one we've seen for a particular font. --Len

    [NSThread lockMainThread];
    NS_DURING {
        width = [aFont widthOfString:[aString stringByAppendingString:@" "]];
    } NS_HANDLER {
        NSLog(@"Warning: Exception calculating width of prefix string '%@': %@.  Using default width of 20 pixels instead", aString, [localException reason]);
    } NS_ENDHANDLER;
    [NSThread unlockMainThread];

    return width;
}

#endif

- (float)widthOfPrefixString:(NSString *)aString inFont:(NSFont *)aFont;
{
    if (!aFont)
        return 20.0;

    if (!aString)
        return 0.0;

    if (aFont != cachedFont) {
        discBulletStringWidth = -1.0;
        circleBulletStringWidth = -1.0;
        squareBulletStringWidth = -1.0;
        [cachedPrefixString release];
        cachedPrefixString = nil;
        [cachedFont release];
        cachedFont = [aFont retain];

        if (aString == discBulletString)
            return (discBulletStringWidth = [self computeWidthOfPrefixString:aString inFont:aFont]);
        else if (aString == circleBulletString)
            return (circleBulletStringWidth = [self computeWidthOfPrefixString:aString inFont:aFont]);
        else if (aString == squareBulletString)
            return (squareBulletStringWidth = [self computeWidthOfPrefixString:aString inFont:aFont]);
        else {
            cachedPrefixString = [aString retain];
            return (cachedPrefixStringWidth = [self computeWidthOfPrefixString:aString inFont:aFont]);
        }
    } else {
        if (aString == discBulletString && discBulletStringWidth >= 0.0)
            return discBulletStringWidth;
        else if (aString == circleBulletString && circleBulletStringWidth >= 0.0)
            return circleBulletStringWidth;
        else if (aString == squareBulletString && squareBulletStringWidth >= 0.0)
            return squareBulletStringWidth;
        else if ([aString isEqualToString:cachedPrefixString])
            return cachedPrefixStringWidth;
        else {
            if (aString == discBulletString)
                return (discBulletStringWidth = [self computeWidthOfPrefixString:aString inFont:aFont]);
            else if (aString == circleBulletString)
                return (circleBulletStringWidth = [self computeWidthOfPrefixString:aString inFont:aFont]);
            else if (aString == squareBulletString)
                return (squareBulletStringWidth = [self computeWidthOfPrefixString:aString inFont:aFont]);
            else {
                [cachedPrefixString release];
                cachedPrefixString = [aString retain];
                return (cachedPrefixStringWidth = [self computeWidthOfPrefixString:aString inFont:aFont]);
            }
        }
    }
}

- (void)processListItemTag:(OWSGMLTag *)tag;
{
    NSString *type, *value, *prefix;
    float prefixWidth;

    [textBuilder ensureAtStartOfLineIgnoringInfiniteHack];

    if (listLabelType == LABEL_NONE) {
	[textBuilder prepareForMoreText];
	[textBuilder pretendInfiniteVerticalSpace];
	return;
    }

    type = sgmlTagValueForAttributeAtIndex(tag, liTypeAttributeIndex);
    value = sgmlTagValueForAttributeAtIndex(tag, liValueAttributeIndex);

    if (type)
	listLabelType = listLabelTypeForString(&bulletImage, type);
    if (value)
	listIndex = [value intValue];

    switch (listLabelType) {
        // Unordered lists
        default:
        case LABEL_UNORDERED:
        case LABEL_DISC:
            prefix = discBulletString;
            break;
        case LABEL_CIRCLE:
            prefix = circleBulletString;
            break;
        case LABEL_SQUARE:
            prefix = squareBulletString;
            break;
        case LABEL_IMAGE:
            prefix = @"";
            break;

        // Ordered lists
        case LABEL_NUMBERS:
            prefix = [NSString stringWithFormat:@"%d.", listIndex++];
            break;
        case LABEL_ROMAN:
            prefix = [NSString stringWithFormat:@"%@.", list_roman_numeral(listIndex++, YES)];
            break;
        case LABEL_SMALLROMAN:
            prefix = [NSString stringWithFormat:@"%@.", list_roman_numeral(listIndex++, NO)];
            break;
        case LABEL_LETTERS:
            prefix = [NSString stringWithFormat:@"%@.", list_letter(listIndex++, YES)];
            break;
        case LABEL_SMALLLETTERS:
            prefix = [NSString stringWithFormat:@"%@.", list_letter(listIndex++, NO)];
            break;
    }

    prefixWidth = [self widthOfPrefixString:prefix inFont:[textBuilder currentFont]];
    if (bulletImage) {
	NSSize bulletSize;
	
	bulletSize = [bulletImage size];
	prefixWidth += bulletSize.width;
    }

#warning This restriction is probably no longer necessary.  Check after DR2 release.

    // Setting a negative indent will hang the Text object in NXScanALine(). So don't do that.
//    if ([textBuilder leftIndent] + [textBuilder hangingIndent] < prefixWidth)
//        [textBuilder setLeftIndent:prefixWidth - [textBuilder hangingIndent]];

    [textBuilder setLeftIndent:[textBuilder leftIndent] + [textBuilder hangingIndent] - prefixWidth];
    [textBuilder setHangingIndent:prefixWidth];
    if (!bulletImage) {
	[textBuilder writeProcessedString:prefix];
    } else {
	OHImageCell *imageCell;
	
    	imageCell = [[OHImageCell alloc] init];
	[imageCell setImage:bulletImage];
	[textBuilder writeNonWhitespaceCellObject:imageCell];
	[imageCell release];
    }
    [textBuilder addWhitespace];
    [textBuilder pretendInfiniteVerticalSpace];
    [textBuilder pretendAtStartOfLine];
}

@end
