/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "nsTextAddress.h"
#include "nsIAbCard.h"
#include "nsIAbDirectory.h"
#include "nsNativeCharsetUtils.h"
#include "nsIFile.h"
#include "nsIInputStream.h"
#include "nsNetUtil.h"
#include "nsMsgI18N.h"
#include "nsMsgUtils.h"
#include "nsIConverterInputStream.h"
#include "nsIUnicharLineInputStream.h"
#include "nsMsgUtils.h"

#include "ImportDebug.h"
#include "plstr.h"
#include "msgCore.h"

#define kWhitespace " \t\b\r\n"

nsTextAddress::nsTextAddress() {
  m_LFCount = 0;
  m_CRCount = 0;
}

nsTextAddress::~nsTextAddress() {}

nsresult nsTextAddress::GetUnicharLineStreamForFile(
    nsIFile* aFile, nsIInputStream* aInputStream,
    nsIUnicharLineInputStream** aStream) {
  nsAutoCString charset;
  nsresult rv = MsgDetectCharsetFromFile(aFile, charset);
  NS_ENSURE_SUCCESS(rv, rv);

  nsCOMPtr<nsIConverterInputStream> converterStream =
      do_CreateInstance("@mozilla.org/intl/converter-input-stream;1", &rv);
  if (NS_SUCCEEDED(rv)) {
    rv = converterStream->Init(
        aInputStream, charset.get(), 8192,
        nsIConverterInputStream::DEFAULT_REPLACEMENT_CHARACTER);
  }

  return CallQueryInterface(converterStream, aStream);
}

nsresult nsTextAddress::ImportAddresses(bool* pAbort, const char16_t* pName,
                                        nsIFile* pSrc,
                                        nsIAbDirectory* pDirectory,
                                        nsIImportFieldMap* fieldMap,
                                        nsString& errors, uint32_t* pProgress) {
  // Open the source file for reading, read each line and process it!
  m_directory = pDirectory;
  m_fieldMap = fieldMap;

  nsCOMPtr<nsIInputStream> inputStream;
  nsresult rv = NS_NewLocalFileInputStream(getter_AddRefs(inputStream), pSrc);
  if (NS_FAILED(rv)) {
    IMPORT_LOG0("*** Error opening address file for reading\n");
    return rv;
  }

  // Here we use this to work out the size of the file, so we can update
  // an integer as we go through the file which will update a progress
  // bar if required by the caller.
  uint64_t bytesLeft = 0;
  rv = inputStream->Available(&bytesLeft);
  if (NS_FAILED(rv)) {
    IMPORT_LOG0("*** Error checking address file for size\n");
    inputStream->Close();
    return rv;
  }

  uint64_t totalBytes = bytesLeft;
  bool skipRecord = false;

  rv = m_fieldMap->GetSkipFirstRecord(&skipRecord);
  if (NS_FAILED(rv)) {
    IMPORT_LOG0(
        "*** Error checking to see if we should skip the first record\n");
    return rv;
  }

  nsCOMPtr<nsIUnicharLineInputStream> lineStream;
  rv = GetUnicharLineStreamForFile(pSrc, inputStream,
                                   getter_AddRefs(lineStream));
  if (NS_FAILED(rv)) {
    IMPORT_LOG0("*** Error opening converter stream for importer\n");
    return rv;
  }

  bool more = true;
  nsAutoString line;

  // Skip the first record if the user has requested it.
  if (skipRecord) rv = ReadRecord(lineStream, line, &more);

  while (!(*pAbort) && more && NS_SUCCEEDED(rv)) {
    // Read the line in
    rv = ReadRecord(lineStream, line, &more);
    if (NS_SUCCEEDED(rv)) {
      // Now process it to add it to the database
      rv = ProcessLine(line, errors);

      if (NS_FAILED(rv)) {
        IMPORT_LOG0("*** Error processing text record.\n");
      }
    }
    if (NS_SUCCEEDED(rv) && pProgress) {
      // This won't be totally accurate, but its the best we can do
      // considering that lineStream won't give us how many bytes
      // are actually left.
      bytesLeft -= line.Length();
      *pProgress = std::min(totalBytes - bytesLeft, uint64_t(PR_UINT32_MAX));
    }
  }

  inputStream->Close();

  if (NS_FAILED(rv)) {
    IMPORT_LOG0(
        "*** Error reading the address book - probably incorrect ending\n");
    return NS_ERROR_FAILURE;
  }

  return NS_OK;
}

nsresult nsTextAddress::ReadRecord(nsIUnicharLineInputStream* aLineStream,
                                   nsAString& aLine, bool* aMore) {
  bool more = true;
  uint32_t numQuotes = 0;
  nsresult rv;
  nsAutoString line;

  // ensure aLine is empty
  aLine.Truncate();

  do {
    if (!more) {
      // No more, so we must have an incorrect file.
      rv = NS_ERROR_FAILURE;
    } else {
      // Read the line and append it
      rv = aLineStream->ReadLine(line, &more);
      if (NS_SUCCEEDED(rv)) {
        if (!aLine.IsEmpty()) aLine.AppendLiteral(MSG_LINEBREAK);
        aLine.Append(line);

        numQuotes += line.CountChar(char16_t('"'));
      }
    }
    // Continue whilst everything is ok, and we have an odd number of quotes.
  } while (NS_SUCCEEDED(rv) && (numQuotes % 2 != 0));

  *aMore = more;
  return rv;
}

nsresult nsTextAddress::ReadRecordNumber(nsIFile* aSrc, nsAString& aLine,
                                         int32_t rNum) {
  nsCOMPtr<nsIInputStream> inputStream;
  nsresult rv = NS_NewLocalFileInputStream(getter_AddRefs(inputStream), aSrc);
  if (NS_FAILED(rv)) {
    IMPORT_LOG0("*** Error opening address file for reading\n");
    return rv;
  }

  int32_t rIndex = 0;
  uint64_t bytesLeft = 0;

  rv = inputStream->Available(&bytesLeft);
  if (NS_FAILED(rv)) {
    IMPORT_LOG0("*** Error checking address file for eof\n");
    inputStream->Close();
    return rv;
  }

  nsCOMPtr<nsIUnicharLineInputStream> lineStream;
  rv = GetUnicharLineStreamForFile(aSrc, inputStream,
                                   getter_AddRefs(lineStream));
  if (NS_FAILED(rv)) {
    IMPORT_LOG0("*** Error opening converter stream for importer\n");
    return rv;
  }

  bool more = true;

  while (more && (rIndex <= rNum)) {
    rv = ReadRecord(lineStream, aLine, &more);
    if (NS_FAILED(rv)) {
      inputStream->Close();
      return rv;
    }
    if (rIndex == rNum) {
      inputStream->Close();
      return NS_OK;
    }

    rIndex++;
  }

  return NS_ERROR_FAILURE;
}

int32_t nsTextAddress::CountFields(const nsAString& aLine, char16_t delim) {
  int32_t pos = 0;
  int32_t maxLen = aLine.Length();
  int32_t count = 0;
  char16_t tab = char16_t('\t');
  char16_t doubleQuote = char16_t('"');

  if (delim == tab) tab = char16_t('\0');

  while (pos < maxLen) {
    while (((aLine[pos] == char16_t(' ')) || (aLine[pos] == tab)) &&
           (pos < maxLen)) {
      pos++;
    }
    if ((pos < maxLen) && (aLine[pos] == doubleQuote)) {
      pos++;
      while ((pos < maxLen) && (aLine[pos] != doubleQuote)) {
        pos++;
        if (((pos + 1) < maxLen) && (aLine[pos] == doubleQuote) &&
            (aLine[pos + 1] == doubleQuote)) {
          pos += 2;
        }
      }
      if (pos < maxLen) pos++;
    }
    while ((pos < maxLen) && (aLine[pos] != delim)) pos++;

    count++;
    pos++;
  }

  return count;
}

bool nsTextAddress::GetField(const nsAString& aLine, int32_t index,
                             nsString& field, char16_t delim) {
  bool result = false;
  int32_t pos = 0;
  int32_t maxLen = aLine.Length();
  char16_t tab = char16_t('\t');
  char16_t doubleQuote = char16_t('"');

  field.Truncate();

  if (delim == tab) tab = 0;

  while (index && (pos < maxLen)) {
    while (((aLine[pos] == char16_t(' ')) || (aLine[pos] == tab)) &&
           (pos < maxLen)) {
      pos++;
    }
    if (pos >= maxLen) break;
    if (aLine[pos] == doubleQuote) {
      do {
        pos++;
        if (((pos + 1) < maxLen) && (aLine[pos] == doubleQuote) &&
            (aLine[pos + 1] == doubleQuote)) {
          pos += 2;
        }
      } while ((pos < maxLen) && (aLine[pos] != doubleQuote));
      if (pos < maxLen) pos++;
    }
    if (pos >= maxLen) break;

    while ((pos < maxLen) && (aLine[pos] != delim)) pos++;

    if (pos >= maxLen) break;

    index--;
    pos++;
  }

  if (pos >= maxLen) return result;

  result = true;

  while ((pos < maxLen) && ((aLine[pos] == ' ') || (aLine[pos] == tab))) pos++;

  int32_t fLen = 0;
  int32_t startPos = pos;
  bool quoted = false;
  if (aLine[pos] == '"') {
    startPos++;
    fLen = -1;
    do {
      pos++;
      fLen++;
      if (((pos + 1) < maxLen) && (aLine[pos] == doubleQuote) &&
          (aLine[pos + 1] == doubleQuote)) {
        quoted = true;
        pos += 2;
        fLen += 2;
      }
    } while ((pos < maxLen) && (aLine[pos] != doubleQuote));
  } else {
    while ((pos < maxLen) && (aLine[pos] != delim)) {
      pos++;
      fLen++;
    }
  }

  if (!fLen) {
    return result;
  }

  field.Append(nsDependentSubstring(aLine, startPos, fLen));
  field.Trim(kWhitespace);

  if (quoted) {
    int32_t offset = field.Find(u"\"\"");
    while (offset != -1) {
      field.Cut(offset, 1);
      offset = field.Find(u"\"\"", offset + 1);
    }
  }

  return result;
}

nsresult nsTextAddress::DetermineDelim(nsIFile* aSrc) {
  nsCOMPtr<nsIInputStream> inputStream;
  nsresult rv = NS_NewLocalFileInputStream(getter_AddRefs(inputStream), aSrc);
  if (NS_FAILED(rv)) {
    IMPORT_LOG0("*** Error opening address file for reading\n");
    return rv;
  }

  int32_t lineCount = 0;
  int32_t tabCount = 0;
  int32_t commaCount = 0;
  int32_t tabLines = 0;
  int32_t commaLines = 0;
  nsAutoString line;
  bool more = true;

  nsCOMPtr<nsIUnicharLineInputStream> lineStream;
  rv = GetUnicharLineStreamForFile(aSrc, inputStream,
                                   getter_AddRefs(lineStream));
  if (NS_FAILED(rv)) {
    IMPORT_LOG0("*** Error opening converter stream for importer\n");
    return rv;
  }

  while (more && NS_SUCCEEDED(rv) && (lineCount < 100)) {
    rv = lineStream->ReadLine(line, &more);
    if (NS_SUCCEEDED(rv)) {
      tabCount = CountFields(line, char16_t('\t'));
      commaCount = CountFields(line, char16_t(','));
      if (tabCount > commaCount)
        tabLines++;
      else if (commaCount)
        commaLines++;
    }
    lineCount++;
  }

  rv = inputStream->Close();

  if (tabLines > commaLines)
    m_delim = char16_t('\t');
  else
    m_delim = char16_t(',');

  IMPORT_LOG2("Tab count = %d, Comma count = %d\n", tabLines, commaLines);

  return rv;
}

/*
    This is where the real work happens!
    Go through the field map and set the data in a new database row
*/
nsresult nsTextAddress::ProcessLine(const nsAString& aLine, nsString& errors) {
  if (!m_fieldMap) {
    IMPORT_LOG0("*** Error, text import needs a field map\n");
    return NS_ERROR_FAILURE;
  }

  nsresult rv;

  // Wait until we get our first non-empty field, then create a new row,
  // fill in the data, then add the row to the database.
  nsCOMPtr<nsIAbCard> newCard;
  nsAutoString fieldVal;
  int32_t fieldNum;
  int32_t numFields = 0;
  bool active;
  rv = m_fieldMap->GetMapSize(&numFields);
  for (int32_t i = 0; (i < numFields) && NS_SUCCEEDED(rv); i++) {
    active = false;
    rv = m_fieldMap->GetFieldMap(i, &fieldNum);
    if (NS_SUCCEEDED(rv)) rv = m_fieldMap->GetFieldActive(i, &active);
    if (NS_SUCCEEDED(rv) && active) {
      if (GetField(aLine, i, fieldVal, m_delim)) {
        if (!fieldVal.IsEmpty()) {
          if (!newCard) {
            newCard = do_CreateInstance(
                "@mozilla.org/addressbook/cardproperty;1", &rv);
          }
          if (newCard) {
            rv = m_fieldMap->SetFieldValue(m_directory, newCard, fieldNum,
                                           fieldVal);
          }
        }
      } else {
        break;
      }
    } else if (active) {
      IMPORT_LOG1("*** Error getting field map for index %ld\n", (long)i);
    }
  }

  nsIAbCard* outCard;
  rv = m_directory->AddCard(newCard, &outCard);

  return rv;
}
