/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla 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/MPL/
 *
 * 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 the Initial Developer are Copyright (C) 1998
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *   Conrad Carlen <ccarlen@netscape.com>
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */

#include "nsEmbedGlobalHistory.h"
#include "nsIObserver.h"
#include "nsIObserverService.h"
#include "nsWeakReference.h"
#include "nsAppDirectoryServiceDefs.h"
#include "nsHashtable.h"
#include "nsInt64.h"
#include "prtypes.h"
#include "nsFixedSizeAllocator.h"
#include "nsVoidArray.h"
#include "nsIPrefService.h"

// Constants
static const PRInt32 kNewEntriesBetweenFlush = 10;

static const PRUint32 kDefaultExpirationIntervalDays = 7;

static const PRInt64 kMSecsPerDay = LL_INIT(0, 60 * 60 * 24 * 1000);
static const PRInt64 kOneThousand = LL_INIT(0, 1000);

#define PREF_BROWSER_HISTORY_EXPIRE_DAYS "browser.history_expire_days"

// Static Routine Prototypes
static nsresult readEntry(FILE *inStream, nsCString& url, HistoryEntry **entry);
static nsresult writeEntry(FILE *outStm, nsCStringKey *url, HistoryEntry *entry);

static PRIntn PR_CALLBACK enumWriteEntry(nsHashKey *aKey, void *aData, void* closure);
static PRIntn PR_CALLBACK enumWriteEntryIfUnwritten(nsHashKey *aKey, void *aData, void* closure);
static PRIntn PR_CALLBACK enumDeleteEntry(nsHashKey *aKey, void *aData, void* closure);

//*****************************************************************************
// HistoryEntry
//*****************************************************************************   

class HistoryEntry {
public:
                HistoryEntry() :
                  mWritten(PR_FALSE) {}
  

  void          OnVisited()
                {
                  mLastVisitTime = PR_Now(); 
                  LL_DIV(mLastVisitTime, mLastVisitTime, kOneThousand);
                }
                
  PRInt64       GetLastVisitTime()
                { return mLastVisitTime; }
  void          SetLastVisitTime(const PRInt64& aTime)
                { mLastVisitTime = aTime; }

  PRBool        GetIsWritten()
                { return mWritten; }
  void          SetIsWritten(PRBool written = PR_TRUE)
                { mWritten = PR_TRUE; }
                  
  // Memory management stuff    
  static void*  operator new(size_t size) CPP_THROW_NEW;
  static void   operator delete(void *p, size_t size);
  
  // Must be called when done with all HistoryEntry objects
  static void ReleasePool();
 
 private:
  PRInt64       mLastVisitTime; // Millisecs
  PRPackedBool  mWritten; // TRUE if ever persisted

  static nsresult InitPool();
  static nsFixedSizeAllocator *sPool;
};

nsFixedSizeAllocator *HistoryEntry::sPool;

//*****************************************************************************   

void* HistoryEntry::operator new(size_t size) CPP_THROW_NEW
{
  if (size != sizeof(HistoryEntry))
    return ::operator new(size);
  if (!sPool && NS_FAILED(InitPool()))
    return nsnull;
    
  return sPool->Alloc(size);
}

void HistoryEntry::operator delete(void *p, size_t size)
{
  if (!p)
    return;
  if (size != sizeof(HistoryEntry))
    ::operator delete(p);
  if (!sPool) {
    NS_ERROR("HistoryEntry outlived its memory pool");
    return;
  }
  sPool->Free(p, size);
}

nsresult HistoryEntry::InitPool()
{
  if (!sPool) {
    sPool = new nsFixedSizeAllocator;
    if (!sPool)
      return NS_ERROR_OUT_OF_MEMORY;

    static const size_t kBucketSizes[] =
      { sizeof(HistoryEntry) };
    static const PRInt32 kInitialPoolSize =
      NS_SIZE_IN_HEAP(sizeof(HistoryEntry)) * 256;

    nsresult rv = sPool->Init("EmbedLite HistoryEntry Pool", kBucketSizes, 1, kInitialPoolSize);
    if (NS_FAILED(rv))
      return rv;
  }
  return NS_OK;
}

void HistoryEntry::ReleasePool()
{
  delete sPool;
  sPool = nsnull;
}

//*****************************************************************************
// nsEmbedGlobalHistory - Creation/Destruction
//*****************************************************************************   

NS_IMPL_ISUPPORTS3(nsEmbedGlobalHistory, nsIGlobalHistory, nsIObserver, nsISupportsWeakReference)

nsEmbedGlobalHistory::nsEmbedGlobalHistory() :
  mDataIsLoaded(PR_FALSE), mEntriesAddedSinceFlush(0),
  mURLTable(nsnull)
{  
  LL_I2L(mExpirationInterval, kDefaultExpirationIntervalDays);
  LL_MUL(mExpirationInterval, mExpirationInterval, kMSecsPerDay);
}

nsEmbedGlobalHistory::~nsEmbedGlobalHistory()
{
  FlushData();
  delete mURLTable;
  HistoryEntry::ReleasePool();
}

NS_IMETHODIMP nsEmbedGlobalHistory::Init()
{
  mURLTable = new nsHashtable;
  NS_ENSURE_TRUE(mURLTable, NS_ERROR_OUT_OF_MEMORY);
  
  // Get Pref and convert to millisecs
  nsCOMPtr<nsIPrefBranch> prefs(do_GetService("@mozilla.org/preferences-service;1"));
  if (prefs) {
    PRInt32 expireDays;
    prefs->GetIntPref(PREF_BROWSER_HISTORY_EXPIRE_DAYS, &expireDays);
    LL_I2L(mExpirationInterval, expireDays);
    LL_MUL(mExpirationInterval, mExpirationInterval, kMSecsPerDay);
  }
  
  // register to observe profile changes
  nsCOMPtr<nsIObserverService> observerService = 
       do_GetService("@mozilla.org/observer-service;1");
  NS_ASSERTION(observerService, "failed to get observer service");
  if (observerService)
    observerService->AddObserver(this, "profile-before-change", PR_TRUE);

  return NS_OK;
}

//*****************************************************************************
// nsEmbedGlobalHistory::nsIGlobalHistory
//*****************************************************************************   

NS_IMETHODIMP nsEmbedGlobalHistory::AddPage(const char *aURL)
{
  NS_ENSURE_ARG(aURL);

  nsresult rv = LoadData();
  NS_ENSURE_SUCCESS(rv, rv);
  
  nsCStringKey asKey(aURL);
  HistoryEntry *entry = NS_STATIC_CAST(HistoryEntry *, mURLTable->Get(&asKey));
  if (!entry) {
    
    if (++mEntriesAddedSinceFlush >= kNewEntriesBetweenFlush)
      FlushData(kFlushModeAppend);

    HistoryEntry *newEntry = new HistoryEntry;
    if (!newEntry)
      return NS_ERROR_FAILURE;
    (void)mURLTable->Put(&asKey, newEntry);
    entry = newEntry;
  }
  entry->OnVisited(); 
  
  return NS_OK;
}

NS_IMETHODIMP nsEmbedGlobalHistory::IsVisited(const char *aURL, PRBool *_retval)
{
  NS_ENSURE_ARG(aURL);
  NS_ENSURE_ARG_POINTER(_retval);
  
  nsresult rv = LoadData();
  NS_ENSURE_SUCCESS(rv, rv);

  nsCStringKey asKey(aURL);
  
  *_retval = (mURLTable->Exists(&asKey));
  return NS_OK;
}

//*****************************************************************************
// nsEmbedGlobalHistory::nsIObserver
//*****************************************************************************   

NS_IMETHODIMP nsEmbedGlobalHistory::Observe(nsISupports *aSubject, const char *aTopic, const PRUnichar *aData)
{
  nsresult rv = NS_OK;
    
  if (strcmp(aTopic, "profile-before-change") == 0) {
    (void)FlushData();
    (void)ResetData();
  }
  return rv;
}

//*****************************************************************************
// nsEmbedGlobalHistory
//*****************************************************************************   

nsresult nsEmbedGlobalHistory::LoadData()
{
  if (!mDataIsLoaded) {
    
    nsresult rv;            
    PRBool exists;

    mDataIsLoaded = PR_TRUE;

    rv = GetHistoryFile();
    if (NS_FAILED(rv))
      return rv;
    rv = mHistoryFile->Exists(&exists);
    if (NS_FAILED(rv))
      return rv;
    if (!exists)
      return NS_OK;
    
    FILE *stdFile;
    rv = mHistoryFile->OpenANSIFileDesc("r", &stdFile);
    if (NS_FAILED(rv))
      return rv;
      
    nsCAutoString outString;
    HistoryEntry *newEntry;      
    while (NS_SUCCEEDED(readEntry(stdFile, outString, &newEntry))) {
      if (EntryHasExpired(newEntry)) {
        delete newEntry;
      }
      else {
        nsCStringKey asKey(outString);
        mURLTable->Put(&asKey, newEntry);
      }
    }
    
    fclose(stdFile);
  }
  return NS_OK;
}

nsresult nsEmbedGlobalHistory::FlushData(PRIntn mode)
{  
  if (mHistoryFile) {

    const char* openMode = (mode == kFlushModeAppend ? "a" : "w");
    FILE *stdFile;
    nsresult rv = mHistoryFile->OpenANSIFileDesc(openMode, &stdFile);
    if (NS_FAILED(rv)) return rv;

    // Before flushing either way, remove dead entries
    mURLTable->Enumerate(enumRemoveEntryIfExpired, this);  
    
    if (mode == kFlushModeAppend)
        mURLTable->Enumerate(enumWriteEntryIfUnwritten, stdFile);
    else
        mURLTable->Enumerate(enumWriteEntry, stdFile);
    
    mEntriesAddedSinceFlush = 0;
    fclose(stdFile);
  }
  return NS_OK; 
}

nsresult nsEmbedGlobalHistory::ResetData()
{
  mURLTable->Reset(enumDeleteEntry);
  mHistoryFile = 0;
  mDataIsLoaded = PR_FALSE;
  mEntriesAddedSinceFlush = 0;
  return NS_OK;
}

nsresult nsEmbedGlobalHistory::GetHistoryFile()
{
  nsresult rv;

  // Get the history file in our profile dir.
  // Notice we are not just getting NS_APP_HISTORY_50_FILE
  // because it is used by the "real" global history component.
  
  nsCOMPtr<nsIFile> aFile; 
  rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, getter_AddRefs(aFile));
  NS_ENSURE_SUCCESS(rv, rv);
  rv = aFile->Append(NS_LITERAL_STRING("history.txt"));
  NS_ENSURE_SUCCESS(rv, rv);
  mHistoryFile = do_QueryInterface(aFile);
  return NS_OK;
}

PRBool nsEmbedGlobalHistory::EntryHasExpired(HistoryEntry *entry)
{
  // convert "now" from microsecs to millisecs
  PRInt64 nowInMilliSecs = PR_Now(); 
  LL_DIV(nowInMilliSecs, nowInMilliSecs, kOneThousand);

  // determine when the entry would have expired
  PRInt64 expirationIntervalAgo;
  LL_SUB(expirationIntervalAgo, nowInMilliSecs, mExpirationInterval);

  PRInt64 lastVisitTime = entry->GetLastVisitTime();
  return (LL_CMP(lastVisitTime, <, expirationIntervalAgo));
}

PRIntn PR_CALLBACK nsEmbedGlobalHistory::enumRemoveEntryIfExpired(nsHashKey *aKey, void *aData, void* closure)
{
  HistoryEntry *entry = NS_STATIC_CAST(HistoryEntry*, aData);
  if (!entry)
    return PR_FALSE;
  nsEmbedGlobalHistory *history = NS_STATIC_CAST(nsEmbedGlobalHistory*, closure);
  if (!history)
    return kHashEnumerateStop;
      
  if (history->EntryHasExpired(entry)) {
    delete entry;
    return kHashEnumerateRemove;
  }
  return kHashEnumerateNext;
}


//*****************************************************************************
// Static Functions
//*****************************************************************************   

static nsresult parsePRInt64(FILE *inStm, PRInt64& outValue)
{
  int c, charsRead = 0;
  nsInt64 value = 0;

  while (PR_TRUE) {
    c = fgetc(inStm);
    if (c == EOF || !isdigit(c))
      break;
      
    ++charsRead;
    PRInt32 digit = c - '0';
    value *= nsInt64(10);
    value += nsInt64(digit);
  }
  if (!charsRead)
    return NS_ERROR_FAILURE;
  outValue = value;
  return NS_OK;
}

nsresult readEntry(FILE *inStream, nsCString& outURL, HistoryEntry **outEntry)
{
  nsresult rv;

  // Get the last visted date
  PRInt64 value;
  rv = parsePRInt64(inStream, value);
  if (NS_FAILED(rv))
    return rv;
  
  // Get the URL
  int c;
  char buf[1024];
  char *next, *end = buf + sizeof(buf);
    
  outURL.Truncate(0);
  next = buf;
  
  while (PR_TRUE) {
    c = fgetc(inStream);
    
    if (c == EOF)
      break;
    else if (c == '\n')
      break;
    else if (c == '\r') {
      c = fgetc(inStream);
      if (c != '\n')
        ungetc(c, inStream);
      break;
    }
    else {
      *next++ = c;
      if (next >= end) {
        outURL.Append(buf, next - buf);
        next = buf;
      }
    }
  }
  if (next > buf)
    outURL.Append(buf, next - buf);
    
  if (!outURL.Length() && c == EOF)
    return NS_ERROR_FAILURE;
    
  *outEntry = new HistoryEntry;
  if (!*outEntry)
    return NS_ERROR_OUT_OF_MEMORY;
  (*outEntry)->SetLastVisitTime(value);
  (*outEntry)->SetIsWritten();
    
  return NS_OK;
}

static nsresult writePRInt64(FILE *outStm, const PRInt64& inValue)
{
  nsInt64 value(inValue);
  
  if (value == nsInt64(0)) {
    fputc('0', outStm);
    return NS_OK;
  }
  
  nsCAutoString tempString;

  while (value != nsInt64(0)) {
    PRInt32 ones = PRInt32(value % nsInt64(10));
    value /= nsInt64(10);
    tempString.Insert(char('0' + ones), 0);
  }
  int result = fputs(tempString.get(), outStm);
  return (result == EOF) ? NS_ERROR_FAILURE : NS_OK;
}

nsresult writeEntry(FILE *outStm, nsCStringKey *url, HistoryEntry *entry)
{
  writePRInt64(outStm, entry->GetLastVisitTime());
  fputc(':', outStm);

  fputs(url->GetString(), outStm);
  entry->SetIsWritten();

#if defined (XP_WIN) || defined(XP_OS2)
  fputc('\r', outStm);
  fputc('\n', outStm);
#elif defined(XP_UNIX)
  fputc('\n', outStm);
#else
  fputc('\r', outStm);
#endif

  return NS_OK;
}

PRIntn PR_CALLBACK enumWriteEntry(nsHashKey *aKey, void *aData, void* closure)
{
  FILE *outStm = NS_STATIC_CAST(FILE*, closure);
  if (!outStm)
    return kHashEnumerateStop;
  nsCStringKey *stringKey = NS_STATIC_CAST(nsCStringKey*, aKey);
  if (!stringKey)
    return kHashEnumerateStop;
  HistoryEntry *entry = NS_STATIC_CAST(HistoryEntry*, aData);
  if (!entry)
    return kHashEnumerateStop;

  nsresult rv = writeEntry(outStm, stringKey, entry);
    
  return NS_SUCCEEDED(rv) ? kHashEnumerateNext : kHashEnumerateStop;
}

PRIntn PR_CALLBACK enumWriteEntryIfUnwritten(nsHashKey *aKey, void *aData, void* closure)
{
  FILE *outStm = NS_STATIC_CAST(FILE*, closure);
  if (!outStm)
    return kHashEnumerateStop;
  nsCStringKey *stringKey = NS_STATIC_CAST(nsCStringKey*, aKey);
  if (!stringKey)
    return kHashEnumerateStop;
  HistoryEntry *entry = NS_STATIC_CAST(HistoryEntry*, aData);
  if (!entry)
    return kHashEnumerateStop;

  nsresult rv = NS_OK;
  if (!entry->GetIsWritten())
    rv = writeEntry(outStm, stringKey, entry);
    
  return NS_SUCCEEDED(rv) ? kHashEnumerateNext : kHashEnumerateStop;
}

PRIntn PR_CALLBACK enumDeleteEntry(nsHashKey *aKey, void *aData, void* closure)
{
  HistoryEntry *entry = NS_STATIC_CAST(HistoryEntry*, aData);
  delete entry;
  
  return kHashEnumerateNext;
}
