/*
 * Copyright 2008 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.gwt.dev.javac;

import com.google.gwt.core.client.GwtScriptOnly;
import com.google.gwt.core.ext.TreeLogger.HelpInfo;
import com.google.gwt.dev.jjs.CorrelationFactory;
import com.google.gwt.dev.jjs.InternalCompilerException;
import com.google.gwt.dev.jjs.SourceInfo;
import com.google.gwt.dev.jjs.SourceOrigin;
import com.google.gwt.dev.js.JsParser;
import com.google.gwt.dev.js.JsParserException;
import com.google.gwt.dev.js.JsParserException.SourceDetail;
import com.google.gwt.dev.js.ast.JsExprStmt;
import com.google.gwt.dev.js.ast.JsFunction;
import com.google.gwt.dev.js.ast.JsParameter;
import com.google.gwt.dev.js.ast.JsScope;
import com.google.gwt.dev.js.ast.JsStatement;
import com.google.gwt.dev.util.collect.IdentityHashMap;
import com.google.gwt.dev.util.collect.IdentityMaps;

import org.eclipse.jdt.core.compiler.CharOperation;
import org.eclipse.jdt.internal.compiler.CompilationResult;
import org.eclipse.jdt.internal.compiler.ast.AbstractMethodDeclaration;
import org.eclipse.jdt.internal.compiler.ast.Annotation;
import org.eclipse.jdt.internal.compiler.ast.Argument;
import org.eclipse.jdt.internal.compiler.ast.CompilationUnitDeclaration;
import org.eclipse.jdt.internal.compiler.ast.MethodDeclaration;
import org.eclipse.jdt.internal.compiler.ast.TypeDeclaration;
import org.eclipse.jdt.internal.compiler.lookup.ReferenceBinding;
import org.eclipse.jdt.internal.compiler.problem.ProblemSeverities;
import org.eclipse.jdt.internal.compiler.util.Util;

import java.io.IOException;
import java.io.Serializable;
import java.io.StringReader;
import java.util.List;
import java.util.Map;

/**
 * Adapts compilation units containing JSNI-accessible code by rewriting the
 * source.
 */
public class JsniMethodCollector {

  private static final class JsniMethodImpl extends JsniMethod implements
      Serializable {
    private final JsFunction func;
    private boolean isScriptOnly;
    private final String name;

    public JsniMethodImpl(String name, JsFunction func, boolean isScriptOnly) {
      this.name = name;
      this.func = func;
      this.isScriptOnly = isScriptOnly;
    }

    @Override
    public JsFunction function() {
      return func;
    }

    @Override
    public boolean isScriptOnly() {
      return isScriptOnly;
    }

    @Override
    public int line() {
      return func.getSourceInfo().getStartLine();
    }

    @Override
    public String location() {
      return func.getSourceInfo().getFileName();
    }

    @Override
    public String name() {
      return name;
    }

    @Override
    public String[] paramNames() {
      List<JsParameter> params = func.getParameters();
      String[] result = new String[params.size()];
      for (int i = 0; i < result.length; ++i) {
        result[i] = params.get(i).getName().getIdent();
      }
      return result;
    }

    @Override
    public String toString() {
      return func.toString();
    }
  }

  private static class Visitor extends MethodVisitor {
    private static boolean isScriptOnly(AbstractMethodDeclaration method) {
      if (method.annotations == null) {
        return false;
      }
      for (Annotation a : method.annotations) {
        ReferenceBinding binding = (ReferenceBinding) a.resolvedType;
        String name = CharOperation.toString(binding.compoundName);
        if (name.equals(GwtScriptOnly.class.getName())) {
          return true;
        }
      }
      return false;
    }

    private final CorrelationFactory correlator;
    private final Map<MethodDeclaration, JsniMethod> jsniMethods;
    private final JsScope scope;
    private final String source;
    private SourceInfo cudInfo;

    public Visitor(String source, JsScope scope, CorrelationFactory correlator,
        Map<MethodDeclaration, JsniMethod> jsniMethods) {
      this.jsniMethods = jsniMethods;
      this.source = source;
      this.scope = scope;
      this.correlator = correlator;
    }

    @Override
    public void collect(CompilationUnitDeclaration cud, String sourceMapPath) {
      cudInfo = correlator.makeSourceInfo(SourceOrigin.create(0, sourceMapPath));
      super.collect(cud, sourceMapPath);
    }

    @Override
    protected boolean interestingMethod(AbstractMethodDeclaration method) {
      return method.isNative();
    }

    @Override
    protected void processMethod(TypeDeclaration typeDecl,
        AbstractMethodDeclaration method, String enclosingType) {
      JsFunction jsFunction = parseJsniFunction(method, source, enclosingType,
          cudInfo, scope);
      if (jsFunction != null) {
        String jsniSignature = getJsniSignature(enclosingType, method);
        jsniMethods.put((MethodDeclaration) method, new JsniMethodImpl(
            jsniSignature, jsFunction, isScriptOnly(method)));
      }
    }
  }

  public static Map<MethodDeclaration, JsniMethod> collectJsniMethods(
      CompilationUnitDeclaration cud, String sourceMapPath,
      String source, JsScope scope,
      CorrelationFactory correlator) {
    Map<MethodDeclaration, JsniMethod> jsniMethods = new IdentityHashMap<MethodDeclaration, JsniMethod>();
    new Visitor(source, scope, correlator, jsniMethods).collect(cud, sourceMapPath);
    return IdentityMaps.normalizeUnmodifiable(jsniMethods);
  }

  public static JsFunction parseJsniFunction(AbstractMethodDeclaration method,
      String unitSource, String enclosingType, SourceInfo baseInfo,
      JsScope scope) {
    CompilationResult compResult = method.compilationResult;
    int[] indexes = compResult.lineSeparatorPositions;
    int startLine = Util.getLineNumber(method.sourceStart, indexes, 0,
        indexes.length - 1);
    SourceInfo info = baseInfo.makeChild(SourceOrigin.create(
        method.sourceStart, method.bodyEnd, startLine, baseInfo.getFileName()));

    // Handle JSNI block
    String jsniCode = unitSource
        .substring(method.bodyStart, method.bodyEnd + 1);
    int startPos = jsniCode.indexOf("/*-{");
    int endPos = jsniCode.lastIndexOf("}-*/");
    if (startPos < 0 && endPos < 0) {
      return null;
    }
    if (startPos < 0) {
      reportJsniError(info, method,
          "Unable to find start of native block; begin your JavaScript block with: /*-{");
      return null;
    }
    if (endPos < 0) {
      reportJsniError(
          info,
          method,
          "Unable to find end of native block; terminate your JavaScript block with: }-*/");
      return null;
    }

    startPos += 3; // move up to open brace
    endPos += 1; // move past close brace

    jsniCode = jsniCode.substring(startPos, endPos);

    // Here we parse it as an anonymous function, but we will give it a
    // name later when we generate the JavaScript during code generation.
    //
    StringBuilder functionSource = new StringBuilder("function (");
    boolean first = true;
    if (method.arguments != null) {
      for (Argument arg : method.arguments) {
        if (first) {
          first = false;
        } else {
          functionSource.append(',');
        }
        functionSource.append(arg.binding.name);
      }
    }
    functionSource.append(") ");
    int functionHeaderLength = functionSource.length();
    functionSource.append(jsniCode);
    StringReader sr = new StringReader(functionSource.toString());

    // Absolute start and end position of braces in original source.
    int absoluteJsStartPos = method.bodyStart + startPos;
    int absoluteJsEndPos = absoluteJsStartPos + jsniCode.length();

    // Adjust the points the JS parser sees to account for the synth header.
    int jsStartPos = absoluteJsStartPos - functionHeaderLength;
    int jsEndPos = absoluteJsEndPos - functionHeaderLength;

    // To compute the start line, count lines from point to point.
    int jsLine = info.getStartLine()
        + countLines(indexes, info.getStartPos(), absoluteJsStartPos);

    SourceInfo jsInfo = baseInfo.makeChild(SourceOrigin.create(jsStartPos,
        jsEndPos, jsLine, baseInfo.getFileName()));
    try {
      List<JsStatement> result = JsParser.parse(jsInfo, scope, sr);
      JsExprStmt jsExprStmt = (JsExprStmt) result.get(0);
      return (JsFunction) jsExprStmt.getExpression();
    } catch (IOException e) {
      throw new InternalCompilerException("Internal error parsing JSNI in '"
          + enclosingType + '.' + method.toString() + '\'', e);
    } catch (JsParserException e) {
      int problemCharPos = computeAbsoluteProblemPosition(indexes, e
          .getSourceDetail());
      SourceInfo errorInfo = SourceOrigin.create(problemCharPos,
          problemCharPos, e.getSourceDetail().getLine(), info.getFileName());
      // Strip the file/line header because reportJsniError will add that.
      String msg = e.getMessage();
      int pos = msg.indexOf(": ");
      msg = msg.substring(pos + 2);
      reportJsniError(errorInfo, method, msg);
      return null;
    }
  }

  public static void reportJsniError(SourceInfo info,
      AbstractMethodDeclaration method, String msg) {
    reportJsniProblem(info, method, msg, ProblemSeverities.Error);
  }

  public static void reportJsniWarning(SourceInfo info,
      MethodDeclaration method, String msg) {
    reportJsniProblem(info, method, msg, ProblemSeverities.Warning);
  }

  /**
   * JS reports the error as a line number, to find the absolute position in the
   * real source stream, we have to walk from the absolute JS start position
   * until we have counted down enough lines. Then we use the column position to
   * find the exact spot.
   */
  private static int computeAbsoluteProblemPosition(int[] indexes,
      SourceDetail detail) {
    // Convert 1-based to -1 - based.
    int line = detail.getLine() - 1;
    if (line == 0) {
      return detail.getLineOffset() - 1;
    }

    int result = indexes[line - 1] + detail.getLineOffset();
    /*
     * In other words, make sure our result is actually on this line (less than
     * the start position of the next line), but make sure we don't overflow if
     * this is the last line in the file.
     */
    assert line >= indexes.length || result < indexes[line];
    return result;
  }

  private static int countLines(int[] indexes, int p1, int p2) {
    assert p1 >= 0;
    assert p2 >= 0;
    assert p1 <= p2;
    int p1line = findLine(p1, indexes, 0, indexes.length);
    int p2line = findLine(p2, indexes, 0, indexes.length);
    return p2line - p1line;
  }

  private static int findLine(int pos, int[] indexes, int lo, int tooHi) {
    assert (lo < tooHi);
    if (lo == tooHi - 1) {
      return lo;
    }
    int mid = lo + (tooHi - lo) / 2;
    assert (lo < mid);
    if (pos < indexes[mid]) {
      return findLine(pos, indexes, lo, mid);
    } else {
      return findLine(pos, indexes, mid, tooHi);
    }
  }

  /**
   * Gets a unique name for this method and its signature (this is used to
   * determine whether one method overrides another).
   */
  private static String getJsniSignature(String enclosingType,
      AbstractMethodDeclaration method) {
    return '@' + enclosingType + "::"
        + MethodVisitor.getMemberSignature(method);
  }

  private static void reportJsniProblem(SourceInfo info,
      AbstractMethodDeclaration methodDeclaration, String message,
      int problemSeverity) {
    // TODO: provide helpInfo for how to write JSNI methods?
    HelpInfo jsniHelpInfo = null;
    CompilationResult compResult = methodDeclaration.compilationResult();
    // recalculate startColumn, because SourceInfo does not hold it
    int startColumn = Util.searchColumnNumber(compResult
        .getLineSeparatorPositions(), info.getStartLine(), info.getStartPos());
    GWTProblem.recordProblem(info, startColumn, compResult, message,
        jsniHelpInfo, problemSeverity);
  }

  private JsniMethodCollector() {
  }
}
