/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* ***** BEGIN LICENSE BLOCK *****
 * Version: NPL 1.1/GPL 2.0/LGPL 2.1
 *
 * 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 Communicator client 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):
 *   Joe Hewitt <hewitt@netscape.com> (Original Author)
 *
 * 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 NPL, 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 NPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */

#include "nsFormHistory.h"

#include "nsIServiceManager.h"
#include "nsIObserverService.h"
#include "nsICategoryManager.h"
#include "nsIDirectoryService.h"
#include "nsAppDirectoryServiceDefs.h"
#include "nsMorkCID.h"
#include "nsIMdbFactoryFactory.h"
#include "nsQuickSort.h"
#include "nsCRT.h"
#include "nsString.h"
#include "nsUnicharUtils.h"
#include "nsReadableUtils.h"
#include "nsIContent.h"
#include "nsIDOMNode.h"
#include "nsIDOMHTMLFormElement.h"
#include "nsIDOMHTMLInputElement.h"
#include "nsIDOMHTMLCollection.h"
#include "nsIPrefService.h"
#include "nsIPrefBranch.h"
#include "nsIPrefBranchInternal.h"
#include "nsVoidArray.h"
#include "nsCOMArray.h"

#define PREF_FORMFILL_BRANCH "browser.formfill."
#define PREF_FORMFILL_ENABLE "enable"

static const char *kFormHistoryFileName = "formhistory.dat";

NS_INTERFACE_MAP_BEGIN(nsFormHistory)
  NS_INTERFACE_MAP_ENTRY(nsIFormHistory)
  NS_INTERFACE_MAP_ENTRY(nsIObserver)
  NS_INTERFACE_MAP_ENTRY(nsIFormSubmitObserver)
  NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
  NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIObserver)
NS_INTERFACE_MAP_END_THREADSAFE

NS_IMPL_THREADSAFE_ADDREF(nsFormHistory)
NS_IMPL_THREADSAFE_RELEASE(nsFormHistory)

mdb_column nsFormHistory::kToken_ValueColumn = 0;
mdb_column nsFormHistory::kToken_NameColumn = 0;

PRBool nsFormHistory::gFormHistoryEnabled = PR_FALSE;
PRBool nsFormHistory::gPrefsInitialized = PR_FALSE;

nsFormHistory::nsFormHistory() :
  mEnv(nsnull),
  mStore(nsnull),
  mTable(nsnull)
{
}

nsFormHistory::~nsFormHistory()
{
  CloseDatabase();
}

nsresult
nsFormHistory::Init()
{
  gFormHistory = this;

  nsCOMPtr<nsIObserverService> service = do_GetService("@mozilla.org/observer-service;1");
  if (service)
    service->AddObserver(this, NS_FORMSUBMIT_SUBJECT, PR_TRUE);
  
  return NS_OK;
}

nsFormHistory *nsFormHistory::gFormHistory = nsnull;

nsFormHistory *
nsFormHistory::GetInstance()
{
  if (!gFormHistory) {
    gFormHistory = new nsFormHistory();
    if (!gFormHistory)
      return nsnull;

    NS_ADDREF(gFormHistory);  // addref for the global

    if (NS_FAILED(gFormHistory->Init())) {
      NS_RELEASE(gFormHistory);

      return nsnull;
    }  
  }

  NS_ADDREF(gFormHistory);   // addref for the getter

  return gFormHistory;
}


void
nsFormHistory::ReleaseInstance()
{
  NS_IF_RELEASE(gFormHistory);
}

/* static */ PRBool
nsFormHistory::FormHistoryEnabled()
{
  if (!gPrefsInitialized) {
    nsCOMPtr<nsIPrefService> prefService = do_GetService(NS_PREFSERVICE_CONTRACTID);

    prefService->GetBranch(PREF_FORMFILL_BRANCH,
                           getter_AddRefs(gFormHistory->mPrefBranch));
    gFormHistory->mPrefBranch->GetBoolPref(PREF_FORMFILL_ENABLE,
                                           &gFormHistoryEnabled);

    nsCOMPtr<nsIPrefBranchInternal> branchInternal =
      do_QueryInterface(gFormHistory->mPrefBranch);
    branchInternal->AddObserver(PREF_FORMFILL_ENABLE, gFormHistory, PR_TRUE);

    gPrefsInitialized = PR_TRUE;
  }

  return gFormHistoryEnabled;
}


////////////////////////////////////////////////////////////////////////
//// nsIFormHistory

NS_IMETHODIMP
nsFormHistory::GetRowCount(PRUint32 *aRowCount)
{
  nsresult rv = OpenDatabase(); // lazily ensure that the database is open
  NS_ENSURE_SUCCESS(rv, rv);

  mdb_err err = mTable->GetCount(mEnv, aRowCount);
  NS_ENSURE_TRUE(!err, NS_ERROR_FAILURE);
  
  return NS_OK;
}

NS_IMETHODIMP
nsFormHistory::GetEntryAt(PRUint32 aIndex, nsAString &aName, nsAString &aValue)
{
  nsresult rv = OpenDatabase(); // lazily ensure that the database is open
  NS_ENSURE_SUCCESS(rv, rv);

  nsCOMPtr<nsIMdbRow> row;
  mdb_err err = mTable->PosToRow(mEnv, aIndex, getter_AddRefs(row));
  NS_ENSURE_TRUE(!err, NS_ERROR_FAILURE);
  
  GetRowValue(row, kToken_NameColumn, aName);
  GetRowValue(row, kToken_ValueColumn, aValue);
    
  return NS_OK;
}

NS_IMETHODIMP
nsFormHistory::GetNameAt(PRUint32 aIndex, nsAString &aName)
{
  nsresult rv = OpenDatabase(); // lazily ensure that the database is open
  NS_ENSURE_SUCCESS(rv, rv);

  nsCOMPtr<nsIMdbRow> row;
  mdb_err err = mTable->PosToRow(mEnv, aIndex, getter_AddRefs(row));
  NS_ENSURE_TRUE(!err, NS_ERROR_FAILURE);
  
  GetRowValue(row, kToken_NameColumn, aName);
    
  return NS_OK;
}

NS_IMETHODIMP
nsFormHistory::GetValueAt(PRUint32 aIndex, nsAString &aValue)
{
  nsresult rv = OpenDatabase(); // lazily ensure that the database is open
  NS_ENSURE_SUCCESS(rv, rv);

  nsCOMPtr<nsIMdbRow> row;
  mdb_err err = mTable->PosToRow(mEnv, aIndex, getter_AddRefs(row));
  NS_ENSURE_TRUE(!err, NS_ERROR_FAILURE);
  
  GetRowValue(row, kToken_ValueColumn, aValue);
    
  return NS_OK;
}

NS_IMETHODIMP
nsFormHistory::AddEntry(const nsAString &aName, const nsAString &aValue)
{
  if (!FormHistoryEnabled())
    return NS_OK;

  nsresult rv = OpenDatabase(); // lazily ensure that the database is open
  NS_ENSURE_SUCCESS(rv, rv);

  nsCOMPtr<nsIMdbRow> row;
  AppendRow(aName, aValue, getter_AddRefs(row));
  return NS_OK;
}

NS_IMETHODIMP
nsFormHistory::RemoveEntryAt(PRUint32 index)
{
  return NS_ERROR_NOT_IMPLEMENTED;
}

NS_IMETHODIMP
nsFormHistory::EntryExists(const nsAString &aName, const nsAString &aValue, PRBool *_retval)
{
  return EntriesExistInternal(&aName, &aValue, _retval);
}

NS_IMETHODIMP
nsFormHistory::NameExists(const nsAString &aName, PRBool *_retval)
{
  return EntriesExistInternal(&aName, nsnull, _retval);
}

NS_IMETHODIMP
nsFormHistory::RemoveEntriesForName(const nsAString &aName)
{
  return RemoveEntriesInternal(&aName);
}

NS_IMETHODIMP
nsFormHistory::RemoveAllEntries()
{
  nsresult rv = RemoveEntriesInternal(nsnull);
  
  rv |= Flush();
  
  return rv;
}

////////////////////////////////////////////////////////////////////////
//// nsIObserver

NS_IMETHODIMP
nsFormHistory::Observe(nsISupports *aSubject, const char *aTopic, const PRUnichar *aData) 
{
  if (!strcmp(aTopic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID)) {
    mPrefBranch->GetBoolPref(PREF_FORMFILL_ENABLE, &gFormHistoryEnabled);
  }

  return NS_OK;
}

////////////////////////////////////////////////////////////////////////
//// nsIFormSubmitObserver

NS_IMETHODIMP
nsFormHistory::Notify(nsIContent* aFormNode, nsIDOMWindowInternal* aWindow, nsIURI* aActionURL, PRBool* aCancelSubmit)
{
  if (!FormHistoryEnabled())
    return NS_OK;

  nsresult rv = OpenDatabase(); // lazily ensure that the database is open
  NS_ENSURE_SUCCESS(rv, rv);
  
  nsCOMPtr<nsIDOMHTMLFormElement> formElt = do_QueryInterface(aFormNode);
  NS_ENSURE_TRUE(formElt, NS_ERROR_FAILURE);

  nsCOMPtr<nsIDOMHTMLCollection> elts;
  formElt->GetElements(getter_AddRefs(elts));
  
  const char *textString = "text";
  
  PRUint32 length;
  elts->GetLength(&length);
  for (PRUint32 i = 0; i < length; ++i) {
    nsCOMPtr<nsIDOMNode> node;
    elts->Item(i, getter_AddRefs(node));
    nsCOMPtr<nsIDOMHTMLInputElement> inputElt = do_QueryInterface(node);
    if (inputElt) {
      // Filter only inputs that are of type "text"
      nsAutoString type;
      inputElt->GetType(type);
      if (type.EqualsIgnoreCase(textString)) {
        // If this input has a name/id and value, add it to the database
        nsAutoString value;
        inputElt->GetValue(value);
        if (!value.IsEmpty()) {
          nsAutoString name;
          inputElt->GetName(name);
          if (name.IsEmpty())
            inputElt->GetId(name);
          
          if (!name.IsEmpty())
            AppendRow(name, value, nsnull);
        }
      }
    }
  }

  return NS_OK;
}

////////////////////////////////////////////////////////////////////////
//// Database I/O

class SatchelErrorHook : public nsIMdbErrorHook
{
public:
  NS_DECL_ISUPPORTS

  // nsIMdbErrorHook
  NS_IMETHOD OnErrorString(nsIMdbEnv* ev, const char* inAscii);
  NS_IMETHOD OnErrorYarn(nsIMdbEnv* ev, const mdbYarn* inYarn);
  NS_IMETHOD OnWarningString(nsIMdbEnv* ev, const char* inAscii);
  NS_IMETHOD OnWarningYarn(nsIMdbEnv* ev, const mdbYarn* inYarn);
  NS_IMETHOD OnAbortHintString(nsIMdbEnv* ev, const char* inAscii);
  NS_IMETHOD OnAbortHintYarn(nsIMdbEnv* ev, const mdbYarn* inYarn);
};

// nsIMdbErrorHook has no IID!
NS_IMPL_ISUPPORTS0(SatchelErrorHook)

NS_IMETHODIMP
SatchelErrorHook::OnErrorString(nsIMdbEnv *ev, const char *inAscii)
{
  printf("mork error: %s\n", inAscii);
  return NS_OK;
}

NS_IMETHODIMP
SatchelErrorHook::OnErrorYarn(nsIMdbEnv *ev, const mdbYarn* inYarn)
{
  printf("mork error yarn: %p\n", inYarn);
  return NS_OK;
}

NS_IMETHODIMP
SatchelErrorHook::OnWarningString(nsIMdbEnv *ev, const char *inAscii)
{
  printf("mork warning: %s\n", inAscii);
  return NS_OK;
}

NS_IMETHODIMP
SatchelErrorHook::OnWarningYarn(nsIMdbEnv *ev, const mdbYarn *inYarn)
{
  printf("mork warning yarn: %p\n", inYarn);
  return NS_OK;
}

NS_IMETHODIMP
SatchelErrorHook::OnAbortHintString(nsIMdbEnv *ev, const char *inAscii)
{
  printf("mork abort: %s\n", inAscii);
  return NS_OK;
}

NS_IMETHODIMP
SatchelErrorHook::OnAbortHintYarn(nsIMdbEnv *ev, const mdbYarn *inYarn)
{
  printf("mork abort yarn: %p\n", inYarn);
  return NS_OK;
}

nsresult
nsFormHistory::OpenDatabase()
{
  if (mStore)
    return NS_OK;
  
  // Get a handle to the database file
  nsCOMPtr <nsIFile> historyFile;
  nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, getter_AddRefs(historyFile));
  NS_ENSURE_SUCCESS(rv, rv);
  historyFile->Append(NS_ConvertUTF8toUCS2(kFormHistoryFileName));

  // Get an Mdb Factory
  static NS_DEFINE_CID(kMorkCID, NS_MORK_CID);
  nsCOMPtr<nsIMdbFactoryFactory> mdbFactory;
  rv = nsComponentManager::CreateInstance(kMorkCID, nsnull, NS_GET_IID(nsIMdbFactoryFactory), getter_AddRefs(mdbFactory));
  NS_ENSURE_SUCCESS(rv, rv);
  rv = mdbFactory->GetMdbFactory(getter_AddRefs(mMdbFactory));
  NS_ENSURE_SUCCESS(rv, rv);

  // Create the Mdb environment
  mdb_err err = mMdbFactory->MakeEnv(nsnull, &mEnv);
  NS_ASSERTION(err == 0, "ERROR: Unable to create Form History mdb");
  mEnv->SetAutoClear(PR_TRUE);
  NS_ENSURE_TRUE(!err, NS_ERROR_FAILURE);
  mEnv->SetErrorHook(new SatchelErrorHook());

  nsCAutoString filePath;
  historyFile->GetNativePath(filePath);
  PRBool exists = PR_TRUE;
  historyFile->Exists(&exists);
  
  if (!exists || NS_FAILED(rv = OpenExistingFile(filePath.get()))) {
    // If the file doesn't exist, or we fail trying to open it,
    // then make sure it is deleted and then create an empty database file
    historyFile->Remove(PR_FALSE);
    rv = CreateNewFile(filePath.get());
  }
  NS_ENSURE_SUCCESS(rv, rv);

  // Get the initial size of the file, needed later for Commit
  historyFile->GetFileSize(&mFileSizeOnDisk);

  /* // TESTING: Add a row to the database
  nsAutoString foopy;
  foopy.AssignWithConversion("foopy");
  nsAutoString oogly;
  oogly.AssignWithConversion("oogly");
  AppendRow(foopy, oogly, nsnull);
  Flush(); */
  
  /* // TESTING: Dump the contents of the database
  PRUint32 count = 0;
  mdb_err err = mTable->GetCount(mEnv, &count);
  printf("%d rows in form history\n", count);

  for (mdb_pos pos = count - 1; pos >= 0; --pos) {
    nsCOMPtr<nsIMdbRow> row;
    err = mTable->PosToRow(mEnv, pos, getter_AddRefs(row));
    
    nsAutoString name;
    GetRowValue(row, kToken_NameColumn, name);
    nsAutoString value;
    GetRowValue(row, kToken_ValueColumn, value);
    printf("ROW: %s - %s\n", ToNewCString(name), ToNewCString(value));
  } */

  return NS_OK;
}

nsresult
nsFormHistory::OpenExistingFile(const char *aPath)
{
  nsCOMPtr<nsIMdbFile> oldFile;
  nsIMdbHeap* dbHeap = 0;
  mdb_err err = mMdbFactory->OpenOldFile(mEnv, dbHeap, aPath, mdbBool_kFalse, getter_AddRefs(oldFile));
  NS_ENSURE_TRUE(!err && oldFile, NS_ERROR_FAILURE);

  mdb_bool canOpen = 0;
  mdbYarn outFormat = {nsnull, 0, 0, 0, 0, nsnull};
  err = mMdbFactory->CanOpenFilePort(mEnv, oldFile, &canOpen, &outFormat);
  NS_ENSURE_TRUE(!err && canOpen, NS_ERROR_FAILURE);

  nsCOMPtr<nsIMdbThumb> thumb;
  mdbOpenPolicy policy = {{0, 0}, 0, 0};
  err = mMdbFactory->OpenFileStore(mEnv, dbHeap, oldFile, &policy, getter_AddRefs(thumb));
  NS_ENSURE_TRUE(!err && thumb, NS_ERROR_FAILURE);

  PRBool done;
  mdb_err thumbErr = UseThumb(thumb, &done);

  if (err == 0 && done)
    err = mMdbFactory->ThumbToOpenStore(mEnv, thumb, &mStore);
  NS_ENSURE_TRUE(!err, NS_ERROR_FAILURE);

  nsresult rv = CreateTokens();
  NS_ENSURE_SUCCESS(rv, rv);

  mdbOid oid = {kToken_RowScope, 1};
  err = mStore->GetTable(mEnv, &oid, &mTable);
  NS_ENSURE_TRUE(!err, NS_ERROR_FAILURE);
  if (!mTable) {
    NS_WARNING("ERROR: Form history file is corrupt, now deleting it.");
    return NS_ERROR_FAILURE;
  }

  if (NS_FAILED(thumbErr))
    err = thumbErr;

  return err ? NS_ERROR_FAILURE : NS_OK;
}

nsresult
nsFormHistory::CreateNewFile(const char *aPath)
{
  nsIMdbHeap* dbHeap = 0;
  nsCOMPtr<nsIMdbFile> newFile;
  mdb_err err = mMdbFactory->CreateNewFile(mEnv, dbHeap, aPath, getter_AddRefs(newFile));
  NS_ENSURE_TRUE(!err && newFile, NS_ERROR_FAILURE);

  nsCOMPtr <nsIMdbTable> oldTable = mTable;;
  nsCOMPtr <nsIMdbStore> oldStore = mStore;
  mdbOpenPolicy policy = {{0, 0}, 0, 0};
  err = mMdbFactory->CreateNewFileStore(mEnv, dbHeap, newFile, &policy, &mStore);
  NS_ENSURE_TRUE(!err, NS_ERROR_FAILURE);
  
  nsresult rv = CreateTokens();
  NS_ENSURE_SUCCESS(rv, rv);

  // Create the one and only table in the database
  err = mStore->NewTable(mEnv, kToken_RowScope, kToken_Kind, PR_TRUE, nsnull, &mTable);
  NS_ENSURE_TRUE(!err && mTable, NS_ERROR_FAILURE);

   // oldTable will only be set if we detected a corrupt db, and are 
   // trying to restore data from it.
  if (oldTable)
    CopyRowsFromTable(oldTable);

  // Force a commit now to get it written out.
  nsCOMPtr<nsIMdbThumb> thumb;
  err = mStore->CompressCommit(mEnv, getter_AddRefs(thumb));
  NS_ENSURE_TRUE(!err, NS_ERROR_FAILURE);

  PRBool done;
  err = UseThumb(thumb, &done);

  return err || !done ? NS_ERROR_FAILURE : NS_OK;
}

nsresult
nsFormHistory::CloseDatabase()
{
  Flush();

  if (mTable)
    mTable->Release();

  if (mStore)
    mStore->Release();

  if (mEnv)
    mEnv->Release();

  mTable = nsnull;
  mEnv = nsnull;
  mStore = nsnull;

  return NS_OK;
}

nsresult
nsFormHistory::CreateTokens()
{
  mdb_err err;

  if (!mStore)
    return NS_ERROR_NOT_INITIALIZED;

  err = mStore->StringToToken(mEnv, "ns:formhistory:db:row:scope:formhistory:all", &kToken_RowScope);
  if (err != 0) return NS_ERROR_FAILURE;
  
  err = mStore->StringToToken(mEnv, "ns:formhistory:db:table:kind:formhistory", &kToken_Kind);
  if (err != 0) return NS_ERROR_FAILURE;
  
  err = mStore->StringToToken(mEnv, "Value", &kToken_ValueColumn);
  if (err != 0) return NS_ERROR_FAILURE;

  err = mStore->StringToToken(mEnv, "Name", &kToken_NameColumn);
  if (err != 0) return NS_ERROR_FAILURE;

  return NS_OK;
}

nsresult
nsFormHistory::Flush()
{
  if (!mStore || !mTable)
    return NS_OK;

  mdb_err err;

  nsCOMPtr<nsIMdbThumb> thumb;
  err = mStore->CompressCommit(mEnv, getter_AddRefs(thumb));

  if (err == 0)
    err = UseThumb(thumb, nsnull);
  
  return err ? NS_ERROR_FAILURE : NS_OK;
}

mdb_err
nsFormHistory::UseThumb(nsIMdbThumb *aThumb, PRBool *aDone)
{
  mdb_count total;
  mdb_count current;
  mdb_bool done;
  mdb_bool broken;
  mdb_err err;
  
  do {
    err = aThumb->DoMore(mEnv, &total, &current, &done, &broken);
  } while ((err == 0) && !broken && !done);
  
  if (aDone)
    *aDone = done;
  
  return err ? NS_ERROR_FAILURE : NS_OK;
}

nsresult
nsFormHistory::CopyRowsFromTable(nsIMdbTable *sourceTable)
{
  nsCOMPtr<nsIMdbTableRowCursor> rowCursor;
  mdb_err err = sourceTable->GetTableRowCursor(mEnv, -1, getter_AddRefs(rowCursor));
  NS_ENSURE_TRUE(!err, NS_ERROR_FAILURE);
  
  nsCOMPtr<nsIMdbRow> row;
  mdb_pos pos;
  do {
    rowCursor->NextRow(mEnv, getter_AddRefs(row), &pos);
    if (!row)
      break;

    mdbOid rowId;
    rowId.mOid_Scope = kToken_RowScope;
    rowId.mOid_Id = mdb_id(-1);

    nsCOMPtr<nsIMdbRow> newRow;
    mdb_err err = mTable->NewRow(mEnv, &rowId, getter_AddRefs(newRow));
    newRow->SetRow(mEnv, row);
    mTable->AddRow(mEnv, newRow);
  } while (row);
  return NS_OK;
}

nsresult
nsFormHistory::AppendRow(const nsAString &aName, const nsAString &aValue, nsIMdbRow **aResult)
{  
  if (!mTable)
    return NS_ERROR_NOT_INITIALIZED;

  PRBool exists;
  EntryExists(aName, aValue, &exists);
  if (exists)
    return NS_OK;

  mdbOid rowId;
  rowId.mOid_Scope = kToken_RowScope;
  rowId.mOid_Id = mdb_id(-1);

  nsCOMPtr<nsIMdbRow> row;
  mdb_err err = mTable->NewRow(mEnv, &rowId, getter_AddRefs(row));
  if (err != 0)
    return NS_ERROR_FAILURE;

  SetRowValue(row, kToken_NameColumn, aName);
  SetRowValue(row, kToken_ValueColumn, aValue);

  if (aResult) {
    *aResult = row;
    NS_ADDREF(*aResult);
  }
  
  return NS_OK;  
}

nsresult
nsFormHistory::SetRowValue(nsIMdbRow *aRow, mdb_column aCol, const nsAString &aValue)
{
  PRInt32 len = aValue.Length() * sizeof(PRUnichar);

  mdbYarn yarn = {(void *)ToNewUnicode(aValue), len, len, 0, 0, nsnull};
  mdb_err err = aRow->AddColumn(mEnv, aCol, &yarn);
  
  return err ? NS_ERROR_FAILURE : NS_OK;
}

nsresult
nsFormHistory::GetRowValue(nsIMdbRow *aRow, mdb_column aCol, nsAString &aValue)
{
  mdbYarn yarn;
  mdb_err err = aRow->AliasCellYarn(mEnv, aCol, &yarn);
  if (err != 0)
    return NS_ERROR_FAILURE;

  aValue.Truncate(0);
  if (!yarn.mYarn_Fill)
    return NS_OK;
  
  switch (yarn.mYarn_Form) {
    case 0: // unicode
      aValue.Assign((const PRUnichar *)yarn.mYarn_Buf, yarn.mYarn_Fill/sizeof(PRUnichar));
      break;
    default:
      return NS_ERROR_UNEXPECTED;
  }
  
  return NS_OK;
}

nsresult
nsFormHistory::AutoCompleteSearch(const nsAString &aInputName,
                                  const nsAString &aInputValue,
                                  nsIAutoCompleteMdbResult *aPrevResult,
                                  nsIAutoCompleteResult **aResult)
{
  if (!FormHistoryEnabled())
    return NS_OK;

  nsresult rv = OpenDatabase(); // lazily ensure that the database is open
  NS_ENSURE_SUCCESS(rv, rv);

  nsCOMPtr<nsIAutoCompleteMdbResult> result;
  
  if (aPrevResult) {
    result = aPrevResult;
    
    PRUint32 rowCount;
    result->GetMatchCount(&rowCount);
    
    for (PRInt32 i = rowCount-1; i >= 0; --i) {
      nsIMdbRow *row;
      result->GetRowAt(i, &row);
      if (!RowMatch(row, aInputName, aInputValue, nsnull))
        result->RemoveValueAt(i, PR_FALSE);
    }
  } else {
    result = do_CreateInstance("@mozilla.org/autocomplete/mdb-result;1");

    result->SetSearchString(aInputValue);
    result->Init(mEnv, mTable);
    result->SetTokens(kToken_ValueColumn, nsIAutoCompleteMdbResult::kUnicharType, nsnull, nsIAutoCompleteMdbResult::kUnicharType);

    // Get a cursor to iterate through all rows in the database
    nsCOMPtr<nsIMdbTableRowCursor> rowCursor;
    mdb_err err = mTable->GetTableRowCursor(mEnv, -1, getter_AddRefs(rowCursor));
    NS_ENSURE_TRUE(!err, NS_ERROR_FAILURE);
    
    // Store only the matching values
    nsAutoVoidArray matchingValues;
    nsCOMArray<nsIMdbRow> matchingRows;

    nsCOMPtr<nsIMdbRow> row;
    mdb_pos pos;
    do {
      rowCursor->NextRow(mEnv, getter_AddRefs(row), &pos);
      if (!row)
        break;

      PRUnichar *value = 0; // We will own the allocated string value
      if (RowMatch(row, aInputName, aInputValue, &value)) {
        matchingRows.AppendObject(row);
        matchingValues.AppendElement(value);
      }
    } while (row);

    // Turn auto array into flat array for quick sort, now that we
    // know how many items there are
    PRUint32 count = matchingRows.Count();

    if (count > 0) {
      PRUint32* items = new PRUint32[count];
      PRUint32 i;
      for (i = 0; i < count; ++i)
        items[i] = i;

      NS_QuickSort(items, count, sizeof(PRUint32),
                   SortComparison, &matchingValues);

      for (i = 0; i < count; ++i) {
        // Place the sorted result into the autocomplete result
        result->AddRow(matchingRows[items[i]]);

        // Free up these strings we owned.
        delete (PRUnichar *) matchingValues[i];
      }

      delete[] items;
    }

    PRUint32 matchCount;
    result->GetMatchCount(&matchCount);
    if (matchCount > 0) {
      result->SetSearchResult(nsIAutoCompleteResult::RESULT_SUCCESS);
      result->SetDefaultIndex(0);
    } else {
      result->SetSearchResult(nsIAutoCompleteResult::RESULT_NOMATCH);
      result->SetDefaultIndex(-1);
    }
  }
  
  *aResult = result;
  NS_IF_ADDREF(*aResult);

  return NS_OK;
}

int PR_CALLBACK 
nsFormHistory::SortComparison(const void *v1, const void *v2, void *closureVoid) 
{
  PRUint32 *index1 = (PRUint32 *)v1;
  PRUint32 *index2 = (PRUint32 *)v2;
  nsAutoVoidArray *array = (nsAutoVoidArray *)closureVoid;
  
  PRUnichar *s1 = (PRUnichar *)array->ElementAt(*index1);
  PRUnichar *s2 = (PRUnichar *)array->ElementAt(*index2);
  
  return nsCRT::strcmp(s1, s2);
}

PRBool
nsFormHistory::RowMatch(nsIMdbRow *aRow, const nsAString &aInputName, const nsAString &aInputValue, PRUnichar **aValue)
{
  nsAutoString name;
  GetRowValue(aRow, kToken_NameColumn, name);

  if (name.Equals(aInputName)) {
    nsAutoString value;
    GetRowValue(aRow, kToken_ValueColumn, value);
    if (Compare(Substring(value, 0, aInputValue.Length()), aInputValue, nsCaseInsensitiveStringComparator()) == 0) {
      if (aValue)
        *aValue = ToNewUnicode(value);
      return PR_TRUE;
    }
  }
  
  return PR_FALSE;
}

nsresult
nsFormHistory::EntriesExistInternal(const nsAString *aName, const nsAString *aValue, PRBool *_retval)
{
  // Unfortunately we have to do a brute force search through the database
  // because mork didn't bother to implement any indexing functionality
  
  *_retval = PR_FALSE;
  
  nsresult rv = OpenDatabase(); // lazily ensure that the database is open
  NS_ENSURE_SUCCESS(rv, rv);

  // Get a cursor to iterate through all rows in the database
  nsCOMPtr<nsIMdbTableRowCursor> rowCursor;
  mdb_err err = mTable->GetTableRowCursor(mEnv, -1, getter_AddRefs(rowCursor));
  NS_ENSURE_TRUE(!err, NS_ERROR_FAILURE);
  
  nsCOMPtr<nsIMdbRow> row;
  mdb_pos pos;
  do {
    rowCursor->NextRow(mEnv, getter_AddRefs(row), &pos);
    if (!row)
      break;

    // Check if the name and value combination match this row
    nsAutoString name;
    GetRowValue(row, kToken_NameColumn, name);

    if (Compare(name, *aName, nsCaseInsensitiveStringComparator()) == 0) {
      nsAutoString value;
      GetRowValue(row, kToken_ValueColumn, value);
      if (!aValue || Compare(value, *aValue, nsCaseInsensitiveStringComparator()) == 0) {
        *_retval = PR_TRUE;
        break;
      }
    }
  } while (1);
  
  return NS_OK;
}

nsresult
nsFormHistory::RemoveEntriesInternal(const nsAString *aName)
{
  nsresult rv = OpenDatabase(); // lazily ensure that the database is open
  NS_ENSURE_SUCCESS(rv, rv);

  if (!mTable) return NS_OK;

  mdb_err err;
  mdb_count count;
  nsAutoString name;
  err = mTable->GetCount(mEnv, &count);
  if (err != 0) return NS_ERROR_FAILURE;

  // Begin the batch.
  int marker;
  err = mTable->StartBatchChangeHint(mEnv, &marker);
  NS_ASSERTION(err == 0, "unable to start batch");
  if (err != 0) return NS_ERROR_FAILURE;

  for (mdb_pos pos = count - 1; pos >= 0; --pos) {
    nsCOMPtr<nsIMdbRow> row;
    err = mTable->PosToRow(mEnv, pos, getter_AddRefs(row));
    NS_ASSERTION(err == 0, "unable to get row");
    if (err != 0)
      break;

    NS_ASSERTION(row != nsnull, "no row");
    if (! row)
      continue;

    // Check if the name matches this row
    GetRowValue(row, kToken_NameColumn, name);
    
    if (!aName || Compare(name, *aName, nsCaseInsensitiveStringComparator()) == 0) {

      // Officially cut the row *now*, before notifying any observers:
      // that way, any re-entrant calls won't find the row.
      err = mTable->CutRow(mEnv, row);
      NS_ASSERTION(err == 0, "couldn't cut row");
      if (err != 0)
        continue;
  
      // possibly avoid leakage
      err = row->CutAllColumns(mEnv);
      NS_ASSERTION(err == 0, "couldn't cut all columns");
      // we'll notify regardless of whether we could successfully
      // CutAllColumns or not.
    }

  }
  
  // Finish the batch.
  err = mTable->EndBatchChangeHint(mEnv, &marker);
  NS_ASSERTION(err == 0, "error ending batch");

  return (err == 0) ? NS_OK : NS_ERROR_FAILURE;

}
