/* -*- Mode: C++; tab-width: 4; 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 "nsLogging.h"

#ifdef NS_ENABLE_LOGGING

#include "nsCRT.h"
#include "prthread.h"
#include "nsAutoLock.h"
#include "nsISupportsArray.h"
#include "nsIServiceManager.h"
#include "nsSpecialSystemDirectory.h"
#ifdef XP_WIN
#include <windows.h>
#endif

#undef printf
#undef fprintf

static PRMonitor* gLogMonitor = nsnull;
static nsObjectHashtable* gSettings = nsnull;

NS_IMPL_LOG_ENABLED(LogInfo)
#define PRINTF  NS_LOG_PRINTF(LogInfo)
#define FLUSH   NS_LOG_FLUSH(LogInfo)

NS_DEFINE_CID(kLoggingServiceCID, NS_LOGGINGSERVICE_CID);

////////////////////////////////////////////////////////////////////////////////
// nsLoggingService

static nsLoggingService* gLoggingService = nsnull;

nsLoggingService::nsLoggingService()
    : mLogs(16),
      mDefaultControlFlags(nsILog::DEFAULT_DISABLED |
                           nsILog::PRINT_THREAD_ID |
                           nsILog::PRINT_LOG_NAME)
{
    NS_INIT_ISUPPORTS();
}

nsLoggingService::~nsLoggingService()
{
#ifdef DEBUG_warren
    DescribeLogs(LogInfo);
#endif
}

NS_IMPL_QUERY_INTERFACE1(nsLoggingService, nsILoggingService)
NS_IMPL_ADDREF(nsLoggingService)

NS_IMETHODIMP_(nsrefcnt) 
nsLoggingService::Release(void) 
{
  NS_PRECONDITION(0 != mRefCnt, "dup release"); 
  NS_ASSERT_OWNINGTHREAD(nsLoggingService); 
  --mRefCnt; 
  NS_LOG_RELEASE(this, mRefCnt, "nsLoggingService"); 
  if (mRefCnt == 0) {
    mRefCnt = 1; /* stabilize */ 
    NS_DELETEXPCOM(this);
    
    // special action -- null out global 
    gLoggingService = nsnull;

    return 0; 
  } 
  return mRefCnt; 
} 

static void* PR_CALLBACK
levelClone(nsHashKey *aKey, void *aData, void* closure)
{
    PRUint32 level = (PRUint32)aData;
    return (void*)level;
}

static PRBool PR_CALLBACK
levelDestroy(nsHashKey *aKey, void *aData, void* closure)
{
    return PR_TRUE;
}

static void
RecordSetting(const char* name, const char* value)
{
    PRUint32 level = 2;
    if (nsCRT::strcasecmp(value, "ERROR") == 0 ||
        nsCRT::strcasecmp(value, "2") == 0) {
        level = 2;
        PRINTF("### NS_LOG: %s = ERROR\n", name);
    }
    else if (nsCRT::strcasecmp(value, "WARN") == 0 ||
             nsCRT::strcasecmp(value, "WARNING") == 0 ||
             nsCRT::strcasecmp(value, "3") == 0) {
        level = 3;
        PRINTF("### NS_LOG: %s = WARN\n", name);
    }
    else if (nsCRT::strcasecmp(value, "STDOUT") == 0 ||
             nsCRT::strcasecmp(value, "OUT") == 0 ||
             nsCRT::strcasecmp(value, "4") == 0) {
        level = 4;
        PRINTF("### NS_LOG: %s = STDOUT\n", name);
    }
    else if (nsCRT::strcasecmp(value, "DBG") == 0 ||
             nsCRT::strcasecmp(value, "DEBUG") == 0 ||
             nsCRT::strcasecmp(value, "5") == 0) {
        level = 5;
        PRINTF("### NS_LOG: %s = DBG\n", name);
    }
    else {
        PRINTF("### NS_LOG error: %s = %s (bad level)\n", name, value);
    }

    nsCStringKey key(name);
    gSettings->Put(&key, (void*)level);
}

nsresult
nsLoggingService::Init()
{
    nsresult rv;
    nsFileLogEventSink* defaultSink = nsnull;
    const char* outputPath = nsnull;

    if (gLogMonitor == nsnull) {
        gLogMonitor = PR_NewMonitor();
        if (gLogMonitor == nsnull)
            return NS_ERROR_OUT_OF_MEMORY;
    }

    if (gSettings == nsnull) {
        gSettings = new nsObjectHashtable(levelClone, nsnull,
                                          levelDestroy, nsnull,
                                          16);
        if (gSettings == nsnull) {
            rv = NS_ERROR_OUT_OF_MEMORY;
            goto error;
        }
    }

    // try the nspr log environment variables first:
    {
        const char* nspr_log_modules = getenv("NSPR_LOG_MODULES");
        if (nspr_log_modules) {
            PRINTF("### NS_LOG: using NSPR_LOG_MODULES (instead of .nslog)\n");
            char* head = nsCRT::strdup(nspr_log_modules);
            char* rest = nsCRT::strdup(nspr_log_modules);
            while (1) {
                char* name = nsCRT::strtok(rest, ":", &rest);
                if (name == nsnull) break;
                char* value = nsCRT::strtok(rest, ";", &rest);
                if (value == nsnull) break;
                RecordSetting(name, value);
            }
            nsCRT::free(head);
        }
        const char* nspr_log_file = getenv("NSPR_LOG_FILE");
        if (nspr_log_file) {
            PRINTF("### NS_LOG: using NSPR_LOG_FILE (instead of .nslog) -- logging to %s\n", 
                    nspr_log_file);
            outputPath = nspr_log_file;
        }
    }
    // then load up log description file:
    {
        nsSpecialSystemDirectory file(nsSpecialSystemDirectory::OS_CurrentProcessDirectory);
        file += ".nslog";
        const char* path = file.GetNativePathCString();
        FILE* f = ::fopen(path, "r");
        if (f != nsnull) {
            PRInt32 cnt;
            while (PR_TRUE) {
                char name[64];
                char value[64];
//                cnt = ::fscanf(f, "%64s=%64s\n", name, value);
                cnt = ::fscanf(f, "%64s\n", name);
                if (cnt <= 0) break;
                cnt = ::fscanf(f, "%64s\n", value);
                if (cnt <= 0) break;
                RecordSetting(name, value);
            }
            ::fclose(f);
        }
    }

    defaultSink = new nsFileLogEventSink();
    if (defaultSink == nsnull) {
        rv = NS_ERROR_OUT_OF_MEMORY;
        goto error;
    }

    NS_ADDREF(defaultSink);
    if (outputPath)
        rv = defaultSink->Init(outputPath);
    else
        rv = defaultSink->InitFromFILE("stderr", stderr);
    if (NS_FAILED(rv)) goto error;

    mDefaultSink = defaultSink;
    NS_RELEASE(defaultSink);

#ifdef DEBUG
    DescribeLogs(LogInfo);
#endif
    return NS_OK;

  error:
    NS_IF_RELEASE(defaultSink);
    if (gLogMonitor) {
        PR_DestroyMonitor(gLogMonitor);
        gLogMonitor = nsnull;
    }
    return rv;
}

static nsresult
EnsureLoggingService()
{
    nsresult rv;
    if (gLoggingService == nsnull) {
        gLoggingService = new nsLoggingService();
        if (gLoggingService == NULL)
            return NS_ERROR_OUT_OF_MEMORY;

        rv = gLoggingService->Init();
        if (NS_FAILED(rv)) {
            delete gLoggingService;
            gLoggingService = nsnull;
            return rv; 
        }
        // Note that there's no AddRef here. That's because when the service manager
        // gets around to calling Create (below) sometime later, we'll AddRef it then
        // and the service manager will be the sole owner. This allows us to use it
        // before xpcom has started up.
    }
    return NS_OK;
}

NS_METHOD
nsLoggingService::Create(nsISupports* outer, const nsIID& aIID, void* *aInstancePtr)
{
    nsresult rv;
    if (outer)
        return NS_ERROR_NO_AGGREGATION;

    if (gLoggingService == nsnull) {
        rv = EnsureLoggingService();
        if (NS_FAILED(rv)) return rv;
    }

    return gLoggingService->QueryInterface(aIID, aInstancePtr);
}

NS_IMETHODIMP
nsLoggingService::GetLog(const char* name, nsILog* *result)
{
    nsAutoMonitor monitor(gLogMonitor);
    
    nsCStringKey key(name);
    nsILog* log = (nsILog*)mLogs.Get(&key);
    if (log) {
        *result = log;
        return NS_OK;
    }

    nsLog* newLog = new nsLog();
    if (newLog == nsnull)
        return NS_ERROR_OUT_OF_MEMORY;
    nsresult rv = newLog->Init(name, mDefaultControlFlags, mDefaultSink);
    if (NS_FAILED(rv)) {
        delete newLog;
        return rv;
    }
    NS_ADDREF(newLog);
    mLogs.Put(&key, newLog);
    *result = newLog;
    return NS_OK;
}

PR_IMPLEMENT(nsILog*)
NS_GetLog(const char* name, PRUint32 controlFlags)
{
    nsresult rv;

    if (gLoggingService == nsnull) {
        rv = EnsureLoggingService();
        if (NS_FAILED(rv)) return nsnull;
    }
    
    nsILog* log;
    rv = gLoggingService->GetLog(name, &log);
    if (NS_FAILED(rv)) return nsnull;

    // add in additional flags:
    PRUint32 flags;
    log->GetControlFlags(&flags);
    flags |= controlFlags;
    log->SetControlFlags(flags);

    return log;
}

static PRBool PR_CALLBACK
DescribeLog(nsHashKey *aKey, void *aData, void* closure)
{
    nsILog* log = (nsILog*)aData;
    nsILog* out = (nsILog*)closure;
    (void)log->Describe(out);
    return PR_TRUE;
}

NS_IMETHODIMP
nsLoggingService::DescribeLogs(nsILog* out)
{
    NS_LOG_PRINTF(out)("%-20.20s %-8.8s %s\n", "LOG NAME", "ENABLED", "DESTINATION");
    mLogs.Enumerate(DescribeLog, out);
    return NS_OK;
}

NS_IMETHODIMP
nsLoggingService::GetDefaultControlFlags(PRUint32 *controlFlags)
{
    *controlFlags = mDefaultControlFlags;
    return NS_OK;
}

NS_IMETHODIMP
nsLoggingService::SetDefaultControlFlags(PRUint32 controlFlags)
{
    mDefaultControlFlags = controlFlags;
    return NS_OK;
}

NS_IMETHODIMP
nsLoggingService::GetDefaultLogEventSink(nsILogEventSink* *sink)
{
    *sink = mDefaultSink;
    NS_ADDREF(*sink);
    return NS_OK;
}

NS_IMETHODIMP
nsLoggingService::SetDefaultLogEventSink(nsILogEventSink* sink)
{
    mDefaultSink = sink;
    return NS_OK;
}

////////////////////////////////////////////////////////////////////////////////
// nsLog

nsLog::nsLog()
    : mName(nsnull),
      mIndentLevel(0)
{
    NS_INIT_ISUPPORTS();
}

nsLog::~nsLog()
{
    if (mName) nsCRT::free(mName);
}

// !!! We don't use NS_IMPL_ISUPPORTS for nsLog because logs always appear to 
// leak due to the way NS_IMPL_LOG works.

NS_IMETHODIMP_(nsrefcnt)
nsLog::AddRef(void)
{
  NS_PRECONDITION(PRInt32(mRefCnt) >= 0, "illegal refcnt");
  NS_ASSERT_OWNINGTHREAD(nsLog);
  ++mRefCnt;
  return mRefCnt;
}

NS_IMETHODIMP_(nsrefcnt)
nsLog::Release(void)
{
  NS_PRECONDITION(0 != mRefCnt, "dup release");
  NS_ASSERT_OWNINGTHREAD(nsLog);
  --mRefCnt;
  if (mRefCnt == 0) {
    mRefCnt = 1; /* stabilize */
    NS_DELETEXPCOM(this);
    return 0;
  }
  return mRefCnt;
}

NS_IMPL_QUERY_INTERFACE1(nsLog, nsILog)

nsresult
nsLog::Init(const char* name, PRUint32 controlFlags, nsILogEventSink* sink)
{
    mName = nsCRT::strdup(name);
    if (mName == nsnull)
        return NS_ERROR_OUT_OF_MEMORY;
    mControlFlags = controlFlags; 
    mSink = sink;

    nsCStringKey key(name);
    PRUint32 level = (PRUint32)gSettings->Get(&key);
    if (level != 0) {
        mControlFlags |= nsILog::DEFAULT_ENABLED;
    }
    return NS_OK;
}

NS_IMETHODIMP
nsLog::GetName(char* *aName)
{
    *aName = nsCRT::strdup(mName);
    return *aName ? NS_OK : NS_ERROR_OUT_OF_MEMORY;
}

NS_IMETHODIMP
nsLog::Enabled(PRBool *result)
{
    *result = Test();
    return NS_OK;
}

NS_IMETHODIMP
nsLog::Print(const PRUnichar *message)
{
    nsAutoMonitor monitor(gLogMonitor);

    nsCString str; str.AssignWithConversion(message);
    nsresult rv = Printf(str.GetBuffer());
    return rv;
}

NS_IMETHODIMP
nsLog::Flush(void)
{
    nsAutoMonitor monitor(gLogMonitor);
    
    return mSink->Flush(this);
}

NS_IMETHODIMP
nsLog::IncreaseIndent()
{
    nsAutoMonitor monitor(gLogMonitor);
    
    mIndentLevel++;
    return NS_OK;
}

NS_IMETHODIMP
nsLog::DecreaseIndent()
{
    nsAutoMonitor monitor(gLogMonitor);
    
    PR_ASSERT(mIndentLevel > 0);
    if (mIndentLevel == 0)
        return NS_ERROR_FAILURE;
    mIndentLevel--;
    return NS_OK;
}

NS_IMETHODIMP
nsLog::GetIndentLevel(PRUint32 *aIndentLevel)
{
    *aIndentLevel = mIndentLevel;
    return NS_OK;
}

NS_IMETHODIMP
nsLog::Describe(nsILog* out)
{
    nsresult rv;
#if 0
    const char* levelName;
    switch (mEnabledLevel) {
      case nsILog::LEVEL_NEVER:  levelName = "NEVER"; break;
      case nsILog::LEVEL_ERROR:  levelName = "ERROR"; break;
      case nsILog::LEVEL_WARN:   levelName = "WARN"; break;
      case nsILog::LEVEL_STDOUT: levelName = "STDOUT"; break;
      case nsILog::LEVEL_DBG:    levelName = "DBG"; break;
      default:                   levelName = "<unknown>"; break;
    }
#endif
    char* dest = nsnull;
    rv = mSink->GetDestinationName(&dest);
    if (NS_FAILED(rv)) {
        dest = nsCRT::strdup("<unknown>");
    }
//    NS_LOG(out, ("  %-20.20s %-8.8s %s\n", mName, levelName, dest));
    PRBool enabled = mControlFlags & nsILog::DEFAULT_ENABLED;
    NS_LOG_PRINTF(out)("%-20.20s %-8.8s %s\n", 
                       mName, (enabled ? "yes" : "no"), dest);
    if (dest) nsCRT::free(dest);
    return NS_OK;
}

NS_IMETHODIMP
nsLog::GetControlFlags(PRUint32 *flags)
{
    nsAutoMonitor monitor(gLogMonitor);
    
    *flags = mControlFlags;
    return NS_OK;
}

NS_IMETHODIMP
nsLog::SetControlFlags(PRUint32 flags)
{
    nsAutoMonitor monitor(gLogMonitor);
    
    mControlFlags = flags;
    return NS_OK;
}

NS_IMETHODIMP
nsLog::GetLogEventSink(nsILogEventSink* *sink)
{
    nsAutoMonitor monitor(gLogMonitor);
    
    *sink = mSink;
    NS_ADDREF(*sink);
    return NS_OK;
}

NS_IMETHODIMP
nsLog::SetLogEventSink(nsILogEventSink* sink)
{
    nsAutoMonitor monitor(gLogMonitor);
    
    mSink = sink;
    return NS_OK;
}

////////////////////////////////////////////////////////////////////////////////
// nsLog

NS_IMETHODIMP
nsLog::Printf(const char* format, ...)
{
    nsAutoMonitor monitor(gLogMonitor);
    
    va_list args;
    va_start(args, format);
    nsresult rv = Vprintf(format, args);
    va_end(args);
    return rv;
}

NS_IMETHODIMP
nsLog::Vprintf(const char* format, va_list args)
{
    nsAutoMonitor monitor(gLogMonitor);
    
    char* msg = PR_vsmprintf(format, args);
    if (!msg) return NS_ERROR_OUT_OF_MEMORY;
    nsresult rv = mSink->Print(this, msg);
    PR_smprintf_free(msg);
    return rv;
}

////////////////////////////////////////////////////////////////////////////////
// nsLogIndent

nsLogIndent::nsLogIndent(nsILog* log, const char* msg)
    : mLog(log), mHeaderMsg(msg)
{
    if (mHeaderMsg) mLog->Printf("[ Begin %s", mHeaderMsg);
    (void)mLog->IncreaseIndent();
}

nsLogIndent::~nsLogIndent()
{
    (void)mLog->DecreaseIndent();
    if (mHeaderMsg) mLog->Printf("] End   %s", mHeaderMsg);
}

////////////////////////////////////////////////////////////////////////////////
// nsFileLogEventSink 

nsFileLogEventSink::nsFileLogEventSink()
    : mName(nsnull),
      mOutput(nsnull),
      mBeginningOfLine(PR_TRUE),
      mCloseFile(PR_FALSE)
{
    NS_INIT_REFCNT();
}

nsFileLogEventSink::~nsFileLogEventSink()
{
    if (mCloseFile) {
        ::fclose(mOutput);
        mOutput = nsnull;
    }
    if (mName) nsCRT::free(mName);
}

NS_IMPL_ISUPPORTS2(nsFileLogEventSink, 
                   nsIFileLogEventSink,
                   nsILogEventSink)

NS_IMETHODIMP
nsFileLogEventSink::GetDestinationName(char* *result)
{
    *result = nsCRT::strdup(mName);
    return *result ? NS_OK : NS_ERROR_OUT_OF_MEMORY;
}

NS_IMETHODIMP
nsFileLogEventSink::Init(const char* filePath)
{
    FILE* filePtr;
    if (nsCRT::strcmp(filePath, "1") == 0) {
        filePtr = stdout;
    }
    else if (nsCRT::strcmp(filePath, "2") == 0) {
        filePtr = stderr;
    }
    else {
        filePtr = ::fopen(filePath, "w");
        if (filePtr == nsnull)
            return NS_ERROR_FAILURE;
        mCloseFile = PR_TRUE;
    }
    return InitFromFILE(filePath, filePtr);
}

NS_IMETHODIMP
nsFileLogEventSink::InitFromFILE(const char* name, FILE* filePtr)
{
    mOutput = filePtr;
    mName = nsCRT::strdup(name);
    return mName ? NS_OK : NS_ERROR_OUT_OF_MEMORY;
}

NS_IMETHODIMP
nsFileLogEventSink::Print(nsILog* log, const char* msg)
{
    nsresult rv;

    if (!NS_LOG_ENABLED(log))
        return NS_OK;

    nsAutoMonitor monitor(gLogMonitor);
    
    // do debug output first
#ifdef XP_WIN
    OutputDebugString(msg);
#elif defined(XP_MAC)
    {
#       define BUF_SIZE 1024
        char buf[BUF_SIZE];
        PRUint32 len =
            PR_snprintf(buf+1, BUF_SIZE-1, "%s", msg);
        buf[0] = (char) (len > 255 ? 255 : len);
        DebugStr(StringPtr(buf));
    }
#endif

    if (!mBeginningOfLine) {
        ::fputc('\n', mOutput);
        mBeginningOfLine = PR_TRUE;
    }

    // print preamble
    PRUint32 flags;
    rv = log->GetControlFlags(&flags);
    if (NS_FAILED(rv)) return rv;

    if (flags & nsILog::PRINT_THREAD_ID) {
        ::fprintf(mOutput, "%8x ", (PRInt32)PR_CurrentThread());
        mBeginningOfLine = PR_FALSE;
    }

    if (flags & nsILog::PRINT_LOG_NAME) {
        char* name;
        rv = log->GetName(&name);
        ::fprintf(mOutput, "%-8.8s ",
                  flags & nsILog::PRINT_LOG_NAME ? name : "");
        nsCRT::free(name);
        mBeginningOfLine = PR_FALSE;
    }

    PRUint32 indentLevel;
    rv = log->GetIndentLevel(&indentLevel);
    if (NS_FAILED(rv)) return rv;
    do {
      indent:
        // do indentation
        for (PRUint32 i = 0; i < indentLevel; i++) {
            ::fprintf(mOutput, "|  ");
            mBeginningOfLine = PR_FALSE;
        }

        char c;
        while ((c = *msg++)) {
            switch (c) {
              case '\n':
                if (*msg == '\0') {
                    ::fputc('\n', mOutput);
                    mBeginningOfLine = PR_TRUE;
                    break;
                }
                else {
                    ::fprintf(mOutput, "\n                    ");
                    mBeginningOfLine = PR_FALSE;
                    goto indent;
                }
              default:
                ::fputc(c, mOutput);
                mBeginningOfLine = PR_FALSE;
            }
        }
    } while (0);

    if (!mBeginningOfLine) {
        ::fputc('\n', mOutput);
        mBeginningOfLine = PR_TRUE;
    }
    return NS_OK;
}

NS_IMETHODIMP
nsFileLogEventSink::Flush(nsILog* log)
{
    nsAutoMonitor monitor(gLogMonitor);
    
    ::fflush(mOutput);
    return NS_OK;
}

////////////////////////////////////////////////////////////////////////////////

#else

PR_IMPLEMENT(nsILog*)
NS_GetLog(const char* name, PRUint32 controlFlags)
{
    return nsnull;
}

#endif // NS_ENABLE_LOGGING
