/* -*- 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 "InternetCiter.h"

#include "nsAString.h"
#include "nsCOMPtr.h"
#include "nsCRT.h"
#include "nsDebug.h"
#include "nsDependentSubstring.h"
#include "nsError.h"
#include "nsILineBreaker.h"
#include "nsLWBrkCIID.h"
#include "nsServiceManagerUtils.h"
#include "nsString.h"
#include "nsStringIterator.h"

namespace mozilla {

const char16_t gt ('>');
const char16_t space (' ');
const char16_t nl ('\n');
const char16_t cr('\r');

/**
 * Mail citations using the Internet style: > This is a citation.
 */

nsresult
InternetCiter::GetCiteString(const nsAString& aInString,
                             nsAString& aOutString)
{
  aOutString.Truncate();
  char16_t uch = nl;

  // Strip trailing new lines which will otherwise turn up
  // as ugly quoted empty lines.
  nsReadingIterator <char16_t> beginIter,endIter;
  aInString.BeginReading(beginIter);
  aInString.EndReading(endIter);
  while(beginIter!= endIter &&
        (*endIter == cr || *endIter == nl)) {
    --endIter;
  }

  // Loop over the string:
  while (beginIter != endIter) {
    if (uch == nl) {
      aOutString.Append(gt);
      // No space between >: this is ">>> " style quoting, for
      // compatibility with RFC 2646 and format=flowed.
      if (*beginIter != gt) {
        aOutString.Append(space);
      }
    }

    uch = *beginIter;
    ++beginIter;

    aOutString += uch;
  }

  if (uch != nl) {
    aOutString += nl;
  }
  return NS_OK;
}

nsresult
InternetCiter::StripCitesAndLinebreaks(const nsAString& aInString,
                                       nsAString& aOutString,
                                       bool aLinebreaksToo,
                                       int32_t* aCiteLevel)
{
  if (aCiteLevel) {
    *aCiteLevel = 0;
  }

  aOutString.Truncate();
  nsReadingIterator <char16_t> beginIter,endIter;
  aInString.BeginReading(beginIter);
  aInString.EndReading(endIter);
  while (beginIter!= endIter) { // loop over lines
    // Clear out cites first, at the beginning of the line:
    int32_t thisLineCiteLevel = 0;
    while (beginIter!= endIter &&
           (*beginIter == gt || nsCRT::IsAsciiSpace(*beginIter))) {
      if (*beginIter == gt) {
        ++thisLineCiteLevel;
      }
      ++beginIter;
    }
    // Now copy characters until line end:
    while (beginIter != endIter && (*beginIter != '\r' && *beginIter != '\n')) {
      aOutString.Append(*beginIter);
      ++beginIter;
    }
    if (aLinebreaksToo) {
      aOutString.Append(char16_t(' '));
    } else {
      aOutString.Append(char16_t('\n'));    // DOM linebreaks, not NS_LINEBREAK
    }
    // Skip over any more consecutive linebreak-like characters:
    while (beginIter != endIter && (*beginIter == '\r' || *beginIter == '\n')) {
      ++beginIter;
    }
    // Done with this line -- update cite level
    if (aCiteLevel && (thisLineCiteLevel > *aCiteLevel)) {
      *aCiteLevel = thisLineCiteLevel;
    }
  }
  return NS_OK;
}

nsresult
InternetCiter::StripCites(const nsAString& aInString,
                          nsAString& aOutString)
{
  return StripCitesAndLinebreaks(aInString, aOutString, false, 0);
}

static void AddCite(nsAString& aOutString, int32_t citeLevel)
{
  for (int32_t i = 0; i < citeLevel; ++i) {
    aOutString.Append(gt);
  }
  if (citeLevel > 0) {
    aOutString.Append(space);
  }
}

static inline void
BreakLine(nsAString& aOutString, uint32_t& outStringCol,
          uint32_t citeLevel)
{
  aOutString.Append(nl);
  if (citeLevel > 0) {
    AddCite(aOutString, citeLevel);
    outStringCol = citeLevel + 1;
  } else {
    outStringCol = 0;
  }
}

static inline bool IsSpace(char16_t c)
{
  const char16_t nbsp (0xa0);
  return (nsCRT::IsAsciiSpace(c) || (c == nl) || (c == cr) || (c == nbsp));
}

nsresult
InternetCiter::Rewrap(const nsAString& aInString,
                      uint32_t aWrapCol,
                      uint32_t aFirstLineOffset,
                      bool aRespectNewlines,
                      nsAString& aOutString)
{
  // There shouldn't be returns in this string, only dom newlines.
  // Check to make sure:
#ifdef DEBUG
  int32_t cr = aInString.FindChar(char16_t('\r'));
  NS_ASSERTION((cr < 0), "Rewrap: CR in string gotten from DOM!\n");
#endif /* DEBUG */

  aOutString.Truncate();

  nsresult rv;
  nsCOMPtr<nsILineBreaker> lineBreaker = do_GetService(NS_LBRK_CONTRACTID, &rv);
  NS_ENSURE_SUCCESS(rv, rv);

  // Loop over lines in the input string, rewrapping each one.
  uint32_t length;
  uint32_t posInString = 0;
  uint32_t outStringCol = 0;
  uint32_t citeLevel = 0;
  const nsPromiseFlatString &tString = PromiseFlatString(aInString);
  length = tString.Length();
  while (posInString < length) {
    // Get the new cite level here since we're at the beginning of a line
    uint32_t newCiteLevel = 0;
    while (posInString < length && tString[posInString] == gt) {
      ++newCiteLevel;
      ++posInString;
      while (posInString < length && tString[posInString] == space) {
        ++posInString;
      }
    }
    if (posInString >= length) {
      break;
    }

    // Special case: if this is a blank line, maintain a blank line
    // (retain the original paragraph breaks)
    if (tString[posInString] == nl && !aOutString.IsEmpty()) {
      if (aOutString.Last() != nl) {
        aOutString.Append(nl);
      }
      AddCite(aOutString, newCiteLevel);
      aOutString.Append(nl);

      ++posInString;
      outStringCol = 0;
      continue;
    }

    // If the cite level has changed, then start a new line with the
    // new cite level (but if we're at the beginning of the string,
    // don't bother).
    if (newCiteLevel != citeLevel && posInString > newCiteLevel+1 &&
        outStringCol) {
      BreakLine(aOutString, outStringCol, 0);
    }
    citeLevel = newCiteLevel;

    // Prepend the quote level to the out string if appropriate
    if (!outStringCol) {
      AddCite(aOutString, citeLevel);
      outStringCol = citeLevel + (citeLevel ? 1 : 0);
    }
    // If it's not a cite, and we're not at the beginning of a line in
    // the output string, add a space to separate new text from the
    // previous text.
    else if (outStringCol > citeLevel) {
      aOutString.Append(space);
      ++outStringCol;
    }

    // find the next newline -- don't want to go farther than that
    int32_t nextNewline = tString.FindChar(nl, posInString);
    if (nextNewline < 0) {
      nextNewline = length;
    }

    // For now, don't wrap unquoted lines at all.
    // This is because the plaintext edit window has already wrapped them
    // by the time we get them for rewrap, yet when we call the line
    // breaker, it will refuse to break backwards, and we'll end up
    // with a line that's too long and gets displayed as a lone word
    // on a line by itself.  Need special logic to detect this case
    // and break it ourselves without resorting to the line breaker.
    if (!citeLevel) {
      aOutString.Append(Substring(tString, posInString,
                                  nextNewline-posInString));
      outStringCol += nextNewline - posInString;
      if (nextNewline != (int32_t)length) {
        aOutString.Append(nl);
        outStringCol = 0;
      }
      posInString = nextNewline+1;
      continue;
    }

    // Otherwise we have to use the line breaker and loop
    // over this line of the input string to get all of it:
    while ((int32_t)posInString < nextNewline) {
      // Skip over initial spaces:
      while ((int32_t)posInString < nextNewline &&
             nsCRT::IsAsciiSpace(tString[posInString])) {
        ++posInString;
      }

      // If this is a short line, just append it and continue:
      if (outStringCol + nextNewline - posInString <= aWrapCol-citeLevel-1) {
        // If this short line is the final one in the in string,
        // then we need to include the final newline, if any:
        if (nextNewline+1 == (int32_t)length && tString[nextNewline-1] == nl) {
          ++nextNewline;
        }
        // Trim trailing spaces:
        int32_t lastRealChar = nextNewline;
        while ((uint32_t)lastRealChar > posInString &&
               nsCRT::IsAsciiSpace(tString[lastRealChar-1])) {
          --lastRealChar;
        }

        aOutString += Substring(tString,
                                posInString, lastRealChar - posInString);
        outStringCol += lastRealChar - posInString;
        posInString = nextNewline + 1;
        continue;
      }

      int32_t eol = posInString + aWrapCol - citeLevel - outStringCol;
      // eol is the prospective end of line.
      // We'll first look backwards from there for a place to break.
      // If it's already less than our current position,
      // then our line is already too long, so break now.
      if (eol <= (int32_t)posInString) {
        BreakLine(aOutString, outStringCol, citeLevel);
        continue;    // continue inner loop, with outStringCol now at bol
      }

      int32_t breakPt = 0;
      // XXX Why this uses NS_ERROR_"BASE"?
      rv = NS_ERROR_BASE;
      if (lineBreaker) {
        breakPt = lineBreaker->Prev(tString.get() + posInString,
                                 length - posInString, eol + 1 - posInString);
        if (breakPt == NS_LINEBREAKER_NEED_MORE_TEXT) {
          // if we couldn't find a breakpoint looking backwards,
          // and we're not starting a new line, then end this line
          // and loop around again:
          if (outStringCol > citeLevel + 1) {
            BreakLine(aOutString, outStringCol, citeLevel);
            continue;    // continue inner loop, with outStringCol now at bol
          }

          // Else try looking forwards:
          breakPt = lineBreaker->Next(tString.get() + posInString,
                                      length - posInString, eol - posInString);

          rv = breakPt == NS_LINEBREAKER_NEED_MORE_TEXT ? NS_ERROR_BASE :
                                                          NS_OK;
        } else {
          rv = NS_OK;
        }
      }
      // If rv is okay, then breakPt is the place to break.
      // If we get out here and rv is set, something went wrong with line
      // breaker.  Just break the line, hard.
      if (NS_FAILED(rv)) {
        breakPt = eol;
      }

      // Special case: maybe we should have wrapped last time.
      // If the first breakpoint here makes the current line too long,
      // then if we already have text on the current line,
      // break and loop around again.
      // If we're at the beginning of the current line, though,
      // don't force a break since the long word might be a url
      // and breaking it would make it unclickable on the other end.
      const int SLOP = 6;
      if (outStringCol + breakPt > aWrapCol + SLOP &&
          outStringCol > citeLevel+1) {
        BreakLine(aOutString, outStringCol, citeLevel);
        continue;
      }

      nsAutoString sub (Substring(tString, posInString, breakPt));
      // skip newlines or whitespace at the end of the string
      int32_t subend = sub.Length();
      while (subend > 0 && IsSpace(sub[subend-1])) {
        --subend;
      }
      sub.Left(sub, subend);
      aOutString += sub;
      outStringCol += sub.Length();
      // Advance past the whitespace which caused the wrap:
      posInString += breakPt;
      while (posInString < length && IsSpace(tString[posInString])) {
        ++posInString;
      }

      // Add a newline and the quote level to the out string
      if (posInString < length) {  // not for the last line, though
        BreakLine(aOutString, outStringCol, citeLevel);
      }
    } // end inner loop within one line of aInString
  } // end outer loop over lines of aInString

  return NS_OK;
}

} // namespace mozilla
