/*==LICENSE==*

CyanWorlds.com Engine - MMOG client, server and tools
Copyright (C) 2011  Cyan Worlds, Inc.

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.

You can contact Cyan Worlds, Inc. by email legal@cyan.com
 or by snail mail at:
      Cyan Worlds, Inc.
      14617 N Newport Hwy
      Mead, WA   99021

*==LICENSE==*/
//////////////////////////////////////////////////////////////////////////////
//                                                                          //
//  plStatusLog Functions                                                   //
//                                                                          //
//// History /////////////////////////////////////////////////////////////////
//                                                                          //
//  10.02.2001 mcn  - Created                                               //
//  10.16.2001 mcn  - Added static versions of AddLine() so you don't need  //
//                    to explicitly create a log                            //
//  10.17.2001 mcn  - Added support for carriage returns in printed lines   //
//  10.24.2002 eap  - Added kDebugOutput flag for writing to debug window   //
//  10.25.2002 eap  - Updated to work under unix                            //
//  12.13.2002 eap  - Added kStdout flag                                    //
//                                                                          //
//////////////////////////////////////////////////////////////////////////////

#include <stdarg.h>
#include <stdlib.h>
#include "hsThread.h"
#include "hsTemplates.h"
#include "hsTimer.h"
#include "hsStlUtils.h"
#include "plFile/plFileUtils.h"
#include "plStatusLog.h"
#include "plPipeline.h"
#include "plPipeline/plDebugText.h"
#include "hsStlUtils.h"
#include "plFile/hsFiles.h"
#include "plUnifiedTime/plUnifiedTime.h"
#include "pnNetCommon/plNetApp.h"
#include "pnUtils/pnUtils.h"
#include "pnProduct/pnProduct.h"

#include "plEncryptLogLine.h"

#if HS_BUILD_FOR_UNIX
    #include <limits.h>
    #define MAX_PATH PATH_MAX
#endif

//////////////////////////////////////////////////////////////////////////////
//// plStatusLogMgr Stuff ////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////

wchar       plStatusLogMgr::fBasePath[ MAX_PATH ] = L"";

//// Constructor & Destructor ////////////////////////////////////////////////

plStatusLogMgr::plStatusLogMgr()
{
    fDisplays = nil;
    fCurrDisplay = nil;
    fDrawer = nil;
    fLastLogChangeTime = 0;

    PathGetLogDirectory(fBasePath, arrsize(fBasePath));
}

plStatusLogMgr::~plStatusLogMgr()
{
    // Unlink all the displays, but don't delete them; leave that to whomever owns them
    while( fDisplays != nil )
    {
        plStatusLog *log = fDisplays;

        fDisplays->IUnlink();

        if( log->fFlags & plStatusLog::kDeleteForMe )
            DEL(log);
    }
}

plStatusLogMgr  &plStatusLogMgr::GetInstance( void )
{
    static plStatusLogMgr   theManager;
    return theManager;
}

//// IEnsurePathExists ///////////////////////////////////////////////////////

void    plStatusLogMgr::IEnsurePathExists( const wchar *dirName )
{
    // Note: this creates the directory if it doesn't exist, or if it does,
    // returns false
    plFileUtils::CreateDir( dirName );
}

//// IPathAppend /////////////////////////////////////////////////////////////

void    plStatusLogMgr::IPathAppend( wchar *base, const wchar *extra, unsigned maxLen )
{
    if (!base || !extra)
        return;

    unsigned baseLen = wcslen(base);
    unsigned extraLen = wcslen(extra);

    bool needsSeparator = false;
    if (baseLen >= 1)
        needsSeparator = (base[baseLen - 1] != WPATH_SEPARATOR);

    if (needsSeparator)
    {
        if ((baseLen + 1 + 1) >= maxLen)
            return; // abort, buffer isn't big enough
        base[baseLen] = WPATH_SEPARATOR;
        ++baseLen;
        base[baseLen] = '\0';
    }
    
    // concat the strings, making sure not to overrun the buffer
    unsigned curExtraPos = 0;
    for (unsigned curBasePos = baseLen; curBasePos < maxLen; ++curBasePos)
    {
        base[curBasePos] = extra[curExtraPos];
        if (extra[curExtraPos] == '\0')
            break; // done
        ++curExtraPos;
    }

    // ensure we are null-terminated
    base[maxLen - 1] = '\0';
}

//// Draw ////////////////////////////////////////////////////////////////////

void    plStatusLogMgr::Draw( void )
{
    /// Just draw current plStatusLog
    if( fCurrDisplay != nil && fDrawer != nil )
    {
        plStatusLog* firstLog = nil;
        if (hsTimer::GetSysSeconds() - fLastLogChangeTime < 1)
            firstLog = fDisplays;

        fDrawer->Draw(fCurrDisplay, firstLog);
    }
}

//// CreateStatusLog /////////////////////////////////////////////////////////

plStatusLog *plStatusLogMgr::CreateStatusLog( UInt8 numDisplayLines, const char *filename, UInt32 flags )
{
    wchar* wFilename = hsStringToWString(filename);
    plStatusLog* ret = CreateStatusLog(numDisplayLines, wFilename, flags);
    delete [] wFilename;
    return ret;
}

plStatusLog *plStatusLogMgr::CreateStatusLog( UInt8 numDisplayLines, const wchar *filename, UInt32 flags )
{
    IEnsurePathExists( fBasePath );
    plStatusLog *log = NEW(plStatusLog)( numDisplayLines, filename, flags );

    // Put the new log in its alphabetical position
    plStatusLog** nextLog = &fDisplays;
    while (*nextLog)
    {
        if (wcsicmp(filename, (*nextLog)->GetFileNameW()) <= 0)
            break;
        nextLog = &(*nextLog)->fNext;
    }
    log->ILink(nextLog);

    log->fDisplayPointer = &fCurrDisplay;

    return log;
}

//// ToggleStatusLog /////////////////////////////////////////////////////////

void    plStatusLogMgr::ToggleStatusLog( plStatusLog *logToDisplay )
{
    if( fCurrDisplay == logToDisplay )
        fCurrDisplay = nil;
    else
        fCurrDisplay = logToDisplay;

    fLastLogChangeTime = hsTimer::GetSysSeconds();
}

//// SetCurrStatusLog ////////////////////////////////////////////////////////

void plStatusLogMgr::SetCurrStatusLog(const char* logName)
{
    wchar* wLogName = hsStringToWString(logName);
    SetCurrStatusLog(wLogName);
    delete [] wLogName;
}

void plStatusLogMgr::SetCurrStatusLog(const wchar* logName)
{
    plStatusLog* log = FindLog(logName, false);
    if (log != nil)
        fCurrDisplay = log;
}

//// NextStatusLog ///////////////////////////////////////////////////////////

void    plStatusLogMgr::NextStatusLog( void )
{
    if( fCurrDisplay == nil )
        fCurrDisplay = fDisplays;
    else
        fCurrDisplay = fCurrDisplay->fNext;

    fLastLogChangeTime = hsTimer::GetSysSeconds();
}

void    plStatusLogMgr::PrevStatusLog( void )
{
    if( fCurrDisplay == nil )
    {
        fCurrDisplay = fDisplays;
        while (fCurrDisplay && fCurrDisplay->fNext)
            fCurrDisplay = fCurrDisplay->fNext;
    }
    else
    {
        plStatusLog* lastLog = fDisplays;
        while (lastLog && lastLog->fNext != fCurrDisplay)
            lastLog = lastLog->fNext;

        fCurrDisplay = lastLog;
    }

    fLastLogChangeTime = hsTimer::GetSysSeconds();
}

//// FindLog ////////////////////////////////////////////////////////////////

plStatusLog *plStatusLogMgr::FindLog( const char *filename, hsBool createIfNotFound )
{
    wchar* wFilename = hsStringToWString(filename);
    plStatusLog* ret = FindLog(wFilename, createIfNotFound);
    delete [] wFilename;
    return ret;
}

plStatusLog *plStatusLogMgr::FindLog( const wchar *filename, hsBool createIfNotFound )
{
    plStatusLog *log = fDisplays;

    while( log != nil )
    {
        if( wcsicmp( log->GetFileNameW(), filename ) == 0 )
            return log;

        log = log->fNext;
    }

    if( !createIfNotFound )
        return nil;

    // Didn't find one, so create one! (make it a nice default one :)
    log = CreateStatusLog( kDefaultNumLines, filename, plStatusLog::kFilledBackground | 
                                                       plStatusLog::kDeleteForMe );

    return log;
}

//// SetBasePath ////////////////////////////////////////////////////////////////

void plStatusLogMgr::SetBasePath( const char * path )
{
    wchar* wPath = hsStringToWString(path);
    SetBasePath(wPath);
    delete [] wPath;
}

void plStatusLogMgr::SetBasePath( const wchar * path )
{
    wcscpy( fBasePath, path );
}


//// BounceLogs ///////////////////////////////////////////////////////////////

void plStatusLogMgr::BounceLogs()
{
    plStatusLog *log = fDisplays;

    while( log != nil )
    {
        plStatusLog * tmp = log;
        log = log->fNext;
        tmp->Bounce();
    }
}

//// DumpLogs ////////////////////////////////////////////////////////////////

bool plStatusLogMgr::DumpLogs( const char *newFolderName )
{
    wchar* wFolderName = hsStringToWString(newFolderName);
    bool ret = DumpLogs(wFolderName);
    delete [] wFolderName;
    return ret;
}

bool plStatusLogMgr::DumpLogs( const wchar *newFolderName )
{
    bool retVal = true; // assume success
    // create root path and make sure it exists
    wchar temp[MAX_PATH];
    std::wstring newPath = L"";
    if (fBasePath)
    {
        wcsncpy(temp, fBasePath, MAX_PATH);
        IPathAppend(temp, newFolderName, MAX_PATH);
        newPath = temp;
    }
    else
        newPath = newFolderName;
    IEnsurePathExists(newPath.c_str());

    hsWFolderIterator folderIterator;
    if (fBasePath)
        folderIterator.SetPath(fBasePath);
    else
        folderIterator.SetPath(L".");

    while (folderIterator.NextFile())
    {
        if (folderIterator.IsDirectory())
            continue;

        std::wstring baseFilename = folderIterator.GetFileName();
        std::wstring source;
        if (fBasePath)
        {
            wcsncpy(temp, fBasePath, MAX_PATH);
            IPathAppend(temp, baseFilename.c_str(), MAX_PATH);
            source = temp;
        }
        else
            source = baseFilename;
        
        std::wstring destination;
        wcsncpy(temp, newPath.c_str(), MAX_PATH);
        IPathAppend(temp, baseFilename.c_str(), MAX_PATH);
        destination = temp;

        bool succeeded = (CopyFileW(source.c_str(), destination.c_str(), FALSE) != 0);
        retVal = retVal && succeeded;
    }
    return retVal;
}

//////////////////////////////////////////////////////////////////////////////
//// plStatusLog ////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////

UInt32 plStatusLog::fLoggingOff = false;

plStatusLog::plStatusLog( UInt8 numDisplayLines, const wchar *filename, UInt32 flags )
{
    fFileHandle = nil;
    fSize = 0;
    fForceLog = false;

    fMaxNumLines = numDisplayLines;
    if( filename != nil )
    {
        fFilename = filename;
        char* temp = hsWStringToString(filename);
        fCFilename = temp;
        delete [] temp;
    }
    else
    {
        fFilename = L"";
        fCFilename = "";
        flags |= kDontWriteFile;
    }

    fOrigFlags = fFlags = flags;

    IInit();
}

plStatusLog::~plStatusLog()
{
    IFini();
}

void    plStatusLog::IInit()
{
    int i;

    fFlags = fOrigFlags;

    fLines = TRACKED_NEW char *[ fMaxNumLines ];
    fColors = TRACKED_NEW UInt32[ fMaxNumLines ];
    for( i = 0; i < fMaxNumLines; i++ )
    {
        fLines[ i ] = nil;
        fColors[ i ] = kWhite;
    }

    fNext = nil;
    fBack = nil;

}

bool plStatusLog::IReOpen( void )
{
    if( fFileHandle != nil )
    {
        fclose( fFileHandle );
        fFileHandle = nil;
    }

    // Open the file, clearing it, if necessary
    if(!(fFlags & kDontWriteFile))
    {
        wchar file[ MAX_PATH ];
        wchar fileNoExt[MAX_PATH];
        wchar* ext=nil;
        IParseFileName(file, MAX_PATH, fileNoExt, &ext);
        fEncryptMe = false;
#ifdef PLASMA_EXTERNAL_RELEASE
        fEncryptMe = ( wcsicmp( fFilename.c_str(), L"chat.log" ) != 0 ) ? true : false;
        if( fEncryptMe )
            ext = L".elf";
#endif
        wchar fileToOpen[MAX_PATH];
        swprintf(fileToOpen, MAX_PATH, L"%s.0%s", fileNoExt, ext);
        if (!(fFlags & kDontRotateLogs))
        {
            wchar work[MAX_PATH], work2[MAX_PATH];
            swprintf(work, MAX_PATH, L"%s.3%s",fileNoExt,ext);
            _wremove(work);
            swprintf(work2, MAX_PATH, L"%s.2%s",fileNoExt,ext);
            _wrename(work2, work);
            swprintf(work, MAX_PATH, L"%s.1%s",fileNoExt,ext);
            _wrename(work, work2);
            _wrename(fileToOpen, work);
        }
        
        if (fFlags & kAppendToLast)
        {
            fFileHandle = _wfopen( fileToOpen, fEncryptMe ? L"ab" : L"at" );
        }
        else
        {
            fFileHandle = _wfopen( fileToOpen, fEncryptMe ? L"wb" : L"wt" );
            // if we need to reopen lets just append
            fFlags |= kAppendToLast;
        }
    }

    if (fFileHandle)
        fSize = ftell( fFileHandle );
    else
        fSize = 0;

    return fFileHandle != nil;
}

void    plStatusLog::IFini( void )
{
    int     i;

    if( fFileHandle != nil )
    {
        fclose( fFileHandle );
        fFileHandle = nil;
    }

    if( *fDisplayPointer == this )
        *fDisplayPointer = nil;

    if( fBack != nil || fNext != nil )
        IUnlink();

    for( i = 0; i < fMaxNumLines; i++ )
        delete [] fLines[ i ];

    delete [] fLines;
    delete [] fColors;
}


void plStatusLog::IParseFileName(wchar* file, size_t fnsize, wchar* fileNoExt, wchar** ext) const
{
    const wchar *base = plStatusLogMgr::IGetBasePath();
    if( wcslen( base ) != nil )
        swprintf( file, fnsize, L"%s%s%s", base, WPATH_SEPARATOR_STR, fFilename.c_str() );
    else
        wcscpy( file, fFilename.c_str() );

    plFileUtils::EnsureFilePathExists( file );
    
    // apache-style file backup
    
    *ext = wcsrchr(file, L'.');
    if (*ext)
    {
        int fileLen = *ext - file;
        wcsncpy(fileNoExt, file, fileLen);
        fileNoExt[fileLen] = L'\0';
    }
    else
    {
        wcscpy(fileNoExt, file);
        *ext = L"";
    }
    
}

//// IUnlink /////////////////////////////////////////////////////////////////

void    plStatusLog::IUnlink( void )
{
    hsAssert( fBack, "plStatusLog not in list" );
    if( fNext )
        fNext->fBack = fBack;
    *fBack = fNext;

    fBack = nil;
    fNext = nil;
}

//// ILink ///////////////////////////////////////////////////////////////////

void    plStatusLog::ILink( plStatusLog **back )
{
    hsAssert( fNext == nil && fBack == nil, "Trying to link a plStatusLog that's already linked" );

    fNext = *back;
    if( *back )
        (*back)->fBack = &fNext;
    fBack = back;
    *back = this;
}

//// IAddLine ////////////////////////////////////////////////////////////////
//  Actually add a stinking line.

bool plStatusLog::IAddLine( const char *line, Int32 count, UInt32 color )
{
    int     i;

    if(fLoggingOff && !fForceLog)
        return true;

    /// Scroll pointers up
    hsTempMutexLock lock( fMutex );

    bool ret = true;

    if (fMaxNumLines > 0)
    {
        delete [] fLines[ 0 ];
        for( i = 0; i < fMaxNumLines - 1; i++ )
        {
            fLines[ i ] = fLines[ i + 1 ];
            fColors[ i ] = fColors[ i + 1 ];
        }
    }

    /// Add new
    if( line == nil || strlen( line ) == 0 )
    {
        if (fMaxNumLines > 0)
        {
            fColors[ i ] = 0;
            fLines[ i ] = nil;
        }
        ret = IPrintLineToFile( "", 0 );
    }
    else
    {
        if( count < 0 )
            count = strlen( line );

        if (fMaxNumLines > 0)
        {
            fLines[ i ] = TRACKED_NEW char[ count + 1 ];
            hsStrncpy( fLines[ i ], line, count + 1 );
            fLines[ i ][ count ] = 0;

            char *c = strchr( fLines[ i ], '\n' );
            if( c != nil )
            {
                *c = 0;
                count--;
            }

            fColors[ i ] = color;
        }

        ret = IPrintLineToFile( line, count );
    }

    return ret;
}

//// AddLine /////////////////////////////////////////////////////////////////

bool plStatusLog::AddLine( const char *line, UInt32 color )
{
    char    *c, *str;
    if(fLoggingOff && !fForceLog)
        return true;

    bool ret = true;

    /// Scan for carriage returns and feed each section into IAddLine()
    for( str = (char *)line; ( c = strchr( str, '\n' ) ) != nil; str = c + 1 )
    {
        // So if we got here, c points to a carriage return...
        ret = IAddLine( str, (UInt32)c - (UInt32)str, color );
    }

    /// We might have some left over
    if( strlen( str ) > 0 )
    {
        ret &= IAddLine( str, -1, color );
    }

    return ret;
}

//// AddLine printf-style Variations /////////////////////////////////////////

bool plStatusLog::AddLineV( const char *format, va_list arguments )
{
    if(fLoggingOff && !fForceLog)
        return true;
    return AddLineV( kWhite, format, arguments );
}

bool plStatusLog::AddLineV( UInt32 color, const char *format, va_list arguments )
{
    if(fLoggingOff && !fForceLog)
        return true;
    char buffer[2000];
    vsprintf(buffer, format, arguments);
    return AddLine( buffer, color );
}

bool plStatusLog::AddLineF( const char *format, ... )
{
    if(fLoggingOff && !fForceLog)
        return true;
    va_list arguments;
    va_start( arguments, format );

    return AddLineV( kWhite, format, arguments );
}

bool plStatusLog::AddLineF( UInt32 color, const char *format, ... )
{
    if(fLoggingOff && !fForceLog)
        return true;
    va_list arguments;
    va_start( arguments, format );

    return AddLineV( color, format, arguments );
}

//// AddLine Static Variations ///////////////////////////////////////////////

bool plStatusLog::AddLineS( const char *filename, const char *format, ... )
{
    plStatusLog *log = plStatusLogMgr::GetInstance().FindLog( filename );

    if(fLoggingOff && !log->fForceLog)
        return true;

    va_list arguments;
    va_start( arguments, format );

    return log->AddLineV( format, arguments );
}

bool plStatusLog::AddLineS( const char *filename, UInt32 color, const char *format, ... )
{
    plStatusLog *log = plStatusLogMgr::GetInstance().FindLog( filename );

    if(fLoggingOff && !log->fForceLog)
        return true;

    va_list arguments;
    va_start( arguments, format );

    return log->AddLineV( color, format, arguments );
}

//// Clear ///////////////////////////////////////////////////////////////////

void    plStatusLog::Clear( void )
{
    int     i;


    for( i = 0; i < fMaxNumLines; i++ )
    {
        delete [] fLines[ i ];
        fLines[ i ] = nil;
    }
}


//// Bounce //////////////////////////////////////////////////////////////////

void    plStatusLog::Bounce( UInt32 flags)
{
    if (flags)
        fOrigFlags=flags;
    Clear();
    if( fFileHandle != nil )
    {
        fclose( fFileHandle );
        fFileHandle = nil;
    }
    AddLine( "--------- Bounced Log ---------" );
}

//// IPrintLineToFile ////////////////////////////////////////////////////////

bool plStatusLog::IPrintLineToFile( const char *line, UInt32 count )
{
    if( fFlags & kDontWriteFile )
        return true;

#ifdef PLASMA_EXTERNAL_RELEASE
    UInt8 hint = 0;
    if( fFlags & kAppendToLast )
    {
        hint = (UInt8)fSize;
    }
#endif

    if (!fFileHandle)
        IReOpen();

    bool ret = ( fFileHandle!=nil );

    if( fFileHandle != nil )
    {
        char work[256];
        char buf[2000];
        buf[0] = 0;

        //build line to encrypt

        if( count != 0 )
        {
            if ( fFlags & kTimestamp )
            {
                StrPrintf(work, arrsize(work), "(%s) ", plUnifiedTime(kNow).Format("%m/%d %H:%M:%S").c_str());
                StrPack(buf, work, arrsize(buf));
            }
            if ( fFlags & kTimestampGMT )
            {
                StrPrintf(work, arrsize(work), "(%s) ", plUnifiedTime::GetCurrentTime().Format("%m/%d %H:%M:%S UTC").c_str());
                StrPack(buf, work, arrsize(buf));
            }
            if ( fFlags & kTimeInSeconds )
            {
                StrPrintf(work, arrsize(work), "(%lu) ", (unsigned long)plUnifiedTime(kNow).GetSecs());
                StrPack(buf, work, arrsize(buf));
            }
            if ( fFlags & kTimeAsDouble )
            {
                StrPrintf(work, arrsize(work), "(%f) ", plUnifiedTime(kNow).GetSecsDouble());
                StrPack(buf, work, arrsize(buf));
            }
            if (fFlags & kRawTimeStamp)
            {
                StrPrintf(work, arrsize(work), "[t=%10u] ", hsTimer::GetSeconds());
                StrPack(buf, work, arrsize(buf));
            }
            if (fFlags & kThreadID)
            {
                StrPrintf(work, arrsize(work), "[t=%u] ", hsThread::GetMyThreadId());
                StrPack(buf, work, arrsize(buf));
            }

            // find the size of the buf plus the size of the line and only pack that much
            unsigned BufAndLine = StrLen(buf)+count+1;
            if ( BufAndLine > arrsize(buf) )
                BufAndLine = arrsize(buf);
            
            StrPack(buf, line, BufAndLine );

            if(!fEncryptMe )
            {
                StrPack(buf, "\n", arrsize(buf));
            }
        }

        unsigned length = StrLen(buf);

#ifdef PLASMA_EXTERNAL_RELEASE
        // Print to a separate line, since we have to encrypt it
        if( fEncryptMe )
        {
            // Encrypt!
            plStatusEncrypt::Encrypt( (UInt8 *)buf, hint );

            // xor the line length, then write it out, then the line, no terminating character
            UInt16 encrySize = length ^ ((UInt16)fSize);

            // try the first write, if it fails reopen and try again
            int err;
            err = fputc( encrySize & 0xff, fFileHandle );
            if (err == EOF && IReOpen())
            {
                err = fputc( encrySize & 0xff, fFileHandle );
            }

            if (err != EOF)
            {
                fSize++; // inc for the last putc
                err = fputc( encrySize >> 8, fFileHandle );
                if (err != EOF)
                    fSize++; // inc for the last putc
                err = fwrite( buf, 1, length, fFileHandle );
                fSize += err;   
                
                if (!(fFlags & kNonFlushedLog))
                    fflush(fFileHandle);
            }
            else
            {
                ret = false;
            }
        }
        else
#endif
        {
            int err;
            err = fwrite(buf,1,length,fFileHandle);
            ret = ( ferror( fFileHandle )==0 );

            if ( ret )
            {
                fSize += err;
                if (!(fFlags & kNonFlushedLog))
                    fflush(fFileHandle);
            }
        }

        if ( fSize>=kMaxFileSize )
        {
            plStatusLogMgr::GetInstance().BounceLogs();
        }

    }

    if ( fFlags & kDebugOutput )
    {
#if HS_BUILD_FOR_WIN32
#ifndef PLASMA_EXTERNAL_RELEASE
        std::string str;
        xtl::format( str, "%.*s\n", count, line );
        OutputDebugString( str.c_str() );
#endif
#else
        fprintf( stderr, "%.*s\n", count, line );
#endif
    }

    if ( fFlags & kStdout )
    {
        fprintf( stdout, "%.*s\n", count, line );
    }

    return ret;
}