// Copyright 1999-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 <OmniBase/OmniBase.h>
#import <OmniFoundation/OmniFoundation.h>
#import <OWF/OWF.h>

#import "OHHTMLDocument.h"
#import "OWScriptContext.h"
#import "OWScriptDocumentProxy.h"
#import "OWPipeline-ScriptExtensions.h"
#import "OWContentInfo-Scripting.h"
#import "OWScriptBlockTarget.h"

RCS_ID("$Header: /Network/Source/CVS/OmniGroup/Frameworks/OmniHTML/Scripting.subproj/OHHTMLDisplayProcessor-Scripting.m,v 1.17 2000/01/20 00:27:56 kc Exp $")

@interface OHHTMLDisplayProcessor_ScriptingJoiner : OFObject <OWTarget>
{
    OHHTMLDisplayProcessor *target;
    OWObjectStream *result;
    NSConditionLock *resultLock;
}

- initWithTarget:(OHHTMLDisplayProcessor *)target;
- (OWObjectStream *)result;
- (OWContentType *)targetContentType;
- (OWTargetContentDisposition)pipeline:(OWPipeline *)aPipeline hasContent:(id <OWContent>)someContent;

@end

@implementation OHHTMLDisplayProcessor (Scripting)

static OWSGMLTagType *headTagType;
static OWSGMLTagType *scriptTagType;
static OWSGMLTagType *noScriptTagType;
static unsigned int scriptLanguageAttributeIndex;
static unsigned int scriptSrcAttributeIndex;
static unsigned int scriptContentTypeAttributeIndex;
static unsigned int scriptDeferAttributeIndex;
static unsigned int noScriptLanguageAttributeIndex;

static unsigned int OHHTMLDisplayProcessorScriptingDebugLevel = 0;

+ (void)didLoad
{
    OWSGMLDTD *dtd;

    dtd = [self dtd];

    headTagType = [dtd tagTypeNamed:@"head"];
    scriptTagType = [dtd tagTypeNamed:@"script"];
    [scriptTagType setContentIsNotValidSGML:YES];
    noScriptTagType = [dtd tagTypeNamed:@"noscript"];

    scriptLanguageAttributeIndex = [scriptTagType addAttributeNamed:@"language"];
    scriptSrcAttributeIndex = [scriptTagType addAttributeNamed:@"src"];
    scriptContentTypeAttributeIndex = [scriptTagType addAttributeNamed:@"type"];
    scriptDeferAttributeIndex = [scriptTagType addAttributeNamed:@"defer"];

    noScriptLanguageAttributeIndex = [noScriptTagType addAttributeNamed:@"language"];
    
    [[self sgmlMethods] registerMethod:@"Script" forTagName:@"script"];
    [[self sgmlMethods] registerMethod:@"Noscript" forTagName:@"noscript"];
}

- (OWScriptContext *)hintedScriptContext
{
    OWScriptContext *scriptContext;

    scriptContext = [[htmlDocument contentInfo] scriptContext];

    /* TODO: This will totally blow dead goats if executed in the HEAD of a document that uses a META tag to redirect itself to somewhere else. */

    if (![scriptContext getFrameHint:NULL]) {
        id <OWTarget> pipelineTarget;
        OWScriptFrameProxy *targetProxy;

        pipelineTarget = [pipeline target];
        if ([pipelineTarget respondsToSelector:@selector(scriptProxy)]) {
            /* this cast really should be unnecessary */
            targetProxy = [(id <OHDocumentFrame>)pipelineTarget scriptProxy];
            [scriptContext setFrame:targetProxy hint:YES];
        }

        /* if we fail to set a frame hint, someone else will inevitably croak, but at least we've passed the buck */
    }

    return scriptContext;
}

- (void)processScriptTag:(OWSGMLTag *)tag;
{
    id <OWSGMLToken> sgmlToken;
    OWSGMLTagType *tagType;
    NSMutableData *accumulatedScript;
    NSMutableDictionary *evalOptions;
    NSString *thisScriptLanguage, *scriptSource;
    OWScriptContext *thisContext;
    id <OWTarget> pipelineTarget;
    id <OWScriptInterpreter> scriptInterpreter;
    OWScriptDocumentProxy *thisDocProxy;
    OWDataStream *interjections;
    unsigned int interjectionSize;
    BOOL defer, useInlineScript;

    thisScriptLanguage = sgmlTagValueForAttributeAtIndex(tag, scriptLanguageAttributeIndex);
    scriptSource = sgmlTagValueForAttributeAtIndex(tag, scriptSrcAttributeIndex);
    defer = sgmlTagAttributePresentAtIndex(tag, scriptDeferAttributeIndex);

    // Any document which is the result of JavaScript is uncacheable. Ideally we should extend the lifetime of the text/html or whatever in this case.
    [pipeline disableContentCache];
    [[pipeline contentCacheForLastAddress] flushContentOfType:[htmlDocument contentType]];
    
    scriptBlockCount++;
    
    // TODO: Handle the Script-Content-Type: header (HTML4.0) 
    
    // Get the script context.
    thisContext = [[htmlDocument contentInfo] scriptContext];

    // Figure out what frame to give the script context. 
    if ([self hasOpenTagOfType:headTagType]) {
        /* Scripts evaluated in the HEAD element don't get a frame. */
    } else {
        /* TODO: -[pipeline target] doesn't necessarily return the object the pipeline will give its content to; we need to correct for that */
        pipelineTarget = [pipeline target];
        if ([pipelineTarget respondsToSelector:@selector(scriptProxy)]) {
            id targetProxy; // OWScriptFrameProxy

            targetProxy = [(id <OHDocumentFrame>)pipelineTarget scriptProxy];
            [thisContext setFrame:targetProxy hint:YES];
        }
    }
    
    [thisContext setBaseAddress:baseAddress];
    // TODO: Check how the base address scopes wrt javascript.

    accumulatedScript = nil;
    useInlineScript = YES;
    
    // Fetch from scriptSource if specified
    if (scriptSource) {
        OWWebPipeline *fetchPipeline;
        OWScriptBlockTarget *fetchTarget;
        OWAddress *sourceAddress;
        BOOL badSrc = NO;

        sourceAddress = [baseAddress addressForRelativeString:scriptSource];
        if (!sourceAddress)
            /* TODO: what to do with garbage addresses? */
            badSrc = YES;

        if (!badSrc) {
            fetchTarget = [[OWScriptBlockTarget alloc] initWithParentContentInfo:[pipeline contentInfo]];
            fetchPipeline = [[OWWebPipeline alloc] initWithContent:sourceAddress target:fetchTarget];
            [fetchPipeline startProcessingContent];
            [fetchPipeline release];
            accumulatedScript = [[fetchTarget result] retain];
            if (!accumulatedScript) {
                badSrc = YES;
            } else {
                if ([fetchTarget resultLanguage])
                    thisScriptLanguage = [fetchTarget resultLanguage];
                useInlineScript = NO;
            }
            [fetchTarget release];
        }
    }

    if (!accumulatedScript)
        accumulatedScript = [[NSMutableData alloc] init];

    NS_DURING;

    /* Consume html up to </SCRIPT> */
    tagType = sgmlTagType(tag);
    while ((sgmlToken = [objectCursor readObject])) {
        switch ([sgmlToken tokenType]) {
            case OWSGMLTokenTypeEndTag:
                // if (sgmlTagType((OWSGMLTag *)sgmlToken) == tagType)
                goto breakbreak; // eat hot flaming language lossage, Dijkstra!
            case OWSGMLTokenTypeCData:
                if (useInlineScript) {
                    NSData *tokenData;

                    tokenData = [(NSString *)sgmlToken dataUsingEncoding:NSISOLatin1StringEncoding allowLossyConversion:YES];
                    [accumulatedScript appendData:tokenData];
                }
                break;
            default:
                break;
        }
    }
breakbreak:

    NS_HANDLER {
        [accumulatedScript release];
        [localException raise];
    } NS_ENDHANDLER;
            

    /* TODO: We don't handle scriptContentType or defer. They're both HTML4.0 additions; defer could speed us up considerably in some cases */
    
    scriptInterpreter = [thisContext interpreterForLanguage:thisScriptLanguage];

    if (!defer) {
        thisDocProxy = [pipeline scriptProxy];
        OBASSERT([thisDocProxy isKindOfClass:[OWScriptDocumentProxy class]]);
        interjections = [[OWDataStream alloc] init];
        [interjections setContentType:[OWContentType contentTypeForString:@"text/html"]];
        OBASSERT(![thisDocProxy isWritable]);
        [thisDocProxy setWriteBuffer:interjections];
    } 

    evalOptions = [[NSMutableDictionary alloc] init];
    if (scriptSource) {
        [evalOptions setObject:scriptSource forKey:@"source"];
    } else {
        [evalOptions setObject:[pipeline lastAddress] forKey:@"source"];
        [evalOptions setIntValue:scriptBlockCount forKey:@"block"];
    }
    NS_DURING {
        [scriptInterpreter evaluate:accumulatedScript options:evalOptions];
    } NS_HANDLER {
        // TODO: Something clever with exceptions raised during script execution
        NSLog(@"%@: Exception during script execution: %@", OBShortObjectDescription(self), localException);
    } NS_ENDHANDLER;
    [accumulatedScript release];
    [evalOptions release];

    if (!defer) {
        [thisDocProxy setWriteBuffer:nil];
        [interjections dataEnd];
        interjectionSize = [interjections dataLength];
        if (OHHTMLDisplayProcessorScriptingDebugLevel >= 1) {
            NSLog(@"%@: Interjected %d bytes", OBShortObjectDescription(self), interjectionSize);
        }
        if (interjectionSize > 0) {
            // TODO: It might be more efficient to always start the pipeline, and let it run in parallel with the JS interpreter --- not sure.
            OWPipeline *interjectionPipe;
            OHHTMLDisplayProcessor_ScriptingJoiner *joiner;
            OWObjectStream *interjectedObjects;
            OWObjectStreamCursor *newObjectCursor;

            joiner = [[OHHTMLDisplayProcessor_ScriptingJoiner alloc] initWithTarget:self];
            interjectionPipe = [[OWWebPipeline alloc] initWithContent:interjections target:joiner];
            [interjectionPipe startProcessingContent];

            interjectedObjects = [joiner result];
            [interjectionPipe release];

            newObjectCursor = [OWCompoundObjectStream cursorAtCursor:objectCursor beforeStream:interjectedObjects];
            [[newObjectCursor objectStream] setContentType:[[objectCursor objectStream] contentType]];

            [objectCursor release];
            objectCursor = [newObjectCursor retain];

            [joiner release];
        }
        [interjections release];
    }
}

- (void)processNoscriptTag:(OWSGMLTag *)tag;
{
    NSString *noscriptLanguage;
    id <OWScriptInterpreter> scriptInterpreter;

    noscriptLanguage = sgmlTagValueForAttributeAtIndex(tag, scriptLanguageAttributeIndex);

    scriptInterpreter = [[[htmlDocument contentInfo] scriptContext] interpreterForLanguage:noscriptLanguage];

    /* If we can get an interpreter for this language, we want to skip everything until the </noscript> tag */
    if (scriptInterpreter) {
        OWSGMLTagType *tagType;
        id <OWSGMLToken> sgmlToken;

        /* TODO: Does this actually work in all cases? What about nested noscript tags? What about premature eof? */
        tagType = sgmlTagType(tag);
        while ((sgmlToken = [objectCursor readObject])) {
            switch ([sgmlToken tokenType]) {
                case OWSGMLTokenTypeEndTag:
                    if (sgmlTagType((OWSGMLTag *)sgmlToken) == tagType)
                        return;
                default:
                    break;
            }
        }
    }
}

@end

@implementation OHHTMLDisplayProcessor_ScriptingJoiner

#define NO_RESULT 0
#define HAVE_RESULT 1

- initWithTarget:(OHHTMLDisplayProcessor *)aTarget;
{
    self = [super init];
    target = [aTarget retain];
    result = nil;
    resultLock = [[NSConditionLock alloc] initWithCondition:NO_RESULT];
    OBASSERT([self parentContentInfo]);
    return self;
}

- (void)dealloc;
{
    [OWPipeline cancelTarget:self];
    [target release];
    [result release];
    [resultLock release];
    [super dealloc];
}

- (OWObjectStream *)result;
{
    OWObjectStream *returnValue;

    [resultLock lockWhenCondition:HAVE_RESULT];
    returnValue = [result retain];
    [resultLock unlock];
    return [returnValue autorelease];
}

// OWTarget protocol

- (OWContentType *)targetContentType
{
    return [OWContentType contentTypeForString:@"ObjectStream/sgml"];
}

- (OWTargetContentDisposition)pipeline:(OWPipeline *)aPipeline hasContent:(id <OWContent>)someContent
{
    if (![[someContent contentType] isEqual:[self targetContentType]])
        return OWTargetContentDisposition_ContentRejectedCancelPipeline;

    OBASSERT([someContent isKindOfClass:[OWObjectStream class]]);
    [resultLock lock];
    OBASSERT(result == nil);
    result = [someContent retain];
    [resultLock unlockWithCondition:HAVE_RESULT];
    return OWTargetContentDisposition_ContentAccepted;
}

- (NSString *)targetTypeFormatString;
{
    return @"%@ Script";
}

- (OWContentInfo *)parentContentInfo;
{
    return [[target pipeline] contentInfo];
}

@end

