/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 4 -*-
 *
 * The contents of this file are subject to the Netscape Public
 * License Version 1.1 (the "License"); you may not use this file
 * except in compliance with the License. You may obtain a copy of
 * the License at http://www.mozilla.org/NPL/
 *
 * Software distributed under the License is distributed on an "AS
 * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
 * implied. See the License for the specific language governing
 * rights and limitations under the License.
 *
 * The Original Code is mozilla.org code.
 *
 * The Initial Developer of the Original Code is Netscape
 * Communications Corporation.  Portions created by Netscape are
 * Copyright (C) 1998 Netscape Communications Corporation. All
 * Rights Reserved.
 *
 * Contributor(s): 
 */

#include "nsMultiMixedConv.h"
#include "nsIAllocator.h"
#include "plstr.h"
#include "nsIStringStream.h"
#include "nsIHTTPChannel.h"
#include "nsIAtom.h"
#include "nsIServiceManager.h"
#include "nsIGenericFactory.h"
#include "nsCOMPtr.h"
#include "nsNetUtil.h"
#include "nsXPIDLString.h"
static NS_DEFINE_CID(kComponentManagerCID, NS_COMPONENTMANAGER_CID);
static NS_DEFINE_CID(kIOServiceCID, NS_IOSERVICE_CID);

// other converters for the converter export factory fuctions
#include "nsFTPDirListingConv.h"


// nsISupports implementation
NS_IMPL_ISUPPORTS3(nsMultiMixedConv, nsIStreamConverter, nsIStreamListener, nsIStreamObserver);


// nsIStreamConverter implementation

// No syncronous conversion at this time.
NS_IMETHODIMP
nsMultiMixedConv::Convert(nsIInputStream *aFromStream,
                          const PRUnichar *aFromType,
                          const PRUnichar *aToType,
                          nsISupports *aCtxt, nsIInputStream **_retval) {
    return NS_ERROR_NOT_IMPLEMENTED;
}

// Stream converter service calls this to initialize the actual stream converter (us).
NS_IMETHODIMP
nsMultiMixedConv::AsyncConvertData(const PRUnichar *aFromType, const PRUnichar *aToType,
                                   nsIStreamListener *aListener, nsISupports *aCtxt) {
    NS_ASSERTION(aListener && aFromType && aToType, "null pointer passed into multi mixed converter");

    // hook up our final listener. this guy gets the various On*() calls we want to throw
    // at him.
    //
    // WARNING: this listener must be able to handle multiple OnStartRequest, OnDataAvail()
    //  and OnStopRequest() call combinations. We call of series of these for each sub-part
    //  in the raw stream.
    mFinalListener = aListener;
    return NS_OK;
}

// nsIStreamListener implementation
NS_IMETHODIMP
nsMultiMixedConv::OnDataAvailable(nsIChannel *channel, nsISupports *context,
                                  nsIInputStream *inStr, PRUint32 sourceOffset, PRUint32 count) {
    nsresult rv;
    char *buffer = nsnull, *rootMemPtr = nsnull;
    PRUint32 bufLen, read;

    NS_ASSERTION(channel, "multimixed converter needs a channel");

    if (!mBoundaryCStr) {
        char *bndry = nsnull;
        nsXPIDLCString delimiter;

        // ask the HTTP channel for the content-type and extract the boundary from it.
        nsCOMPtr<nsIHTTPChannel> httpChannel;
        rv = channel->QueryInterface(NS_GET_IID(nsIHTTPChannel), getter_AddRefs(httpChannel));
        if (NS_SUCCEEDED(rv)) {
            nsCOMPtr<nsIAtom> header = NS_NewAtom("content-type");
            if (!header) return NS_ERROR_OUT_OF_MEMORY;
            rv = httpChannel->GetResponseHeader(header, getter_Copies(delimiter));
            if (NS_FAILED(rv)) return rv;
        } else {
            // try asking the channel directly
            rv = channel->GetContentType(getter_Copies(delimiter));
            if (NS_FAILED(rv)) return rv;
        }
        if (!delimiter) return NS_ERROR_FAILURE;
        bndry = PL_strstr(delimiter, "boundary");
        if (!bndry) return NS_ERROR_FAILURE;

        bndry = PL_strchr(bndry, '=');
        if (!bndry) return NS_ERROR_FAILURE;

        bndry++; // move past the equals sign

        nsCAutoString boundaryString(bndry);
        boundaryString.StripWhitespace();
        // we're not using the beginning "--" to delimit boundaries
        // so just make them part of the boundary delimiter.
        boundaryString.Insert("--", 0, 2);                 ;
        mBoundaryCStr = boundaryString.ToNewCString();
        if (!mBoundaryCStr) return NS_ERROR_OUT_OF_MEMORY;
        mBoundaryStrLen = boundaryString.Length();
    }


    rv = inStr->Available(&bufLen);
    if (NS_FAILED(rv)) return rv;

    rootMemPtr = buffer = (char*)nsAllocator::Alloc(bufLen + 1);
    if (!buffer) return NS_ERROR_OUT_OF_MEMORY;

    rv = inStr->Read(buffer, bufLen, &read);
    if (NS_FAILED(rv) || read == 0) return rv;
    buffer[bufLen] = '\0';

    if (mBufferedData.Length() > 0) {
        mBufferedData.Append(buffer);
        nsAllocator::Free(buffer);
        buffer = mBufferedData.ToNewCString();
    }
    char *cursor = buffer;
    PRBool done = PR_FALSE;

    while (cursor) {
        char *boundary = PL_strstr(cursor, mBoundaryCStr);
        if (boundary) {
            mFoundBoundary = PR_TRUE;
            if (cursor == boundary) {
                if (!mNewPart) {
                    // we were processing a part and ran into a boundary
                    // thus that makes it an ending boundary. finish this
                    // part and move on.
                    rv = mFinalListener->OnStopRequest(mPartChannel, context, NS_OK, nsnull);
                    if (NS_FAILED(rv)) break;

                    // Remove the channel from its load group (if any)
                    nsCOMPtr<nsILoadGroup> loadGroup;
            
                    (void) mPartChannel->GetLoadGroup(getter_AddRefs(loadGroup));
                    if (loadGroup) {
                        loadGroup->RemoveChannel(mPartChannel, context, NS_OK, nsnull);
                    }

                    mPartChannel = 0;
                }
                // the boundary occurs at the beginning of the data.
                // we obviously don't want to set it to null, instead
                // set the cursor to the beginnning of the real data
                // and look for another boundary after that.
                cursor += mBoundaryStrLen;
                if (*cursor == '-') {
                    // we've reached the end of the line
                    done = PR_TRUE;
                } else {
                    // we just passed a boundary, therefore we're guaranteed to be
                    // processing a new part.
                    mNewPart = PR_TRUE;
                    // see if there's another boundary.
                    boundary = PL_strstr(cursor, mBoundaryCStr);
                    if (boundary) *boundary = '\0';
                }

                if ( (*cursor == '\r') || (*cursor == '\n') ) {
                    if (cursor[1] == '\n')
                        cursor++; // if it's a CRLF, move two places.
                    cursor++;
                }
            } else {
                *boundary = '\0';
            }
        } else if (!mFoundBoundary) {
            // we haven't seen a boundary yet, buffer this up.
            mBufferedData = cursor;
            break; // XXX do more here.
        }
        if (mNewPart) {
            mLineFeedIncrement = 1;
            // check for a newline char, then figure out if
            // we're dealing w/ CRLFs as line delimiters.
            // unfortunately we can see both :(
            char *newLine = PL_strchr(cursor, '\n');
            if (newLine) {
                if ( (newLine > cursor) && (newLine[-1] == '\r') ) {
                     // CRLF
                     mLineFeedIncrement = 2;
                     newLine--;
                }
            }
            char *headerStart = cursor;
            while (newLine) {
                *newLine = '\0';
                char *colon = PL_strchr(headerStart, ':');
                if (colon) {
                    // Header name
                    *colon = '\0';
                    nsCAutoString headerStr(headerStart);
                    headerStr.StripWhitespace();
                    headerStr.ToLowerCase();
                    nsCOMPtr<nsIAtom> header = NS_NewAtom(headerStr.GetBuffer());
                    if (!header) {
                        nsAllocator::Free(buffer);
                        return NS_ERROR_OUT_OF_MEMORY;
                    }
                    *colon = ':';
                    // END Header name

                    // Header value
                    nsCAutoString headerVal(colon + 1);
                    headerVal.StripWhitespace();
                    // END Header value

                    // examine header
                    if (headerStr.Equals("content-type")) {
                        mContentType = headerVal;
                    } else if (headerStr.Equals("content-length")) {
                        mContentLength = atoi(headerVal);
                    } else if (headerStr.Equals("set-cookie")) {
                        // setting headers on the HTTP channel
                        // causes HTTP to notify, again if necessary,
                        // it's header observers.
                        nsCOMPtr<nsIHTTPChannel> httpChannel = do_QueryInterface(channel, &rv);
                        if (NS_SUCCEEDED(rv)) {
                            rv = httpChannel->SetResponseHeader(header, headerVal);
                            if (NS_FAILED(rv)) {
                                nsAllocator::Free(buffer);
                                return rv;
                            }
                        }
                    }
                }
                if (    (newLine[mLineFeedIncrement] == '\n')
                     || (newLine[mLineFeedIncrement] == '\r')
                     || (newLine == cursor) ) {
                    nsCOMPtr<nsILoadGroup> loadGroup;

                    // that's it we've processed all the headers and 
                    // this is no longer a mNewPart
                    mNewPart = PR_FALSE;

                    // move the newLine beyond the double linefeed marker
                    newLine += mLineFeedIncrement;

                    // First build up a dummy uri.
                    nsCOMPtr<nsIURI> partURI;
                    rv = BuildURI(channel, getter_AddRefs(partURI));
                    if (NS_FAILED(rv)) {
                        nsAllocator::Free(buffer);
                        return rv;
                    }
                    (void) channel->GetLoadGroup(getter_AddRefs(loadGroup));

                    if (mContentType.Length() < 1)
                        mContentType = "text/html"; // default to text/html, that's all we'll ever see anyway
                    rv = NS_NewInputStreamChannel(partURI, mContentType.GetBuffer(), mContentLength,
                                                  nsnull,    // inStr
                                                  loadGroup, // loadGroup
                                                  nsnull,    // notificationCallbacks
                                                  nsIChannel::LOAD_NORMAL,
                                                  nsnull,    // originalURI
                                                  0, 0,
                                                  getter_AddRefs(mPartChannel));
                    if (NS_FAILED(rv)) {
                        nsAllocator::Free(buffer);
                        return rv;
                    }

                    // Add the new channel to the load group (if any)
                    if (loadGroup) {
                      loadGroup->AddChannel(mPartChannel, nsnull);
                    }

                    // Let's start off the load. NOTE: we don't forward on the channel passed
                    // into our OnDataAvailable() as it's the root channel for the raw stream.
                    rv = mFinalListener->OnStartRequest(mPartChannel, context);
                    if (NS_FAILED(rv)) {
                        nsAllocator::Free(buffer);
                        return rv;
                    }
                    break;
                }
                headerStart = newLine + mLineFeedIncrement;
                newLine = PL_strchr(headerStart, '\n');
                if (newLine) {
                    // we're catching LF, LFLF, and CRLF
                    if ( (newLine > cursor) && (newLine[-1] == '\r') ) {
                         mLineFeedIncrement = 2;
                         newLine--;
                    }
                }
            } // end while (newLine)

            if (mNewPart) {
                // we broke out because we have incomplete headers
                // buffer the data and fall out.
                if (*headerStart)
                    mBufferedData = headerStart;
                break;
            } else {
                mPartCount++;
                // we broke out because we reached two linefeeds (noting
                // the end of the header section. move the cursor over the
                // linefeeds into the data and start processing it.
                if (cursor == newLine) {
                    // all we got was a newline in this read. kick out
                    break;
                } else {
                    cursor = newLine + mLineFeedIncrement;
                }
            }
        } // end mNewPart

        // after processing headers, it's possible that cursor points to
        // a null byte. If it does don't send it off because it's meaningless.
        if (!*cursor)
            break; // XXX more to do here.

        // Check for a completed part
        if (!done) {
            // If we've gotten this far we know we're processing a hunk of data
            // and that all the headers for this part have been processed.
            // Just send off the data.
            SendData(cursor, mPartChannel, context);
        }

        if (boundary) {
            // We know this is the end of a part. Set mNewPart to TRUE
            // so anymore data we receive gets kicked into the mNewPart
            // cycle, tell the listener we're done w/ this part, reset
            // the partChannel (mNewPart will create a new one if we
            // get that far), and see if we're completely done.
            mNewPart = PR_TRUE;
            rv = mFinalListener->OnStopRequest(mPartChannel, context, NS_OK, nsnull);
            if (NS_FAILED(rv)) break;

            // Remove the channel from its load group (if any)
            nsCOMPtr<nsILoadGroup> loadGroup;
            
            (void) mPartChannel->GetLoadGroup(getter_AddRefs(loadGroup));
            if (loadGroup) {
                loadGroup->RemoveChannel(mPartChannel, context, NS_OK, nsnull);
            }

            mPartChannel = 0; // kill this channel. it's done
            if (done || (*(boundary+mBoundaryStrLen+1) == '-') ) {
                // it's completely over
                break;
            } else {
                cursor = boundary + mBoundaryStrLen;
            }
            if ( (*cursor == '\r') || (*cursor == '\n') ){
                if (cursor[1] == '\n')
                    cursor++; // if it's a CRLF move two places.
                cursor++;
            }
        } else {
            // no more data. kick out
            break; 
        }
    } // end while (cursor)
    nsAllocator::Free(buffer);

    return rv;
}


// nsIStreamObserver implementation
NS_IMETHODIMP
nsMultiMixedConv::OnStartRequest(nsIChannel *channel, nsISupports *ctxt) {
    return NS_OK;
}

NS_IMETHODIMP
nsMultiMixedConv::OnStopRequest(nsIChannel *channel, nsISupports *ctxt,
                                nsresult status, const PRUnichar *errorMsg) {
    return NS_OK;
}


// nsMultiMixedConv methods
nsMultiMixedConv::nsMultiMixedConv() {
    NS_INIT_ISUPPORTS();
    mBoundaryCStr       = nsnull;
    mBoundaryStrLen     = mPartCount = 0;
    mNewPart            = PR_TRUE;
    mFoundBoundary      = PR_FALSE;
    mContentLength      = -1;
    mLineFeedIncrement  = 1;
}

nsMultiMixedConv::~nsMultiMixedConv() {
    if (mBoundaryCStr) nsAllocator::Free(mBoundaryCStr);
}

nsresult
nsMultiMixedConv::Init() {
    return NS_OK;
}


nsresult
nsMultiMixedConv::SendData(const char *aBuffer, nsIChannel *aChannel, nsISupports *aCtxt) {

    nsresult rv;
    nsCOMPtr<nsISupports> inStreamSup;
    rv = NS_NewStringInputStream(getter_AddRefs(inStreamSup), aBuffer);
    if (NS_FAILED(rv)) return rv;

    nsCOMPtr<nsIInputStream> inStream = do_QueryInterface(inStreamSup, &rv);
    if (NS_FAILED(rv)) return rv;

    PRUint32 len;
    rv = inStream->Available(&len);
    if (NS_FAILED(rv)) return rv;

    return mFinalListener->OnDataAvailable(aChannel, aCtxt, inStream, 0, len);
}

nsresult
nsMultiMixedConv::BuildURI(nsIChannel *aChannel, nsIURI **_retval) {
    nsresult rv;
    nsXPIDLCString uriSpec;
    nsCOMPtr<nsIURI> rootURI;
    rv = aChannel->GetURI(getter_AddRefs(rootURI));
    if (NS_FAILED(rv)) return rv;

    rv = rootURI->GetSpec(getter_Copies(uriSpec));
    if (NS_FAILED(rv)) return rv;

    nsCAutoString dummyURIStr(uriSpec);
    dummyURIStr.Append("##");
    dummyURIStr.Append(mPartCount);

    NS_WITH_SERVICE(nsIIOService, serv, kIOServiceCID, &rv);
    if (NS_FAILED(rv)) return rv;

    return serv->NewURI(dummyURIStr.GetBuffer(), nsnull, _retval);
}

nsresult
NS_NewMultiMixedConv(nsMultiMixedConv** aMultiMixedConv)
{
    NS_PRECONDITION(aMultiMixedConv != nsnull, "null ptr");
    if (! aMultiMixedConv)
        return NS_ERROR_NULL_POINTER;

    *aMultiMixedConv = new nsMultiMixedConv();
    if (! *aMultiMixedConv)
        return NS_ERROR_OUT_OF_MEMORY;

    NS_ADDREF(*aMultiMixedConv);
    return (*aMultiMixedConv)->Init();
}
