/* -*- 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 "msgCore.h"
#include "prlog.h"
#include "prmem.h"
#include "nsMsgLineBuffer.h"
#include "nsMsgUtils.h"
#include "nsIInputStream.h"  // used by nsMsgLineStreamBuffer

nsByteArray::nsByteArray() {
  MOZ_COUNT_CTOR(nsByteArray);
  m_buffer = NULL;
  m_bufferSize = 0;
  m_bufferPos = 0;
}

nsByteArray::~nsByteArray() {
  MOZ_COUNT_DTOR(nsByteArray);
  PR_FREEIF(m_buffer);
}

nsresult nsByteArray::GrowBuffer(uint64_t desired_size, uint32_t quantum) {
  if (desired_size > PR_UINT32_MAX) {
    return NS_ERROR_OUT_OF_MEMORY;
  }
  if (m_bufferSize < desired_size) {
    char* new_buf;
    uint32_t increment = desired_size - m_bufferSize;
    if (increment < quantum) /* always grow by a minimum of N bytes */
      increment = quantum;

    new_buf =
        (m_buffer ? (char*)PR_REALLOC(m_buffer, (m_bufferSize + increment))
                  : (char*)PR_MALLOC(m_bufferSize + increment));
    if (!new_buf) return NS_ERROR_OUT_OF_MEMORY;
    m_buffer = new_buf;
    m_bufferSize += increment;
  }
  return NS_OK;
}

nsresult nsByteArray::AppendString(const char* string) {
  uint32_t strLength = (string) ? PL_strlen(string) : 0;
  return AppendBuffer(string, strLength);
}

nsresult nsByteArray::AppendBuffer(const char* buffer, uint32_t length) {
  nsresult ret = NS_OK;
  if (m_bufferPos + length > m_bufferSize)
    ret = GrowBuffer(m_bufferPos + length, 1024);
  if (NS_SUCCEEDED(ret)) {
    memcpy(m_buffer + m_bufferPos, buffer, length);
    m_bufferPos += length;
  }
  return ret;
}

nsMsgLineBuffer::nsMsgLineBuffer() { MOZ_COUNT_CTOR(nsMsgLineBuffer); }

nsMsgLineBuffer::~nsMsgLineBuffer() { MOZ_COUNT_DTOR(nsMsgLineBuffer); }

nsresult nsMsgLineBuffer::BufferInput(const char* net_buffer,
                                      int32_t net_buffer_size) {
  if (net_buffer_size < 0) {
    return NS_ERROR_INVALID_ARG;
  }
  nsresult status = NS_OK;
  if (m_bufferPos > 0 && m_buffer && m_buffer[m_bufferPos - 1] == '\r' &&
      net_buffer_size > 0 && net_buffer[0] != '\n') {
    /* The last buffer ended with a CR.  The new buffer does not start
       with a LF.  This old buffer should be shipped out and discarded. */
    PR_ASSERT(m_bufferSize > m_bufferPos);
    if (m_bufferSize <= m_bufferPos) {
      return NS_ERROR_UNEXPECTED;
    }
    if (NS_FAILED(HandleLine(m_buffer, m_bufferPos))) {
      return NS_ERROR_FAILURE;
    }
    m_bufferPos = 0;
  }
  while (net_buffer_size > 0) {
    const char* net_buffer_end = net_buffer + net_buffer_size;
    const char* newline = 0;
    const char* s;

    for (s = net_buffer; s < net_buffer_end; s++) {
      /* Move forward in the buffer until the first newline.
         Stop when we see CRLF, CR, or LF, or the end of the buffer.
         *But*, if we see a lone CR at the *very end* of the buffer,
         treat this as if we had reached the end of the buffer without
         seeing a line terminator.  This is to catch the case of the
         buffers splitting a CRLF pair, as in "FOO\r\nBAR\r" "\nBAZ\r\n".
      */
      if (*s == '\r' || *s == '\n') {
        newline = s;
        if (newline[0] == '\r') {
          if (s == net_buffer_end - 1) {
            /* CR at end - wait for the next character. */
            newline = 0;
            break;
          } else if (newline[1] == '\n') {
            /* CRLF seen; swallow both. */
            newline++;
          }
        }
        newline++;
        break;
      }
    }

    /* Ensure room in the net_buffer and append some or all of the current
       chunk of data to it. */
    {
      const char* end = (newline ? newline : net_buffer_end);
      uint64_t desired_size = (end - net_buffer) + (uint64_t)m_bufferPos + 1;
      if (desired_size >= PR_INT32_MAX) {
        // We're not willing to buffer more than 2GB data without seeing
        // a newline, something is wrong with the input.
        // Using this limit prevents us from overflowing.
        return NS_ERROR_UNEXPECTED;
      }

      if (desired_size >= m_bufferSize) {
        status = GrowBuffer(desired_size, 1024);
        if (NS_FAILED(status)) return status;
      }
      memcpy(m_buffer + m_bufferPos, net_buffer, (end - net_buffer));
      m_bufferPos += (end - net_buffer);
    }

    /* Now m_buffer contains either a complete line, or as complete
       a line as we have read so far.

       If we have a line, process it, and then remove it from `m_buffer'.
       Then go around the loop again, until we drain the incoming data.
     */
    if (!newline) return NS_OK;

    if (NS_FAILED(HandleLine(m_buffer, m_bufferPos))) {
      return NS_ERROR_FAILURE;
    }

    net_buffer_size -= (newline - net_buffer);
    net_buffer = newline;
    m_bufferPos = 0;
  }
  return NS_OK;
}

// If there's still some data (non CRLF terminated) flush it out
nsresult nsMsgLineBuffer::Flush() {
  nsresult rv = NS_OK;
  if (m_bufferPos > 0) {
    rv = HandleLine(m_buffer, m_bufferPos);
    m_bufferPos = 0;
  }
  return rv;
}

///////////////////////////////////////////////////////////////////////////////////////////////////
// This is a utility class used to efficiently extract lines from an input
// stream by buffering read but unprocessed stream data in a buffer.
///////////////////////////////////////////////////////////////////////////////////////////////////

nsMsgLineStreamBuffer::nsMsgLineStreamBuffer(uint32_t aBufferSize,
                                             bool aAllocateNewLines,
                                             bool aEatCRLFs, char aLineToken)
    : m_eatCRLFs(aEatCRLFs),
      m_allocateNewLines(aAllocateNewLines),
      m_lineToken(aLineToken) {
  NS_ASSERTION(aBufferSize > 0, "invalid buffer size!!!");
  m_dataBuffer = nullptr;
  m_startPos = 0;
  m_numBytesInBuffer = 0;

  // used to buffer incoming data by ReadNextLineFromInput
  if (aBufferSize > 0) {
    m_dataBuffer = (char*)PR_CALLOC(sizeof(char) * aBufferSize);
  }

  m_dataBufferSize = aBufferSize;
}

nsMsgLineStreamBuffer::~nsMsgLineStreamBuffer() {
  PR_FREEIF(m_dataBuffer);  // release our buffer...
}

nsresult nsMsgLineStreamBuffer::GrowBuffer(uint32_t desiredSize) {
  char* newBuffer = (char*)PR_REALLOC(m_dataBuffer, desiredSize);
  NS_ENSURE_TRUE(newBuffer, NS_ERROR_OUT_OF_MEMORY);
  m_dataBuffer = newBuffer;
  m_dataBufferSize = desiredSize;
  return NS_OK;
}

void nsMsgLineStreamBuffer::ClearBuffer() {
  m_startPos = 0;
  m_numBytesInBuffer = 0;
}

// aInputStream - the input stream we want to read a line from
// aPauseForMoreData is returned as true if the stream does not yet contain a
// line and we must wait for more data to come into the stream. Note to people
// wishing to modify this function: Be *VERY CAREFUL* this is a critical
// function used by all of our mail protocols including imap, nntp, and pop. If
// you screw it up, you could break a lot of stuff.....

char* nsMsgLineStreamBuffer::ReadNextLine(nsIInputStream* aInputStream,
                                          uint32_t& aNumBytesInLine,
                                          bool& aPauseForMoreData,
                                          nsresult* prv,
                                          bool addLineTerminator) {
  // try to extract a line from m_inputBuffer. If we don't have an entire line,
  // then read more bytes out from the stream. If the stream is empty then wait
  // on the monitor for more data to come in.

  NS_ASSERTION(m_dataBuffer && m_dataBufferSize > 0,
               "invalid input arguments for read next line from input");

  if (prv) *prv = NS_OK;
  // initialize out values
  aPauseForMoreData = false;
  aNumBytesInLine = 0;
  char* endOfLine = nullptr;
  char* startOfLine = m_dataBuffer + m_startPos;

  if (m_numBytesInBuffer > 0)  // any data in our internal buffer?
    endOfLine = PL_strchr(startOfLine, m_lineToken);  // see if we already
                                                      // have a line ending...

  // it's possible that we got here before the first time we receive data from
  // the server so aInputStream will be nullptr...
  if (!endOfLine && aInputStream)  // get some more data from the server
  {
    nsresult rv;
    uint64_t numBytesInStream = 0;
    uint32_t numBytesCopied = 0;
    bool nonBlockingStream;
    aInputStream->IsNonBlocking(&nonBlockingStream);
    rv = aInputStream->Available(&numBytesInStream);
    if (NS_FAILED(rv)) {
      if (prv) *prv = rv;
      aNumBytesInLine = 0;
      return nullptr;
    }
    if (!nonBlockingStream && numBytesInStream == 0)  // if no data available,
      numBytesInStream = m_dataBufferSize / 2;        // ask for half the data
                                                      // buffer size.

    // if the number of bytes we want to read from the stream, is greater than
    // the number of bytes left in our buffer, then we need to shift the start
    // pos and its contents down to the beginning of m_dataBuffer...
    uint32_t numFreeBytesInBuffer =
        m_dataBufferSize - m_startPos - m_numBytesInBuffer;
    if (numBytesInStream >= numFreeBytesInBuffer) {
      if (m_startPos) {
        memmove(m_dataBuffer, startOfLine, m_numBytesInBuffer);
        // make sure the end of the buffer is terminated
        m_dataBuffer[m_numBytesInBuffer] = '\0';
        m_startPos = 0;
        startOfLine = m_dataBuffer;
        numFreeBytesInBuffer = m_dataBufferSize - m_numBytesInBuffer;
      }
      // If we didn't make enough space (or any), grow the buffer
      if (numBytesInStream >= numFreeBytesInBuffer) {
        int64_t growBy = (numBytesInStream - numFreeBytesInBuffer) * 2 + 1;
        // GrowBuffer cannot handle over 4GB size.
        if (m_dataBufferSize + growBy > PR_UINT32_MAX) return nullptr;
        // try growing buffer by twice as much as we need.
        nsresult rv = GrowBuffer(m_dataBufferSize + growBy);
        // if we can't grow the buffer, we have to bail.
        if (NS_FAILED(rv)) return nullptr;
        startOfLine = m_dataBuffer;
        numFreeBytesInBuffer += growBy;
      }
      NS_ASSERTION(m_startPos == 0, "m_startPos should be 0 .....");
    }

    uint32_t numBytesToCopy = /* leave one for a null terminator */
        std::min(uint64_t(numFreeBytesInBuffer - 1), numBytesInStream);
    if (numBytesToCopy > 0) {
      // read the data into the end of our data buffer
      char* startOfNewData = startOfLine + m_numBytesInBuffer;
      rv = aInputStream->Read(startOfNewData, numBytesToCopy, &numBytesCopied);
      if (prv) *prv = rv;
      uint32_t i;
      for (i = 0; i < numBytesCopied; i++)  // replace nulls with spaces
      {
        if (!startOfNewData[i]) startOfNewData[i] = ' ';
      }
      m_numBytesInBuffer += numBytesCopied;
      m_dataBuffer[m_startPos + m_numBytesInBuffer] = '\0';

      // okay, now that we've tried to read in more data from the stream,
      // look for another end of line character in the new data
      endOfLine = PL_strchr(startOfNewData, m_lineToken);
    }
  }

  // okay, now check again for endOfLine.
  if (endOfLine) {
    if (!m_eatCRLFs) endOfLine += 1;  // count for LF or CR

    aNumBytesInLine = endOfLine - startOfLine;

    if (m_eatCRLFs && aNumBytesInLine > 0 &&
        startOfLine[aNumBytesInLine - 1] == '\r')
      aNumBytesInLine--;  // Remove the CR in a CRLF sequence.

    // PR_CALLOC zeros out the allocated line
    char* newLine = (char*)PR_CALLOC(
        aNumBytesInLine + (addLineTerminator ? MSG_LINEBREAK_LEN : 0) + 1);
    if (!newLine) {
      aNumBytesInLine = 0;
      aPauseForMoreData = true;
      return nullptr;
    }

    memcpy(newLine, startOfLine,
           aNumBytesInLine);  // copy the string into the new line buffer
    if (addLineTerminator) {
      memcpy(newLine + aNumBytesInLine, MSG_LINEBREAK, MSG_LINEBREAK_LEN);
      aNumBytesInLine += MSG_LINEBREAK_LEN;
    }

    if (m_eatCRLFs)
      endOfLine += 1;  // advance past LF or CR if we haven't already done so...

    // now we need to update the data buffer to go past the line we just read
    // out.
    m_numBytesInBuffer -= (endOfLine - startOfLine);
    if (m_numBytesInBuffer)
      m_startPos = endOfLine - m_dataBuffer;
    else
      m_startPos = 0;

    return newLine;
  }

  aPauseForMoreData = true;
  return nullptr;  // if we somehow got here. we don't have another line in the
                   // buffer yet...need to wait for more data...
}

bool nsMsgLineStreamBuffer::NextLineAvailable() {
  return (m_numBytesInBuffer > 0 &&
          PL_strchr(m_dataBuffer + m_startPos, m_lineToken));
}
