/* vim: set ts=4 sw=4 tw=99 et:
 *
 * Copyright (C) 2012 David Anderson
 *
 * This file is part of JITCraft.
 *
 * JITCraft is free software: you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option)
 * any later version.
 * 
 * Foobar is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * JITCraft. If not, see http://www.gnu.org/licenses/.
 */
#include <assert.h>
#include <ctype.h>
#include <math.h>
#include <limits.h>
#include <string.h>
#include "Scanner.h"
#include "Utility.h"

using namespace ke;

const char *ke::TokenNames[] =
{
#define _(name, str) str,
    TOKENMAP(_)
#undef _
    NULL
};

Scanner::Scanner(CompileContext &cc, const char *stream, size_t length)
  : cc_(cc),
    stream_(stream),
    length_(length),
    pushed_(false),
    saved_('\0'),
    pos_(stream_)
{
    cur_.line = 1;
    cur_.col = 1;
    last_ = cur_;
}

char
Scanner::getChar()
{
    if (saved_ != '\0') {
        char temp = saved_;
        saved_ = '\0';
        return temp;
    }
    if (pos_ >= stream_ + length_)
        return '\0';
    last_.col = cur_.col++;
    return *pos_++;
}

void
Scanner::putChar(char c)
{
    assert(saved_ == '\0');
    saved_ = c;
    cur_.col = last_.col;
}

void
Scanner::calcNewLine(char c)
{
    cur_.line++;
    cur_.col = 1;
    last_ = cur_;

    char o;
    if (c == '\r' && ((o = getChar()) != '\n'))
        putChar(o);
}

void
Scanner::eatSingleLine()
{
    char c;
    while ((c = getChar()) != '\0') {
        if (c == '\n' || c == '\r') {
            calcNewLine(c);
            break;
        }
    }
}

bool
Scanner::eatComment()
{
    char c;

    while ((c = getChar()) != '\0') {
        while (c == '*') {
            c = getChar();
            if (c == '\0')
                return false;
            if (c == '/')
                return true;
        }
        if (c == '\r' || c == '\n')
            calcNewLine(c);
    }

    return false;
}

static inline bool
IsDigit(char c)
{
    return c >= '0' && c <= '9';
}

// Based off the logic in sc2.c's ftoi()...
double
Scanner::ParseDouble(const char *string)
{
    const char *ptr = string;

    double number = 0.0;
    while (IsDigit(*ptr)) {
        number = (number * 10) + (*ptr - '0');
        ptr++;
    }

    assert(*ptr == '.');
    ptr++;

    double fraction = 0.0;
    double multiplier = 1.0;
    while (IsDigit(*ptr)) {
        fraction = (fraction * 10) + (*ptr - '0');
        multiplier = multiplier / 10.0;
        ptr++;
    }

    number += fraction * multiplier;

    if (*ptr++ == 'e') {
        int sign = 1;
        if (*ptr == '-') {
            sign = -1;
            ptr++;
        }

        int exponent = 0;
        while (IsDigit(*ptr)) {
            exponent = (exponent * 10) + (*ptr - '0');
            ptr++;
        }

        multiplier = pow(10.0, exponent * sign);
        number *= multiplier;
    }

    return number;
}

static inline bool
IsHexDigit(char c)
{
    return IsDigit(c) ||
           (c >= 'a' && c <= 'f') ||
           (c >= 'A' && c <= 'F');
}

// Based on the logic for litchar() in sc2.c.
int
Scanner::readEscapeCode()
{
    char c = getChar();
    if (c == '\\')
        return c;

    switch (c) {
      case 'a':
        return '\a';
      case 'b':
        return '\b';
      case 'e':
        return 27;          // Apparently \e is non-standard.
      case 'f':
        return '\f';
      case 'n':
        return '\n';
      case 'r':
        return '\r';
      case 't':
        return '\t';
      case '\v':
        return '\v';

      case 'x':
      {
          unsigned digits = 0;
          char r = 0;
  
          c = getChar();
          while (IsHexDigit(c) && digits < 2) {
              if (IsDigit(c))
                  c = (c << 4) + (c - '0');
              else
                  c = (c << 4) + (tolower(c) - 'a' + 10);
              digits++;
              c = getChar();
           }
                
          // Swallow a trailing ';'
          if (c != ';')
              putChar(c);
  
          return r;
      }

      case '\'':
      case '\"':
      case '%':
        return c;

      default:
      {
        if (IsDigit(c)) {
            // \ddd
            char r = 0;
            while (IsDigit(c)) {
                r = r * 10 + (c - '0');
                c = getChar();
            }

            // Swallow a trailing ;
            if (c != ';')
                putChar(c);

            return r;
        }
        cc_.reportError(begin(), Message_UnknownEscapeCode, c);
        return INT_MAX;
      }
    }
}

TokenKind
Scanner::stringLiteral()
{
    literal_.clear();

    for (;;) {
        char c = getChar();
        if (c == '\"')
            break;
        if (c == '\r' || c == '\n' || c == '\0') {
            cc_.reportError(begin(), Message_UnterminatedString);
            return TOK_ERROR;
        }
        if (c == '\\') {
            int code = readEscapeCode();
            if (code == INT_MAX)
                return TOK_ERROR;
            c = char(code);
        }
        if (!literal_.append(c))
            return TOK_ERROR;
    }

    if (!literal_.append('\0'))
        return TOK_ERROR;

    return TOK_STRING_LITERAL;
}

TokenKind
Scanner::numberLiteral(char first)
{
    literal_.clear();
    if (!literal_.append(first))
        return TOK_ERROR;

    char c;
    for (;;) {
        c = getChar();
        if (!IsDigit(c))
            break;
        if (!literal_.append(c))
            return TOK_ERROR;
    }

    if (c != '.') {
        putChar(c);
        if (!literal_.append('\0'))
            return TOK_ERROR;
        return TOK_INTEGER_LITERAL;
    }
    if (!literal_.append(c))
        return TOK_ERROR;

    c = getChar();
    if (!IsDigit(c)) {
        cc_.reportError(begin(), Message_ExpectedDigitForFloat, c);
        return TOK_ERROR;
    }
    if (!literal_.append(c))
        return TOK_ERROR;

    for (;;) {
        c = getChar();
        if (!IsDigit(c))
            break;
        if (!literal_.append(c))
            return TOK_ERROR;
    }

    if (c != 'e') {
        putChar(c);
    } else {
        if (!literal_.append(c))
            return TOK_ERROR;
        c = getChar();
        if (c == '-') {
            if (!literal_.append(c))
                return TOK_ERROR;
            c = getChar();
        }
        if (!IsDigit(c)) {
            cc_.reportError(begin(), Message_ExpectedDigitForFloat, c);
            return TOK_ERROR;
        }
        if (!literal_.append(c))
            return TOK_ERROR;
        for (;;) {
            c = getChar();
            if (!IsDigit(c)) {
                putChar(c);
                break;
            }
            if (!literal_.append(c))
                return TOK_ERROR;
        }
    }

    if (!literal_.append('\0'))
        return TOK_ERROR;

    return TOK_FLOAT_LITERAL;
}

TokenKind
Scanner::identifier(char first)
{
    literal_.clear();
    if (!literal_.append(first))
        return TOK_ERROR;

    char c;
    for (;;) {
        c = getChar();
        if (!isascii(c) || (!isalnum(c) && c != '_')) {
            if (c != ':')
                putChar(c);
            break;
        }
        if (!literal_.append(c))
            return TOK_ERROR;
    }
    
    if (!literal_.append('\0'))
        return TOK_ERROR;

    if (c != ':' && !strcmp(literal(), "_"))
        return TOK_UNDERSCORE;
    if (!strcmp(literal(),  "new"))
        return TOK_NEW;
    if (!strcmp(literal(),  "for"))
        return TOK_FOR;
    if (!strcmp(literal(),  "return"))
        return TOK_RETURN;
    if (!strcmp(literal(),  "if"))
        return TOK_IF;
    if (!strcmp(literal(),  "else"))
        return TOK_ELSE;
    if (!strcmp(literal(),  "enum"))
        return TOK_ENUM;
    if (!strcmp(literal(),  "while"))
        return TOK_WHILE;
    if (!strcmp(literal(),  "do"))
        return TOK_DO;
    if (!strcmp(literal(),  "break"))
        return TOK_BREAK;
    if (!strcmp(literal(),  "continue"))
        return TOK_CONTINUE;
    if (!strcmp(literal(),  "forward"))
        return TOK_FORWARD;
    if (!strcmp(literal(),  "public"))
        return TOK_PUBLIC;
    if (!strcmp(literal(),  "const"))
        return TOK_CONST;
    if (!strcmp(literal(),  "native"))
        return TOK_NATIVE;
    if (!strcmp(literal(),  "stock"))
        return TOK_STOCK;
    return (c == ':') ? TOK_LABEL : TOK_NAME;
}

TokenKind
Scanner::next()
{
    assert(!pushed_);

    for (;;) {
        begin_ = cur_;

        char c = getChar();
        switch (c) {
          case '/':
          {
            c = getChar();
            if (c == '/') {
                eatSingleLine();
                break;
            }
            if (c == '*') {
                if (!eatComment()) {
                    cc_.reportError(begin(), Message_UnterminatedComment);
                    return TOK_ERROR;
                }
                break;
            }
            putChar(c);
            return TOK_SLASH;
          }

          case '\n':
          case '\r':
            calcNewLine(c);
            break;

          case ';': return TOK_SEMICOLON;
          case '{': return TOK_LBRACE;
          case '}': return TOK_RBRACE;
          case '(': return TOK_LPAREN;
          case ')': return TOK_RPAREN;
          case '[': return TOK_LBRACKET;
          case ']': return TOK_RBRACKET;
          case '~': return TOK_TILDE;
          case '?': return TOK_QMARK;
          case ':': return TOK_COLON;

          case '.':
            c = getChar();
            if (c == '.') {
                c = getChar();
                if (c == '.')
                    return TOK_ELLIPSES;
            }
            cc_.reportError(begin_, Message_UnexpectedCharacter, c, uint8(c));
            return TOK_ERROR;

          case '*':
            c = getChar();
            if (c == '=')
                return TOK_ASSIGN_MUL;
            putChar(c);
            return TOK_STAR;

          case '+':
            c = getChar();
            if (c == '=')
                return TOK_ASSIGN_ADD;
            if (c == '+')
                return TOK_INCREMENT;
            putChar(c);
            return TOK_PLUS;

          case '&':
            c = getChar();
            if (c == '=')
                return TOK_ASSIGN_BITAND;
            if (c == '&')
                return TOK_AND;
            putChar(c);
            return TOK_BITAND;

          case '|':
            c = getChar();
            if (c == '=')
                return TOK_ASSIGN_BITOR;
            if (c == '|')
                return TOK_OR;
            putChar(c);
            return TOK_BITOR;

          case '^':
            c = getChar();
            if (c == '=')
                return TOK_ASSIGN_BITXOR;
            putChar(c);
            return TOK_BITXOR;

          case '%':
            c = getChar();
            if (c == '=')
                return TOK_ASSIGN_MOD;
            putChar(c);
            return TOK_PERCENT;

          case '-':
            c = getChar();
            if (c == '=')
                return TOK_ASSIGN_SUB;
            if (c == '-')
                return TOK_DECREMENT;
            putChar(c);
            return TOK_MINUS;

          case ',': return TOK_COMMA;
          case '\0': return TOK_EOF;

          case '!':
            c = getChar();
            if (c == '=')
                return TOK_NOTEQUALS;
            putChar(c);
            return TOK_NOT;

          case '=':
            c = getChar();
            if (c == '=')
                return TOK_EQUALS;
            putChar(c);
            return TOK_ASSIGN;

          case '<':
            c = getChar();
            if (c == '=')
                return TOK_LE;
            if (c == '<') {
                c = getChar();
                if (c == '=')
                    return TOK_ASSIGN_SHL;
                putChar(c);
                return TOK_SHL;
            }
            putChar(c);
            return TOK_LT;

          case '>':
            c = getChar();
            if (c == '=')
                return TOK_GE;
            if (c == '>') {
                c = getChar();
                if (c == '>') {
                    c = getChar();
                    if (c == '=')
                        return TOK_ASSIGN_USHR;
                    putChar(c);
                    return TOK_USHR;
                }
                return TOK_SHR;
            }
            putChar(c);
            return TOK_GT;

          case ' ':
          case '\t':
          case '\f':
            break;

          case '"':
            return stringLiteral();

          default:
            if (c >= '0' && c <= '9')
                return numberLiteral(c);
            if (c == '_' || (isascii(c) && isalpha(c)))
                return identifier(c);
            cc_.reportError(begin_, Message_UnexpectedCharacter, c, uint8(c));
            return TOK_ERROR;
        }
    }
}

TokenKind
Scanner::scan()
{
    if (pushed_) {
        pushed_ = false;
        return lastToken_;
    }
    lastToken_ = next();
    return lastToken_;
}

TokenKind
Scanner::peek()
{
    if (pushed_)
        return lastToken_;
    scan();
    pushed_ = true;
    return lastToken_;
}
