/*
 * 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 com.google.common.base.Optional;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.errorprone.refaster.Bindings.Key;
import com.google.errorprone.refaster.UTypeVar.TypeWithExpression;

import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.code.Symbol.ClassSymbol;
import com.sun.tools.javac.code.Symbol.TypeSymbol;
import com.sun.tools.javac.code.Symtab;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.code.Type.ArrayType;
import com.sun.tools.javac.code.Type.ClassType;
import com.sun.tools.javac.code.Type.TypeVar;
import com.sun.tools.javac.code.Type.WildcardType;
import com.sun.tools.javac.code.Types;
import com.sun.tools.javac.comp.Enter;
import com.sun.tools.javac.comp.Infer;
import com.sun.tools.javac.main.JavaCompiler;
import com.sun.tools.javac.tree.JCTree.JCExpression;
import com.sun.tools.javac.tree.TreeMaker;
import com.sun.tools.javac.util.Context;
import com.sun.tools.javac.util.List;
import com.sun.tools.javac.util.Name;
import com.sun.tools.javac.util.Names;

import java.util.Map;
import java.util.Set;

/**
 * A context representing all the dependencies necessary to reconstruct a pretty-printable source
 * tree from a {@code UTree} based on a set of substitutions.
 *
 * @author Louis Wasserman
 */
public final class Inliner {
  private final Context context;
  private final Set<String> importsToAdd;
  private final Set<String> staticImportsToAdd;
  public final Bindings bindings;
  
  private final Map<String, TypeVar> typeVarCache;
  
  public Inliner(Context context, Bindings bindings) {
    this.context = context;
    this.bindings = bindings.snapshot();
    this.importsToAdd = Sets.newHashSet();
    this.staticImportsToAdd = Sets.newHashSet();
    this.typeVarCache = Maps.newHashMap();
  }

  public void addImport(String qualifiedImport) {
    if (!qualifiedImport.startsWith("java.lang")) {
      importsToAdd.add(qualifiedImport);
    }
  }
  
  public void addStaticImport(String qualifiedImport) {
    staticImportsToAdd.add(qualifiedImport);
  }
  
  public ClassSymbol resolveClass(String qualifiedClass) throws CouldNotResolveImportException {
    Symbol symbol = JavaCompiler.instance(context).resolveIdent(qualifiedClass);
    if (symbol.equals(symtab().errSymbol) || !(symbol instanceof ClassSymbol)) {
      throw new CouldNotResolveImportException(qualifiedClass);
    } else {
      return (ClassSymbol) symbol;
    }
  }
  
  public Context getContext() {
    return context;
  }

  public Types types() {
    return Types.instance(context);
  }

  public Symtab symtab() {
    return Symtab.instance(context);
  }

  public Enter enter() {
    return Enter.instance(context);
  }

  public Names names() {
    return Names.instance(context);
  }

  public TreeMaker maker() {
    return TreeMaker.instance(context);
  }
  
  public Infer infer() {
    return Infer.instance(context);
  }
  
  public ImportPolicy importPolicy() {
    return ImportPolicy.instance(context);
  }

  public Name asName(String str) {
    return names().fromString(str);
  }
  
  private static final Types.SimpleVisitor<JCExpression, Inliner> INLINE_AS_TREE =
      new Types.SimpleVisitor<JCExpression, Inliner>() {
        @Override
        public JCExpression visitType(Type t, Inliner inliner) {
          return inliner.maker().Type(t);
        }

        @Override
        public JCExpression visitClassType(ClassType type, Inliner inliner) {
          ClassSymbol classSym = (ClassSymbol) type.tsym;
          JCExpression classExpr = inliner.importPolicy().classReference(
              inliner, classSym.outermostClass().getQualifiedName().toString(),
              classSym.getQualifiedName().toString());
          List<JCExpression> argExprs = List.nil();
          for (Type argType : type.getTypeArguments()) {
            argExprs = argExprs.append(visit(argType, inliner));
          }
          return argExprs.isEmpty()
              ? classExpr
              : inliner.maker().TypeApply(classExpr, argExprs);
        }

        @Override
        public JCExpression visitWildcardType(WildcardType type, Inliner inliner) {
          TreeMaker maker = inliner.maker();
          return maker.Wildcard(
              maker.TypeBoundKind(type.kind),
              visit(type.baseType(), inliner));
        }

        @Override
        public JCExpression visitArrayType(ArrayType type, Inliner inliner) {
          return inliner.maker().TypeArray(visit(type.getComponentType(), inliner));
        }
      };
  
  /**
   * Inlines the syntax tree representing the specified type.
   */
  public JCExpression inlineAsTree(Type type) {
    return INLINE_AS_TREE.visit(type, this);
  }

  public <V> V getBinding(Key<V> key) {
    V value = bindings.getBinding(key);
    if (value == null) {
      throw new IllegalStateException("No binding for " + key);
    }
    return value;
  }
  
  public <V> Optional<V> getOptionalBinding(Key<V> key) {
    return Optional.fromNullable(bindings.getBinding(key));
  }

  public <R, T extends Inlineable<R>> com.sun.tools.javac.util.List<R>
      inlineList(java.util.List<T> elements)
      throws CouldNotResolveImportException {
    com.sun.tools.javac.util.List<R> result = com.sun.tools.javac.util.List.nil();
    for (Inlineable<R> e : elements) {
      if (e instanceof URepeated) {
        // URepeated is bound to a list of expressions.
        URepeated repeated = (URepeated) e;
        for (JCExpression expr : getBinding(repeated.key())) {
          result = result.append((R) expr);
        }
      } else {
        result = result.append(e.inline(this));
      }
    }
    return result;
  }
  
  public Set<String> getImportsToAdd() {
    return ImmutableSet.copyOf(importsToAdd);
  }
  
  public Set<String> getStaticImportsToAdd() {
    return ImmutableSet.copyOf(staticImportsToAdd);
  }
  
  public TypeVar inlineAsVar(UTypeVar var) throws CouldNotResolveImportException {
    /*
     * In order to handle recursively bounded type variables without a stack overflow,
     * we first cache a type var with no bounds, then we inline the bounds.
     */
    TypeVar typeVar = typeVarCache.get(var.getName());
    if (typeVar != null) {
      return typeVar;
    }
    Name name = asName(var.getName());
    TypeSymbol sym = new TypeSymbol(0, name, null, symtab().noSymbol);
    typeVar = new TypeVar(sym, null, null);
    sym.type = typeVar;
    typeVarCache.put(var.getName(), typeVar);
    // Any recursive uses of var will point to the same TypeVar object generated above.
    typeVar.bound = var.getUpperBound().inline(this);
    typeVar.lower = var.getLowerBound().inline(this);
    return typeVar;
  }
  
  Type inlineTypeVar(UTypeVar var) throws CouldNotResolveImportException {
    Optional<TypeWithExpression> typeVarBinding = getOptionalBinding(var.key());
    if (typeVarBinding.isPresent()) {
      return typeVarBinding.get().type();
    } else {
      return inlineAsVar(var);
    }
  }
}