/*
 * Copyright (C) 2011 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.doclava.apicheck;

import com.google.doclava.AnnotationInstanceInfo;
import com.google.doclava.ClassInfo;
import com.google.doclava.Converter;
import com.google.doclava.FieldInfo;
import com.google.doclava.MethodInfo;
import com.google.doclava.PackageInfo;
import com.google.doclava.ParameterInfo;
import com.google.doclava.SourcePositionInfo;
import com.google.doclava.TypeInfo;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;

public class ApiFile {

  public static ApiInfo parseApi(String filename, InputStream stream) throws ApiParseException {
    final int CHUNK = 1024*1024;
    int hint = 0;
    try {
      hint = stream.available() + CHUNK;
    } catch (IOException ex) {
    }
    if (hint < CHUNK) {
      hint = CHUNK;
    }
    byte[] buf = new byte[hint];
    int size = 0;

    try {
      while (true) {
        if (size == buf.length) {
          byte[] tmp = new byte[buf.length+CHUNK];
          System.arraycopy(buf, 0, tmp, 0, buf.length);
          buf = tmp;
        }
        int amt = stream.read(buf, size, (buf.length-size));
        if (amt < 0) {
          break;
        } else {
          size += amt;
        }
      }
    } catch (IOException ex) {
      throw new ApiParseException("Error reading API file", ex);
    }

    final Tokenizer tokenizer = new Tokenizer(filename, (new String(buf, 0, size)).toCharArray());
    final ApiInfo api = new ApiInfo();

    while (true) {
      String token = tokenizer.getToken();
      if (token == null) {
        break;
      }
      if ("package".equals(token)) {
        parsePackage(api, tokenizer);
      } else {
        throw new ApiParseException("expected package got " + token, tokenizer.getLine());
      }
    }

    api.resolveSuperclasses();
    api.resolveInterfaces();

    return api;
  }

  private static void parsePackage(ApiInfo api, Tokenizer tokenizer)
      throws ApiParseException {
    String token;
    String name;
    PackageInfo pkg;

    token = tokenizer.requireToken();
    assertIdent(tokenizer, token);
    name = token;
    pkg = new PackageInfo(name, tokenizer.pos());
    token = tokenizer.requireToken();
    if (!"{".equals(token)) {
      throw new ApiParseException("expected '{' got " + token, tokenizer.getLine());
    }
    while (true) {
      token = tokenizer.requireToken();
      if ("}".equals(token)) {
        break;
      } else {
        parseClass(api, pkg, tokenizer, token);
      }
    }
    api.addPackage(pkg);
  }

  private static void parseClass(ApiInfo api, PackageInfo pkg, Tokenizer tokenizer, String token)
      throws ApiParseException {
    boolean pub = false;
    boolean prot = false;
    boolean priv = false;
    boolean pkgpriv = false;
    boolean stat = false;
    boolean fin = false;
    boolean abs = false;
    boolean dep = false;
    boolean iface;
    String name;
    String qname;
    String ext = null;
    ClassInfo cl;

    if ("public".equals(token)) {
      pub = true;
      token = tokenizer.requireToken();
    } else if ("protected".equals(token)) {
      prot = true;
      token = tokenizer.requireToken();
    } else if ("private".equals(token)) {
      priv = true;
      token = tokenizer.requireToken();
    } else {
      pkgpriv = true;
    }
    if ("static".equals(token)) {
      stat = true;
      token = tokenizer.requireToken();
    }
    if ("final".equals(token)) {
      fin = true;
      token = tokenizer.requireToken();
    }
    if ("abstract".equals(token)) {
      abs = true;
      token = tokenizer.requireToken();
    }
    if ("deprecated".equals(token)) {
      dep = true;
      token = tokenizer.requireToken();
    }
    if ("class".equals(token)) {
      iface = false;
      token = tokenizer.requireToken();
    } else if ("interface".equals(token)) {
      iface = true;
      token = tokenizer.requireToken();
    } else {
      throw new ApiParseException("missing class or interface. got: " + token, tokenizer.getLine());
    }
    assertIdent(tokenizer, token);
    name = token;
    qname = qualifiedName(pkg.name(), name, null);
    final TypeInfo typeInfo = Converter.obtainTypeFromString(qname);
    // Simple type info excludes the package name (but includes enclosing class names)
    final TypeInfo simpleTypeInfo = Converter.obtainTypeFromString(name);
    token = tokenizer.requireToken();
    cl = new ClassInfo(null/*classDoc*/, ""/*rawCommentText*/, tokenizer.pos(), pub, prot,
        pkgpriv, priv, stat, iface, abs, true/*isOrdinaryClass*/,
        false/*isException*/, false/*isError*/, false/*isEnum*/, false/*isAnnotation*/,
        fin, false/*isIncluded*/, simpleTypeInfo.qualifiedTypeName(), typeInfo.qualifiedTypeName(),
        null/*qualifiedTypeName*/, false/*isPrimitive*/);
    cl.setTypeInfo(typeInfo);
    cl.setDeprecated(dep);
    if ("extends".equals(token)) {
      token = tokenizer.requireToken();
      assertIdent(tokenizer, token);
      ext = token;
      token = tokenizer.requireToken();
    }
    // Resolve superclass after done parsing
    api.mapClassToSuper(cl, ext);
    cl.setAnnotations(new ArrayList<AnnotationInstanceInfo>());
    if ("implements".equals(token)) {
      while (true) {
        token = tokenizer.requireToken();
        if ("{".equals(token)) {
          break;
        } else {
          /// TODO
          if (!",".equals(token)) {
            api.mapClassToInterface(cl, token);
          }
        }
      }
    }
    if (!"{".equals(token)) {
      throw new ApiParseException("expected {", tokenizer.getLine());
    }
    token = tokenizer.requireToken();
    while (true) {
      if ("}".equals(token)) {
        break;
      } else if ("ctor".equals(token)) {
        token = tokenizer.requireToken();
        parseConstructor(tokenizer, cl, token);
      } else if ("method".equals(token)) {
        token = tokenizer.requireToken();
        parseMethod(tokenizer, cl, token);
      } else if ("field".equals(token)) {
        token = tokenizer.requireToken();
        parseField(tokenizer, cl, token, false);
      } else if ("enum_constant".equals(token)) {
        token = tokenizer.requireToken();
        parseField(tokenizer, cl, token, true);
      } else {
        throw new ApiParseException("expected ctor, enum_constant, field or method", tokenizer.getLine());
      }
      token = tokenizer.requireToken();
    }
    pkg.addClass(cl);
  }
  
  private static void parseConstructor(Tokenizer tokenizer, ClassInfo cl, String token)
      throws ApiParseException {
    boolean pub = false;
    boolean prot = false;
    boolean priv = false;
    boolean pkgpriv = false;
    boolean dep = false;
    String name;
    MethodInfo method;

    if ("public".equals(token)) {
      pub = true;
      token = tokenizer.requireToken();
    } else if ("protected".equals(token)) {
      prot = true;
      token = tokenizer.requireToken();
    } else if ("private".equals(token)) {
      priv = true;
      token = tokenizer.requireToken();
    } else {
      pkgpriv = true;
    }
    if ("deprecated".equals(token)) {
      dep = true;
      token = tokenizer.requireToken();
    }
    assertIdent(tokenizer, token);
    name = token;
    token = tokenizer.requireToken();
    if (!"(".equals(token)) {
      throw new ApiParseException("expected (", tokenizer.getLine());
    }
    //method = new MethodInfo(name, cl.qualifiedName(), false/*static*/, false/*final*/, dep,
    //    pub ? "public" : "protected", tokenizer.pos(), cl);
    method = new MethodInfo(""/*rawCommentText*/, new ArrayList<TypeInfo>()/*typeParameters*/,
        name, null/*signature*/, cl, cl, pub, prot, pkgpriv, priv, false/*isFinal*/,
        false/*isStatic*/, false/*isSynthetic*/, false/*isAbstract*/, false/*isSynthetic*/,
        false/*isNative*/, false/* isDefault */,
        false /*isAnnotationElement*/, "constructor", null/*flatSignature*/,
        null/*overriddenMethod*/, cl.asTypeInfo(), new ArrayList<ParameterInfo>(),
        new ArrayList<ClassInfo>()/*thrownExceptions*/, tokenizer.pos(),
        new ArrayList<AnnotationInstanceInfo>()/*annotations*/);
    method.setDeprecated(dep);
    token = tokenizer.requireToken();
    parseParameterList(tokenizer, method, new HashSet<String>(), token);
    token = tokenizer.requireToken();
    if ("throws".equals(token)) {
      token = parseThrows(tokenizer, method);
    }
    if (!";".equals(token)) {
      throw new ApiParseException("expected ; found " + token, tokenizer.getLine());
    }
    cl.addConstructor(method);
  }

  private static void parseMethod(Tokenizer tokenizer, ClassInfo cl, String token)
      throws ApiParseException {
    boolean pub = false;
    boolean prot = false;
    boolean priv = false;
    boolean pkgpriv = false;
    boolean stat = false;
    boolean fin = false;
    boolean abs = false;
    boolean dep = false;
    boolean syn = false;
    boolean def = false;
    ArrayList<TypeInfo> typeParameters = new ArrayList<>();
    TypeInfo returnType;
    HashSet<String> typeVariableNames;
    String name;
    String ext = null;
    MethodInfo method;

    if ("public".equals(token)) {
      pub = true;
      token = tokenizer.requireToken();
    } else if ("protected".equals(token)) {
      prot = true;
      token = tokenizer.requireToken();
    } else if ("private".equals(token)) {
      priv = true;
      token = tokenizer.requireToken();
    } else {
      pkgpriv = true;
    }
    if ("default".equals(token)) {
      def = true;
      token = tokenizer.requireToken();
    }
    if ("static".equals(token)) {
      stat = true;
      token = tokenizer.requireToken();
    }
    if ("final".equals(token)) {
      fin = true;
      token = tokenizer.requireToken();
    }
    if ("abstract".equals(token)) {
      abs = true;
      token = tokenizer.requireToken();
    }
    if ("deprecated".equals(token)) {
      dep = true;
      token = tokenizer.requireToken();
    }
    if ("synchronized".equals(token)) {
      syn = true;
      token = tokenizer.requireToken();
    }
    if ("<".equals(token)) {
      parseTypeParameterList(tokenizer, typeParameters, cl);
      token = tokenizer.requireToken();
    }
    assertIdent(tokenizer, token);
    returnType = Converter.obtainTypeFromString(token);
    typeVariableNames = TypeInfo.typeVariables(typeParameters);
    if (typeVariableNames.contains(returnType.qualifiedTypeName())) {
      returnType.setIsTypeVariable(true);
    }
    token = tokenizer.requireToken();
    assertIdent(tokenizer, token);
    name = token;
    method = new MethodInfo(""/*rawCommentText*/, typeParameters, name, null/*signature*/, cl, cl,
        pub, prot, pkgpriv, priv, fin, stat, false/*isSynthetic*/, abs/*isAbstract*/,
        syn, false/*isNative*/, def/*isDefault*/, false /*isAnnotationElement*/, "method",
        null/*flatSignature*/, null/*overriddenMethod*/, returnType,
        new ArrayList<ParameterInfo>(), new ArrayList<ClassInfo>()/*thrownExceptions*/,
        tokenizer.pos(), new ArrayList<AnnotationInstanceInfo>()/*annotations*/);
    method.setDeprecated(dep);
    token = tokenizer.requireToken();
    if (!"(".equals(token)) {
      throw new ApiParseException("expected (", tokenizer.getLine());
    }
    token = tokenizer.requireToken();
    parseParameterList(tokenizer, method, typeVariableNames, token);
    token = tokenizer.requireToken();
    if ("throws".equals(token)) {
      token = parseThrows(tokenizer, method);
    }
    if (!";".equals(token)) {
      throw new ApiParseException("expected ; found " + token, tokenizer.getLine());
    }
    cl.addMethod(method);
  }

  private static void parseField(Tokenizer tokenizer, ClassInfo cl, String token, boolean isEnum)
      throws ApiParseException {
    boolean pub = false;
    boolean prot = false;
    boolean priv = false;
    boolean pkgpriv = false;
    boolean stat = false;
    boolean fin = false;
    boolean dep = false;
    boolean trans = false;
    boolean vol = false;
    String type;
    String name;
    String val = null;
    Object v;
    FieldInfo field;

    if ("public".equals(token)) {
      pub = true;
      token = tokenizer.requireToken();
    } else if ("protected".equals(token)) {
      prot = true;
      token = tokenizer.requireToken();
    } else if ("private".equals(token)) {
      priv = true;
      token = tokenizer.requireToken();
    } else {
      pkgpriv = true;
    }
    if ("static".equals(token)) {
      stat = true;
      token = tokenizer.requireToken();
    }
    if ("final".equals(token)) {
      fin = true;
      token = tokenizer.requireToken();
    }
    if ("deprecated".equals(token)) {
      dep = true;
      token = tokenizer.requireToken();
    }
    if ("transient".equals(token)) {
      trans = true;
      token = tokenizer.requireToken();
    }
    if ("volatile".equals(token)) {
      vol = true;
      token = tokenizer.requireToken();
    }
    assertIdent(tokenizer, token);
    type = token;
    token = tokenizer.requireToken();
    assertIdent(tokenizer, token);
    name = token;
    token = tokenizer.requireToken();
    if ("=".equals(token)) {
      token = tokenizer.requireToken(false);
      val = token;
      token = tokenizer.requireToken();
    }
    if (!";".equals(token)) {
      throw new ApiParseException("expected ; found " + token, tokenizer.getLine());
    }
    try {
      v = parseValue(type, val);
    } catch (ApiParseException ex) {
      ex.line = tokenizer.getLine();
      throw ex;
    }
    field = new FieldInfo(name, cl, cl, pub, prot, pkgpriv, priv, fin, stat,
        trans, vol, false, Converter.obtainTypeFromString(type), "", v, tokenizer.pos(),
        new ArrayList<AnnotationInstanceInfo>());
    field.setDeprecated(dep);
    if (isEnum) {
      cl.addEnumConstant(field);
    } else {
      cl.addField(field);
    }
  }

  public static Object parseValue(String type, String val) throws ApiParseException {
    if (val != null) {
      if ("boolean".equals(type)) {
        return "true".equals(val) ? Boolean.TRUE : Boolean.FALSE;
      } else if ("byte".equals(type)) {
        return Integer.valueOf(val);
      } else if ("short".equals(type)) {
        return Integer.valueOf(val);
      } else if ("int".equals(type)) {
        return Integer.valueOf(val);
      } else if ("long".equals(type)) {
        return Long.valueOf(val.substring(0, val.length()-1));
      } else if ("float".equals(type)) {
        if ("(1.0f/0.0f)".equals(val) || "(1.0f / 0.0f)".equals(val)) {
          return Float.POSITIVE_INFINITY;
        } else if ("(-1.0f/0.0f)".equals(val) || "(-1.0f / 0.0f)".equals(val)) {
          return Float.NEGATIVE_INFINITY;
        } else if ("(0.0f/0.0f)".equals(val) || "(0.0f / 0.0f)".equals(val)) {
          return Float.NaN;
        } else {
          return Float.valueOf(val);
        }
      } else if ("double".equals(type)) {
        if ("(1.0/0.0)".equals(val) || "(1.0 / 0.0)".equals(val)) {
          return Double.POSITIVE_INFINITY;
        } else if ("(-1.0/0.0)".equals(val) || "(-1.0 / 0.0)".equals(val)) {
          return Double.NEGATIVE_INFINITY;
        } else if ("(0.0/0.0)".equals(val) || "(0.0 / 0.0)".equals(val)) {
          return Double.NaN;
        } else {
          return Double.valueOf(val);
        }
      } else if ("char".equals(type)) {
        return new Integer((char)Integer.parseInt(val));
      } else if ("java.lang.String".equals(type)) {
        if ("null".equals(val)) {
          return null;
        } else {
          return FieldInfo.javaUnescapeString(val.substring(1, val.length()-1));
        }
      }
    }
    if ("null".equals(val)) {
      return null;
    } else {
      return val;
    }
  }

  private static void parseTypeParameterList(Tokenizer tokenizer,
      List<TypeInfo> methodTypeParameters, ClassInfo cl) throws ApiParseException {
    String token;
    HashSet<String> variables = cl.typeVariables();
    do {
      token = tokenizer.requireToken();
      assertIdent(tokenizer, token);
      TypeInfo type = new TypeInfo(token);
      type.setIsTypeVariable(true);
      variables.add(type.qualifiedTypeName());
      ArrayList<TypeInfo> extendsBounds = new ArrayList<>();
      token = tokenizer.requireToken();
      if ("extends".equals(token)) {
        do {
          token = tokenizer.requireToken();
          assertIdent(tokenizer, token);
          extendsBounds.add(new TypeInfo(token));
          token = tokenizer.requireToken();
        } while ("&".equals(token));
      }
      if (!extendsBounds.isEmpty()) {
        type.setBounds(null, extendsBounds);
      }
      methodTypeParameters.add(type);
    } while (",".equals(token));

    // Type variables aren't guaranteed to be declared before they're referenced so we need to wait
    // until after we've processed them all to figure out which ones are type variables and which
    // ones are classes (which we may not have processed yet either).
    for (TypeInfo type : methodTypeParameters) {
      type.resolveTypeVariables(variables);
    }

    if (!">".equals(token)) {
      throw new ApiParseException("Expected '>' to end type parameter list, found "
          + token, tokenizer.getLine());
    }
  }

  private static void parseParameterList(Tokenizer tokenizer, AbstractMethodInfo method,
      HashSet<String> typeParameters, String token) throws ApiParseException {
    while (true) {
      if (")".equals(token)) {
        return;
      }

      String type = token;
      String name = null;
      token = tokenizer.requireToken();
      if (isIdent(token)) {
        name = token;
        token = tokenizer.requireToken();
      }
      if (",".equals(token)) {
        token = tokenizer.requireToken();
      } else if (")".equals(token)) {
      } else {
        throw new ApiParseException("expected , found " + token, tokenizer.getLine());
      }
      // api file does not preserve annotations.
      List<AnnotationInstanceInfo> annotations = Collections.emptyList();
      TypeInfo typeInfo = Converter.obtainTypeFromString(type);
      if (typeParameters.contains(typeInfo.qualifiedTypeName())) {
        typeInfo.setIsTypeVariable(true);
      }
      method.addParameter(new ParameterInfo(name, type,
            typeInfo,
            type.endsWith("..."),
            tokenizer.pos(),
            annotations));
      if (type.endsWith("...")) {
        method.setVarargs(true);
      }
    }
  }

  private static String parseThrows(Tokenizer tokenizer, AbstractMethodInfo method)
      throws ApiParseException {
    String token = tokenizer.requireToken();
    boolean comma = true;
    while (true) {
      if (";".equals(token)) {
        return token;
      } else if (",".equals(token)) {
        if (comma) {
          throw new ApiParseException("Expected exception, got ','", tokenizer.getLine());
        }
        comma = true;
      } else {
        if (!comma) {
          throw new ApiParseException("Expected ',' or ';' got " + token, tokenizer.getLine());
        }
        comma = false;
        method.addException(token);
      }
      token = tokenizer.requireToken();
    }
  }

  private static String qualifiedName(String pkg, String className, ClassInfo parent) {
    String parentQName = (parent != null) ? (parent.qualifiedName() + ".") : "";
    return pkg + "." + parentQName + className;
  }

  public static boolean isIdent(String token) {
    return isident(token.charAt(0));
  }

  public static void assertIdent(Tokenizer tokenizer, String token) throws ApiParseException {
    if (!isident(token.charAt(0))) {
      throw new ApiParseException("Expected identifier: " + token, tokenizer.getLine());
    }
  }
  
  static class Tokenizer {
    char[] mBuf;
    String mFilename;
    int mPos;
    int mLine = 1;
    Tokenizer(String filename, char[] buf) {
      mFilename = filename;
      mBuf = buf;
    }

    public SourcePositionInfo pos() {
      return new SourcePositionInfo(mFilename, mLine, 0);
    }

    public int getLine() {
      return mLine;
    }

    boolean eatWhitespace() {
      boolean ate = false;
      while (mPos < mBuf.length && isspace(mBuf[mPos])) {
        if (mBuf[mPos] == '\n') {
          mLine++;
        }
        mPos++;
        ate = true;
      }
      return ate;
    }

    boolean eatComment() {
      if (mPos+1 < mBuf.length) {
        if (mBuf[mPos] == '/' && mBuf[mPos+1] == '/') {
          mPos += 2;
          while (mPos < mBuf.length && !isnewline(mBuf[mPos])) {
            mPos++;
          }
          return true;
        }
      }
      return false;
    }

    void eatWhitespaceAndComments() {
      while (eatWhitespace() || eatComment()) {
      }
    }

    public String requireToken() throws ApiParseException {
      return requireToken(true);
    }

    public String requireToken(boolean parenIsSep) throws ApiParseException {
      final String token = getToken(parenIsSep);
      if (token != null) {
        return token;
      } else {
        throw new ApiParseException("Unexpected end of file", mLine);
      }
    }

    public String getToken() throws ApiParseException {
      return getToken(true);
    }

    public String getToken(boolean parenIsSep) throws ApiParseException {
      eatWhitespaceAndComments();
      if (mPos >= mBuf.length) {
        return null;
      }
      final int line = mLine;
      final char c = mBuf[mPos];
      final int start = mPos;
      mPos++;
      if (c == '"') {
        final int STATE_BEGIN = 0;
        final int STATE_ESCAPE = 1;
        int state = STATE_BEGIN;
        while (true) {
          if (mPos >= mBuf.length) {
            throw new ApiParseException("Unexpected end of file for \" starting at " + line, mLine);
          }
          final char k = mBuf[mPos];
          if (k == '\n' || k == '\r') {
            throw new ApiParseException("Unexpected newline for \" starting at " + line, mLine);
          }
          mPos++;
          switch (state) {
            case STATE_BEGIN:
              switch (k) {
                case '\\':
                  state = STATE_ESCAPE;
                  mPos++;
                  break;
                case '"':
                  return new String(mBuf, start, mPos-start);
              }
            case STATE_ESCAPE:
              state = STATE_BEGIN;
              break;
          }
        }
      } else if (issep(c, parenIsSep)) {
        return "" + c;
      } else {
        int genericDepth = 0;
        do {
          while (mPos < mBuf.length && !isspace(mBuf[mPos]) && !issep(mBuf[mPos], parenIsSep)) {
            mPos++;
          }
          if (mPos < mBuf.length) {
            if (mBuf[mPos] == '<') {
              genericDepth++;
              mPos++;
            } else if (genericDepth != 0) {
              if (mBuf[mPos] == '>') {
                genericDepth--;
              }
              mPos++;
            }
          }
        } while (mPos < mBuf.length
            && ((!isspace(mBuf[mPos]) && !issep(mBuf[mPos], parenIsSep)) || genericDepth != 0));
        if (mPos >= mBuf.length) {
          throw new ApiParseException("Unexpected end of file for \" starting at " + line, mLine);
        }
        return new String(mBuf, start, mPos-start);
      }
    }
  }

  static boolean isspace(char c) {
    return c == ' ' || c == '\t' || c == '\n' || c == '\r';
  }

  static boolean isnewline(char c) {
    return c == '\n' || c == '\r';
  }

  static boolean issep(char c, boolean parenIsSep) {
    if (parenIsSep) {
      if (c == '(' || c == ')') {
        return true;
      }
    }
    return c == '{' || c == '}' || c == ',' || c == ';' || c == '<' || c == '>';
  }

  static boolean isident(char c) {
    if (c == '"' || issep(c, true)) {
      return false;
    }
    return true;
  }
}
