/*
 * Copyright 2013 Google Inc. All rights reserved.
 *
 * 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.errorprone.refaster;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static java.util.logging.Level.FINE;
import static java.util.logging.Level.SEVERE;

import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableClassToInstanceMap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.errorprone.fixes.Fix;
import com.google.errorprone.fixes.SuggestedFix;

import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.JCTree.JCBlock;
import com.sun.tools.javac.tree.JCTree.JCStatement;
import com.sun.tools.javac.util.Context;
import com.sun.tools.javac.util.Warner;

import java.io.IOException;
import java.io.StringWriter;
import java.lang.annotation.Annotation;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;

import javax.annotation.Nullable;

/**
 * Template representing a sequence of consecutive statements.
 *
 * @author [email protected] (Louis Wasserman)
 */
@AutoValue
public abstract class BlockTemplate extends Template<BlockTemplateMatch> {
  private static final Logger logger = Logger.getLogger(BlockTemplate.class.toString());
  
  public static BlockTemplate create(UStatement... templateStatements) {
    return create(ImmutableMap.<String, UType>of(), templateStatements);
  }
  
  public static BlockTemplate create(
      Map<String, ? extends UType> expressionArgumentTypes, UStatement... templateStatements) {
    return create(ImmutableList.<UTypeVar>of(), expressionArgumentTypes, templateStatements);
  }
  
  public static BlockTemplate create(
      Iterable<UTypeVar> typeVariables,
      Map<String, ? extends UType> expressionArgumentTypes,
      UStatement... templateStatements) {
    return create(
        ImmutableClassToInstanceMap.<Annotation>builder().build(), 
        typeVariables, expressionArgumentTypes, ImmutableList.copyOf(templateStatements));
  }
  
  public static BlockTemplate create(
      ImmutableClassToInstanceMap<Annotation> annotations,
      Iterable<UTypeVar> typeVariables,
      Map<String, ? extends UType> expressionArgumentTypes,
      Iterable<? extends UStatement> templateStatements) {
    return new AutoValue_BlockTemplate(
        annotations,
        ImmutableList.copyOf(typeVariables),
        ImmutableMap.copyOf(expressionArgumentTypes),
        ImmutableList.copyOf(templateStatements));        
  }
  
  abstract ImmutableList<UStatement> templateStatements();      

  /**
   * If the tree is a {@link JCBlock}, returns a list of disjoint matches corresponding to
   * the exact list of template statements found consecutively; otherwise, returns an
   * empty list.
   */
  @Override
  public Iterable<BlockTemplateMatch> match(JCTree tree, Context context) {
    // TODO(lowasser): consider nonconsecutive matches?
    if (tree instanceof JCBlock) {
      JCBlock block = (JCBlock) tree;
      List<JCStatement> targetStatements = ImmutableList.copyOf(block.getStatements());
      ImmutableList.Builder<BlockTemplateMatch> builder = ImmutableList.builder();
      for (int start = 0; start + templateStatements().size() <= targetStatements.size(); 
          start++) {
        int end = start + templateStatements().size();
        Unifier unifier = match(targetStatements.subList(start, end), context);
        if (unifier != null) {
          builder.add(new BlockTemplateMatch(block, unifier, start, end));
          start = end - 1;
        }
      }
      return builder.build();
    }
    return ImmutableList.of();
  }

  @Nullable
  private Unifier match(List<JCStatement> targetStatements, Context context) {
    checkArgument(templateStatements().size() == targetStatements.size());
    Unifier unifier = new Unifier(context);
    for (int i = 0; i < templateStatements().size() && unifier != null; i++) {
      unifier = templateStatements().get(i).unify(targetStatements.get(i), unifier);
    }
    if (unifier != null) {
      Inliner inliner = unifier.createInliner();
      try {
        return typecheck(
            unifier, inliner, new Warner(targetStatements.get(0)), expectedTypes(inliner),
            actualTypes(inliner));
      } catch (CouldNotResolveImportException e) {
        logger.log(FINE, "Failure to resolve import", e);
      }
    }
    return null;
  }
  
  /**
   * Returns a {@code String} representation of a statement, including semicolon.
   */
  private static String printStatement(Context context, JCStatement statement) {
    StringWriter writer = new StringWriter();
    try {
      pretty(context, writer).printStat(statement);
    } catch (IOException e) {
      throw new AssertionError("StringWriter cannot throw IOExceptions");
    }
    return writer.toString();    
  }
  
  /**
   * Returns a {@code String} representation of a sequence of statements,
   * with semicolons and newlines.
   */
  private static String printStatements(
      Context context, com.sun.tools.javac.util.List<JCStatement> statements) {
    StringWriter writer = new StringWriter();
    try {
      pretty(context, writer).printStats(statements);
    } catch (IOException e) {
      throw new AssertionError("StringWriter cannot throw IOExceptions");
    }
    return writer.toString(); 
  }
  
  @Override
  public Fix replace(BlockTemplateMatch match) {
    checkNotNull(match);
    SuggestedFix.Builder fix = SuggestedFix.builder();
    Inliner inliner = match.createInliner();
    Context context = inliner.getContext();
    List<JCStatement> targetStatements = match.getStatements();
    try {
      int nTemplates = templateStatements().size();
      int nTargets = targetStatements.size();
      if (nTemplates <= nTargets) {
        for (int i = 0; i < nTemplates; i++) {
          fix.replace(
              targetStatements.get(i), 
              printStatement(context, templateStatements().get(i).inline(inliner)));
        }
        for (int i = templateStatements().size(); i < nTargets; i++) {
          fix.delete(targetStatements.get(i));
        }
      } else {
        for (int i = 0; i < nTargets - 1; i++) {
          fix.replace(
              targetStatements.get(i),
              printStatement(context, templateStatements().get(i).inline(inliner)));
        }
        int last = nTargets - 1;
        ImmutableList<UStatement> remainingTemplate = 
            templateStatements().subList(last, templateStatements().size());
        fix.replace(targetStatements.get(last), 
            printStatements(context, inliner.inlineList(remainingTemplate)));
      }
    } catch (CouldNotResolveImportException e) {
      logger.log(SEVERE, "Failure to resolve import in replacement", e);
    }
    return addImports(inliner, fix);
  }
}