/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
 *
 * The contents of this file are subject to the Netscape Public License
 * Version 1.0 (the "NPL"); you may not use this file except in
 * compliance with the NPL.  You may obtain a copy of the NPL at
 * http://www.mozilla.org/NPL/
 *
 * Software distributed under the NPL is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the NPL
 * for the specific language governing rights and limitations under the
 * NPL.
 *
 * The Initial Developer of this code under the NPL is Netscape
 * Communications Corporation.  Portions created by Netscape are
 * Copyright (C) 1998 Netscape Communications Corporation.  All Rights
 * Reserved.
 */
#include "nsCSSScanner.h"
#include "nsIInputStream.h"
#include "nsIUnicharInputStream.h"
#include "nsString.h"
#include "nsCRT.h"

#ifdef NS_DEBUG
static char* kNullPointer = "null pointer";
#endif

// Don't bother collecting whitespace characters in token's mIdent buffer
#undef COLLECT_WHITESPACE

#define BUFFER_SIZE 256

static const PRUnichar CSS_ESCAPE = PRUnichar('\\');

static const PRUint8 IS_LATIN1 = 0x01;
static const PRUint8 IS_DIGIT = 0x02;
static const PRUint8 IS_HEX_DIGIT = 0x04;
static const PRUint8 IS_ALPHA = 0x08;
static const PRUint8 START_IDENT = 0x10;
static const PRUint8 IS_IDENT = 0x20;
static const PRUint8 IS_WHITESPACE = 0x40;

static PRUint8* gLexTable;

static void BuildLexTable()
{
  PRUint8* lt = new PRUint8[256];
  nsCRT::zero(lt, 256);
  gLexTable = lt;

  int i;
  lt[CSS_ESCAPE] = START_IDENT;
  lt['-'] |= IS_IDENT;
  // XXX add in other whitespace chars
  lt[' '] |= IS_WHITESPACE;
  lt['\t'] |= IS_WHITESPACE;
  lt['\r'] |= IS_WHITESPACE;
  lt['\n'] |= IS_WHITESPACE;
  for (i = 161; i <= 255; i++) {
    lt[i] |= IS_LATIN1 | IS_IDENT | START_IDENT;
  }
  for (i = '0'; i <= '9'; i++) {
    lt[i] |= IS_DIGIT | IS_HEX_DIGIT | IS_IDENT;
  }
  for (i = 'A'; i <= 'Z'; i++) {
    if ((i >= 'A') && (i <= 'F')) {
      lt[i] |= IS_HEX_DIGIT;
      lt[i+32] |= IS_HEX_DIGIT;
    }
    lt[i] |= IS_ALPHA | IS_IDENT | START_IDENT;
    lt[i+32] |= IS_ALPHA | IS_IDENT | START_IDENT;
  }
}

nsCSSToken::nsCSSToken()
{
  mType = eCSSToken_Symbol;
}

nsCSSScanner::nsCSSScanner()
{
  if (nsnull == gLexTable) {
    // XXX need a monitor
    BuildLexTable();
  }
  mInput = nsnull;
  mBuffer = new PRUnichar[BUFFER_SIZE];
  mOffset = 0;
  mCount = 0;
  mLookAhead = -1;
}

nsCSSScanner::~nsCSSScanner()
{
  Close();
  if (nsnull != mBuffer) {
    delete mBuffer;
    mBuffer = nsnull;
  }
}

void nsCSSScanner::Init(nsIUnicharInputStream* aInput)
{
  NS_PRECONDITION(nsnull != aInput, kNullPointer);
  Close();
  mInput = aInput;
  if (nsnull != aInput) {
    aInput->AddRef();
  }
}

void nsCSSScanner::Close()
{
  NS_IF_RELEASE(mInput);
}

// Returns -1 on error or eof
PRInt32 nsCSSScanner::Read(PRInt32* aErrorCode)
{
  PRInt32 rv;
  if (mLookAhead >= 0) {
    rv = mLookAhead;
    mLookAhead = -1;
  } else {
    if (mCount < 0) {
      return -1;
    }
    if (mOffset == mCount) {
      mOffset = 0;
      mCount = mInput->Read(aErrorCode, mBuffer, 0, BUFFER_SIZE);
      if (mCount <= 0) {
        return -1;
      }
    }
    rv = PRInt32(mBuffer[mOffset++]);
  }
  mLastRead = rv;
//printf("Read => %x\n", rv);
  return rv;
}

PRInt32 nsCSSScanner::Peek(PRInt32* aErrorCode)
{
  if (mLookAhead < 0) {
    mLookAhead = Read(aErrorCode);
    if (mLookAhead < 0) {
      return -1;
    }
  }
//printf("Peek => %x\n", mLookAhead);
  return mLookAhead;
}

void nsCSSScanner::Unread()
{
  NS_PRECONDITION((mLastRead >= 0) && (mLookAhead < 0), "double pushback");
  mLookAhead = mLastRead;
  mLastRead = -1;
}

PRBool nsCSSScanner::LookAhead(PRInt32* aErrorCode, PRUnichar aChar)
{
  PRInt32 ch = Read(aErrorCode);
  if (ch < 0) {
    return PR_FALSE;
  }
  if (ch == aChar) {
    return PR_TRUE;
  }
  Unread();
  return PR_FALSE;
}

PRBool nsCSSScanner::EatWhiteSpace(PRInt32* aErrorCode)
{
  PRBool eaten = PR_FALSE;
  for (;;) {
    PRInt32 ch = Read(aErrorCode);
    if (ch < 0) {
      break;
    }
    if ((ch == ' ') || (ch == '\n') || (ch == '\r') || (ch == '\t')) {
      eaten = PR_TRUE;
      continue;
    }
    Unread();
    break;
  }
  return eaten;
}

PRBool nsCSSScanner::EatNewline(PRInt32* aErrorCode)
{
  PRInt32 ch = Read(aErrorCode);
  if (ch < 0) {
    return PR_FALSE;
  }
  PRBool eaten = PR_FALSE;
  if (ch == '\r') {
    eaten = PR_TRUE;
    ch = Peek(aErrorCode);
    if (ch == '\n') {
      (void) Read(aErrorCode);
    }
  } else if (ch == '\n') {
    eaten = PR_TRUE;
  } else {
    Unread();
  }
  return eaten;
}

PRBool nsCSSScanner::Next(PRInt32* aErrorCode, nsCSSToken* aToken)
{
  PRInt32 ch = Read(aErrorCode);
  if (ch < 0) {
    return PR_FALSE;
  }
  if (ch < 256) {
    PRUint8* lexTable = gLexTable;

    // IDENT
    if ((lexTable[ch] & START_IDENT) != 0) {
      return ParseIdent(aErrorCode, ch, aToken);
    }

    // AT_KEYWORD
    if (ch == '@') {
      PRInt32 nextChar = Peek(aErrorCode);
      if ((nextChar >= 0) && (nextChar <= 255)) {
        if ((lexTable[nextChar] & START_IDENT) != 0) {
          return ParseAtKeyword(aErrorCode, ch, aToken);
        }
      }
    }

    // NUMBER or DIM
    if ((ch == '.') || (ch == '+') || (ch == '-')) {
      PRInt32 nextChar = Peek(aErrorCode);
      if ((nextChar >= 0) && (nextChar <= 255)) {
        if ((lexTable[nextChar] & IS_DIGIT) != 0) {
          return ParseNumber(aErrorCode, ch, aToken);
        }
      }
    }
    if ((lexTable[ch] & IS_DIGIT) != 0) {
      return ParseNumber(aErrorCode, ch, aToken);
    }

    // ID
    if (ch == '#') {
      return ParseID(aErrorCode, ch, aToken);
    }

    // STRING
    if ((ch == '"') || (ch == '\'')) {
      return ParseString(aErrorCode, ch, aToken);
    }

    // WS
    if ((lexTable[ch] & IS_WHITESPACE) != 0) {
      aToken->mType = eCSSToken_WhiteSpace;
      aToken->mIdent.SetLength(0);
      aToken->mIdent.Append(PRUnichar(ch));
      (void) EatWhiteSpace(aErrorCode);
      return PR_TRUE;
    }
    if (ch == '/') {
      PRInt32 nextChar = Peek(aErrorCode);
      if (nextChar == '/') {
        (void) Read(aErrorCode);
        aToken->mIdent.SetLength(0);
        aToken->mIdent.Append(PRUnichar(ch));
        aToken->mIdent.Append(PRUnichar(ch));
        return ParseEOLComment(aErrorCode, aToken);
      } else if (nextChar == '*') {
        (void) Read(aErrorCode);
        aToken->mIdent.SetLength(0);
        aToken->mIdent.Append(PRUnichar(ch));
        aToken->mIdent.Append(PRUnichar(nextChar));
        return ParseCComment(aErrorCode, aToken);
      }
    }
  }
  aToken->mType = eCSSToken_Symbol;
  aToken->mSymbol = ch;
  return PR_TRUE;
}

PRInt32 nsCSSScanner::ParseEscape(PRInt32* aErrorCode)
{
  PRUint8* lexTable = gLexTable;
  PRInt32 ch = Peek(aErrorCode);
  if (ch < 0) {
    return CSS_ESCAPE;
  }
  if ((ch <= 255) && ((lexTable[ch] & IS_HEX_DIGIT) != 0)) {
    PRInt32 rv = 0;
    for (int i = 0; i < 4; i++) {
      ch = Read(aErrorCode);
      if (ch < 0) {
        // Whoops: error or premature eof
        break;
      }
      if ((lexTable[ch] & IS_HEX_DIGIT) != 0) {
        if ((lexTable[ch] & IS_DIGIT) != 0) {
          rv = rv * 16 + (ch - '0');
        } else {
          // Note: c&7 just keeps the low three bits which causes
          // upper and lower case alphabetics to both yield their
          // "relative to 10" value for computing the hex value.
          rv = rv * 16 + (ch & 0x7) + 10;
        }
      } else {
        Unread();
        break;
      }
    }
    return rv;
  } else {
    // "Any character except a hexidecimal digit can be escaped to
    // remove its special meaning by putting a backslash in front"
    // -- CSS1 spec section 7.1
    (void) Read(aErrorCode);
    return ch;
  }
}

/**
 * Gather up the characters in an identifier. The identfier was
 * started by "aChar" which will be appended to aIdent. The result
 * will be aIdent with all of the identifier characters appended
 * until the first non-identifier character is seen. The termination
 * character is unread for the future re-reading.
 */
PRBool nsCSSScanner::GatherIdent(PRInt32* aErrorCode, PRInt32 aChar,
                                 nsString& aIdent)
{
  if (aChar == CSS_ESCAPE) {
    aChar = ParseEscape(aErrorCode);
  }
  for (;;) {
    aChar = Read(aErrorCode);
    if (aChar < 0) break;
    if (aChar == CSS_ESCAPE) {
      aChar = ParseEscape(aErrorCode);
      aIdent.Append(PRUnichar(aChar));
    } else if ((aChar <= 255) && ((gLexTable[aChar] & IS_IDENT) != 0)) {
      aIdent.Append(PRUnichar(aChar));
    } else {
      Unread();
      break;
    }
  }
  return PR_TRUE;
}

PRBool nsCSSScanner::ParseID(PRInt32* aErrorCode,
                             PRInt32 aChar,
                             nsCSSToken* aToken)
{
  aToken->mIdent.SetLength(0);
  aToken->mType = eCSSToken_ID;
  return GatherIdent(aErrorCode, aChar, aToken->mIdent);
}

PRBool nsCSSScanner::ParseIdent(PRInt32* aErrorCode,
                                PRInt32 aChar,
                                nsCSSToken* aToken)
{
  nsString& ident = aToken->mIdent;
  ident.SetLength(0);
  ident.Append(PRUnichar(aChar));
  if (!GatherIdent(aErrorCode, aChar, ident)) {
    return PR_FALSE;
  }

  // Process a url lexical token. A CSS1 url token can contain
  // characters beyond identifier characters (e.g. '/', ':', etc.)
  // Because of this the normal rules for tokenizing the input don't
  // apply very well. To simplify the parser and relax some of the
  // requirements on the scanner we parse url's here. If we find a
  // malformed URL then we emit a token of type "InvalidURL" so that
  // the CSS1 parser can ignore the invalid input. We attempt to eat
  // the right amount of input data when an invalid URL is presented.
  nsCSSTokenType tokenType = eCSSToken_Ident;
  if (ident.EqualsIgnoreCase("url")) {
    tokenType = eCSSToken_InvalidURL;
    if (LookAhead(aErrorCode, '(')) {
      // Skip leading whitespace
      (void) EatWhiteSpace(aErrorCode);
      ident.SetLength(0);

      PRInt32 c = Read(aErrorCode);
      if (c == ')') {
        // empty url spec: this is invalid
      } else if ((c == '"') || (c == '\'')) {
        // start of a quoted url
        if (!GatherString(aErrorCode, c, ident)) {
          return PR_FALSE;
        }
        if (!EatWhiteSpace(aErrorCode)) {
          return PR_FALSE;
        }
        if (LookAhead(aErrorCode, ')')) {
          tokenType = eCSSToken_URL;
        }
      } else {
        // start of a non-quoted url
        Unread();
        PRBool ok = PR_TRUE;
        for (;;) {
          c = Read(aErrorCode);
          if (c < 0) break;
          if (c == CSS_ESCAPE) {
            c = ParseEscape(aErrorCode);
            ident.Append(PRUnichar(c));
          } else if ((c == '"') || (c == '\'') || (c == '(')) {
            // This is an invalid URL spec
            ok = PR_FALSE;
          } else if ((256 >= c) && ((gLexTable[c] & IS_WHITESPACE) != 0)) {
            // Whitespace is allowed at the end of the URL
            (void) EatWhiteSpace(aErrorCode);
            if (LookAhead(aErrorCode, ')')) {
              // done!
              break;
            }
            // Whitespace is followed by something other than a
            // ")". This is an invalid url spec.
            ok = PR_FALSE;
          } else if (c == ')') {
            // All done
            break;
          } else {
            // A regular url character.
            ident.Append(PRUnichar(c));
          }
        }

        // If the result of the above scanning is ok then change the token
        // type to a useful one.
        if (ok) {
          tokenType = eCSSToken_URL;
        }
      }
    }
  }
  aToken->mType = tokenType;
  return PR_TRUE;
}

PRBool nsCSSScanner::ParseAtKeyword(PRInt32* aErrorCode, PRInt32 aChar,
                                    nsCSSToken* aToken)
{
  aToken->mIdent.SetLength(0);
  aToken->mType = eCSSToken_AtKeyword;
  return GatherIdent(aErrorCode, aChar, aToken->mIdent);
}

PRBool nsCSSScanner::ParseNumber(PRInt32* aErrorCode, PRInt32 c,
                                 nsCSSToken* aToken)
{
  nsString& ident = aToken->mIdent;
  ident.SetLength(0);
  PRBool gotDot = (c == '.') ? PR_TRUE : PR_FALSE;
  if (c != '+') {
    ident.Append(PRUnichar(c));
  }

  // Gather up characters that make up the number
  PRUint8* lexTable = gLexTable;
  for (;;) {
    c = Read(aErrorCode);
    if (c < 0) break;
    if (!gotDot && (c == '.')) {
      gotDot = PR_TRUE;
    } else if ((c > 255) || ((lexTable[c] & IS_DIGIT) == 0)) {
      break;
    }
    ident.Append(PRUnichar(c));
  }

  // Convert number to floating point
  nsCSSTokenType type = eCSSToken_Number;
  PRInt32 ec;
  float value = ident.ToFloat(&ec);

  // Look at character that terminated the number
  aToken->mIntegerValid = PR_FALSE;
  if (c >= 0) {
    if ((c <= 255) && ((lexTable[c] & START_IDENT) != 0)) {
      ident.SetLength(0);
      ident.Append(PRUnichar(c));
      if (!GatherIdent(aErrorCode, c, ident)) {
        return PR_FALSE;
      }
      type = eCSSToken_Dimension;
    } else if ('%' == c) {
      type = eCSSToken_Percentage;
      value = value / 100.0f;
    } else {
      // Put back character that stopped numeric scan
      Unread();
      if (!gotDot) {
        aToken->mInteger = ident.ToInteger(&ec);
        aToken->mIntegerValid = PR_TRUE;
      }
      ident.SetLength(0);
    }
  }
  aToken->mNumber = value;
  aToken->mType = type;
  return PR_TRUE;
}

PRBool nsCSSScanner::ParseCComment(PRInt32* aErrorCode, nsCSSToken* aToken)
{
  nsString& ident = aToken->mIdent;
  for (;;) {
    PRInt32 ch = Read(aErrorCode);
    if (ch < 0) break;
    if (ch == '*') {
      if (LookAhead(aErrorCode, '/')) {
        ident.Append(PRUnichar(ch));
        ident.Append('/');
        break;
      }
    }
#ifdef COLLECT_WHITESPACE
    ident.Append(PRUnichar(ch));
#endif
  }
  aToken->mType = eCSSToken_WhiteSpace;
  return PR_TRUE;
}

PRBool nsCSSScanner::ParseEOLComment(PRInt32* aErrorCode, nsCSSToken* aToken)
{
  nsString& ident = aToken->mIdent;
  ident.SetLength(0);
  for (;;) {
    if (EatNewline(aErrorCode)) {
      break;
    }
    PRInt32 ch = Read(aErrorCode);
    if (ch < 0) {
      break;
    }
#ifdef COLLECT_WHITESPACE
    ident.Append(PRUnichar(ch));
#endif
  }
  aToken->mType = eCSSToken_WhiteSpace;
  return PR_TRUE;
}

PRBool nsCSSScanner::GatherString(PRInt32* aErrorCode, PRInt32 aStop,
                                  nsString& aBuffer)
{
  for (;;) {
    PRInt32 ch = Read(aErrorCode);
    if (ch < 0) {
      return PR_FALSE;
    }
    if (ch == aStop) {
      break;
    }
    if (ch == CSS_ESCAPE) {
      ch = ParseEscape(aErrorCode);
      if (ch < 0) {
        return PR_FALSE;
      }
    }
    aBuffer.Append(PRUnichar(ch));
  }
  return PR_TRUE;
}

PRBool nsCSSScanner::ParseString(PRInt32* aErrorCode, PRInt32 aStop,
                                 nsCSSToken* aToken)
{
  aToken->mIdent.SetLength(0);
  aToken->mType = eCSSToken_String;
  return GatherString(aErrorCode, aStop, aToken->mIdent);
}
