/*
 * 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.devtools.j2objc.translate;

import com.google.common.collect.ListMultimap;
import com.google.common.collect.MultimapBuilder;
import com.google.common.collect.Sets;
import com.google.devtools.j2objc.ast.AbstractTypeDeclaration;
import com.google.devtools.j2objc.ast.Block;
import com.google.devtools.j2objc.ast.BodyDeclaration;
import com.google.devtools.j2objc.ast.Comment;
import com.google.devtools.j2objc.ast.CompilationUnit;
import com.google.devtools.j2objc.ast.EnumDeclaration;
import com.google.devtools.j2objc.ast.MethodDeclaration;
import com.google.devtools.j2objc.ast.NativeDeclaration;
import com.google.devtools.j2objc.ast.NativeStatement;
import com.google.devtools.j2objc.ast.SynchronizedStatement;
import com.google.devtools.j2objc.ast.ThisExpression;
import com.google.devtools.j2objc.ast.TreeNode;
import com.google.devtools.j2objc.ast.TreeUtil;
import com.google.devtools.j2objc.ast.TypeDeclaration;
import com.google.devtools.j2objc.ast.TypeLiteral;
import com.google.devtools.j2objc.ast.UnitTreeVisitor;
import com.google.devtools.j2objc.util.CodeReferenceMap;
import com.google.devtools.j2objc.util.ElementUtil;
import com.google.devtools.j2objc.util.ErrorUtil;
import java.lang.reflect.Modifier;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.lang.model.element.TypeElement;

/**
 * Extracts OCNI code blocks into NativeDeclaration and NativeStatement nodes.
 * Adds native fast enumeration support for Iterable types, when not already
 * defined.
 * Adds synchronized blocks to methods with the synchronized modifier.
 *
 * @author Keith Stanger
 */
public class OcniExtractor extends UnitTreeVisitor {

  private final ListMultimap<TreeNode, Comment> blockComments;
  private final CodeReferenceMap deadCodeMap;

  public OcniExtractor(CompilationUnit unit, CodeReferenceMap deadCodeMap) {
    super(unit);
    blockComments = findBlockComments(unit);
    this.deadCodeMap = deadCodeMap;
  }

  /**
   * Finds all block comments and associates them with their containing type.
   * This is trickier than you might expect because of inner types.
   */
  private static ListMultimap<TreeNode, Comment> findBlockComments(CompilationUnit unit) {
    ListMultimap<TreeNode, Comment> blockComments =
        MultimapBuilder.hashKeys().arrayListValues().build();
    for (Comment comment : unit.getCommentList()) {
      if (!comment.isBlockComment()) {
        continue;
      }
      int commentPos = comment.getStartPosition();
      AbstractTypeDeclaration containingType = null;
      int containingTypePos = -1;
      for (AbstractTypeDeclaration type : unit.getTypes()) {
        int typePos = type.getStartPosition();
        if (typePos < 0) {
          continue;
        }
        int typeEnd = typePos + type.getLength();
        if (commentPos > typePos && commentPos < typeEnd && typePos > containingTypePos) {
          containingType = type;
          containingTypePos = typePos;
        }
      }
      blockComments.put(containingType != null ? containingType : unit, comment);
    }
    return blockComments;
  }

  @Override
  public void endVisit(CompilationUnit node) {
    for (Comment comment : blockComments.get(node)) {
      NativeDeclaration nativeDecl = extractNativeDeclaration(comment);
      if (nativeDecl != null) {
        unit.addNativeBlock(nativeDecl);
      }
    }
  }

  @Override
  public void endVisit(MethodDeclaration node) {
    int modifiers = node.getModifiers();
    if (Modifier.isNative(modifiers)) {
      NativeStatement nativeStmt = extractNativeStatement(node);
      if (nativeStmt != null) {
        Block body = new Block();
        body.addStatement(nativeStmt);
        node.setBody(body);
        node.removeModifiers(Modifier.NATIVE);
      }
    }
    if (Modifier.isSynchronized(modifiers)) {
      TypeElement declaringClass = ElementUtil.getDeclaringClass(node.getExecutableElement());
      SynchronizedStatement syncStmt = new SynchronizedStatement(
          Modifier.isStatic(modifiers) ? new TypeLiteral(declaringClass.asType(), typeUtil)
          : new ThisExpression(declaringClass.asType()));
      syncStmt.setBody(TreeUtil.remove(node.getBody()));
      Block newBody = new Block();
      newBody.addStatement(syncStmt);
      node.setBody(newBody);
      node.removeModifiers(Modifier.SYNCHRONIZED);
    }
  }

  @Override
  public void endVisit(EnumDeclaration node) {
    visitType(node);
  }

  @Override
  public void endVisit(TypeDeclaration node) {
    if (!node.isInterface()) {
      visitType(node);
    }
  }

  private void visitType(AbstractTypeDeclaration node) {
    TypeElement type = node.getTypeElement();
    Set<String> methodsPrinted = Sets.newHashSet();
    List<BodyDeclaration> bodyDeclarations = node.getBodyDeclarations();
    int minPos = 0;
    int declIdx = 0;

    for (Comment comment : blockComments.get(node)) {
      int commentPos = comment.getStartPosition();
      while (declIdx < bodyDeclarations.size()) {
        BodyDeclaration decl = bodyDeclarations.get(declIdx);
        if (decl.getStartPosition() > commentPos) {
          break;
        }
        minPos = Math.max(minPos, decl.getStartPosition() + decl.getLength());
        declIdx++;
      }
      if (commentPos > minPos) {
        NativeDeclaration nativeDecl = extractNativeDeclaration(comment);
        if (nativeDecl != null) {
          findMethodSignatures(nativeDecl.getImplementationCode(), methodsPrinted);
          bodyDeclarations.add(declIdx++, nativeDecl);
        }
      }
    }

    // If the type implements Iterable and there's no existing implementation
    // for NSFastEnumeration's protocol method, then add the default
    // implementation. Don't emit this if the entire class is dead. There is
    // no need to check if any Iterable methods are dead, since ProGuard is
    // conservative -- if a class is live and implements Iterable, those
    // methods are always live.
    if (typeUtil.findSupertype(type.asType(), "java.lang.Iterable") != null
        && !methodsPrinted.contains("countByEnumeratingWithState:objects:count:")
        && (deadCodeMap == null || !deadCodeMap.containsClass(type, elementUtil))) {
      bodyDeclarations.add(NativeDeclaration.newInnerDeclaration(null,
          "- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state "
          + "objects:(__unsafe_unretained id *)stackbuf count:(NSUInteger)len {\n"
          + "  return JreDefaultFastEnumeration(self, state, stackbuf);\n}\n"));
    }
  }

  private static final String TYPE_REGEX = "\\([\\w\\s\\*<>\\[\\]]+\\)";
  private static final String PARAM_REGEX = "\\s*:\\s*" + TYPE_REGEX + "\\s*\\w+";
  private static final String ADDITIONAL_PARAM_REGEX = "\\s+(\\w+)" + PARAM_REGEX;
  private static final Pattern OBJC_METHOD_DECL_PATTERN = Pattern.compile(
      "^\\+|-\\s*" + TYPE_REGEX + "\\s*(\\w+)(" + PARAM_REGEX + "((?:" + ADDITIONAL_PARAM_REGEX
      + ")*))?\\s*\\{");
  private static final Pattern ADDITIONAL_PARAM_PATTERN = Pattern.compile(ADDITIONAL_PARAM_REGEX);

  /**
   * Finds the signatures of methods defined in the native code.
   */
  private static void findMethodSignatures(String code, Set<String> signatures) {
    if (code == null) {
      return;
    }
    Matcher matcher = OBJC_METHOD_DECL_PATTERN.matcher(code);
    while (matcher.find()) {
      StringBuilder signature = new StringBuilder();
      signature.append(matcher.group(1));
      if (matcher.group(2) != null) {
        signature.append(':');
        String additionalParams = matcher.group(3);
        if (additionalParams != null) {
          Matcher paramsMatcher = ADDITIONAL_PARAM_PATTERN.matcher(additionalParams);
          while (paramsMatcher.find()) {
            signature.append(paramsMatcher.group(1)).append(':');
          }
        }
      }
      signatures.add(signature.toString());
    }
  }

  private NativeDeclaration extractNativeDeclaration(TreeNode node) {
    OcniBlock ocniBlock = extractNativeCode(node);
    if (ocniBlock != null) {
      switch (ocniBlock.type) {
        case HEADER:
          {
            NativeDeclaration decl = NativeDeclaration.newInnerDeclaration(
                ocniBlock.code + '\n', null);
            decl.addModifiers(Modifier.PUBLIC);
            return decl;
          }
        case SOURCE: return NativeDeclaration.newInnerDeclaration(null, ocniBlock.code + '\n');
      }
    }
    return null;
  }

  private NativeStatement extractNativeStatement(TreeNode node) {
    OcniBlock ocniBlock = extractNativeCode(node);
    if (ocniBlock != null) {
      switch (ocniBlock.type) {
        case SOURCE: return new NativeStatement(ocniBlock.code);
        default: ErrorUtil.warning(node, "Unexpected OCNI type: " + ocniBlock.type);
      }
    }
    return null;
  }

  /**
   * Kinds of OCNI comments. Currently there are normal source and header types.
   */
  // TODO(kstanger): move to BlockComment, or a new OCNIComment subclass.
  public enum OcniType {
    HEADER("/*-HEADER["), SOURCE("/*-[");

    private final String delimiter;

    private OcniType(String delim) {
      delimiter = delim;
    }

    public String delimiter() {
      return delimiter;
    }

    static OcniType fromString(String type) {
      if (type.isEmpty()) {
        return SOURCE;
      }
      return valueOf(type);
    }
  }

  private static class OcniBlock {
    private final OcniType type;
    private final String code;

    private OcniBlock(OcniType type, String code) {
      this.type = type;
      this.code = code;
    }
  }

  private static final Pattern OCNI_PATTERN =
      Pattern.compile("/\\*-(\\w*)\\[(.*)\\]-\\*/", Pattern.DOTALL);

  /**
   * Returns text from within a source code range, where that text is
   * surrounded by OCNI-like tokens ("/&#42;-[" and "]-&#42;/"), warning
   * if JSNI delimiters are found instead.
   *
   * @param node The node in which to extract the native code.
   * @return the extracted text between the OCNI delimiters, or null if
   *     a pair of JSNI delimiters aren't in the specified text range
   */
  private OcniBlock extractNativeCode(TreeNode node) {
    int offset = node.getStartPosition();
    String text = unit.getSource().substring(offset, offset + node.getLength());
    Matcher m = OCNI_PATTERN.matcher(text);
    if (m.find()) {
      String typeStr = m.group(1);
      try {
        OcniType type = OcniType.fromString(typeStr);
        return new OcniBlock(type, m.group(2).trim());
      } catch (IllegalArgumentException e) {
        ErrorUtil.warning(node, "Unknown OCNI type: " + typeStr);
        return null;
      }
    }
    if (options.jsniWarnings() && hasJsni(text)) {
      ErrorUtil.warning(node, "JSNI comment found");
    }
    return null;
  }

  private boolean hasJsni(String text) {
    int start = text.indexOf("/*-[");  // start after the bracket
    int end = text.lastIndexOf("]-*/");

    if (start == -1 || end <= start) {
      start = text.indexOf("/*-{");
      end = text.lastIndexOf("}-*/");
      if (start != -1 && end > start) {
        return true;
      }
    }
    return false;
  }
}
