/*
 * Copyright 2015 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.template.soy.jbcsrc;

import com.google.common.collect.ImmutableList;

import org.objectweb.asm.ClassVisitor;

import java.util.LinkedHashMap;
import java.util.Map;

/**
 * Tracks a collection of inner classes and aids in name management and calling 
 * {@link ClassVisitor#visitInnerClass(String, String, String, int)} to ensure that they are 
 * registered correctly.
 */
final class InnerClasses {
  private final TypeInfo outer;
  private final Map<TypeInfo, ClassData> innerClasses = new LinkedHashMap<>();
  private final Map<TypeInfo, Integer> innerClassesAccessModifiers = new LinkedHashMap<>();
  private final UniqueNameGenerator classNames = UniqueNameGenerator.forClassNames();

  InnerClasses(TypeInfo outer) {
    this.outer = outer;
  }
  
  /** Returns all the {@link ClassData} for every InnerClass registered. */
  ImmutableList<ClassData> getInnerClassData() {
    return ImmutableList.copyOf(innerClasses.values());
  }

  /** 
   * Register the given name as an inner class with the given access modifiers.
   * 
   * @return A {@link TypeInfo} with the full class name
   */
  TypeInfo registerInnerClass(String simpleName, int accessModifiers) {
    classNames.claimName(simpleName);
    TypeInfo innerClass = outer.innerClass(simpleName);
    innerClassesAccessModifiers.put(innerClass, accessModifiers);
    return innerClass;
  }

  /** 
   * Register the name (or a simpl mangling of it) as an inner class with the given access 
   * modifiers.
   * 
   * @return A {@link TypeInfo} with the full (possibly mangled) class name 
   */
  TypeInfo registerInnerClassWithGeneratedName(String simpleName, int accessModifiers) {
    simpleName = classNames.generateName(simpleName);
    TypeInfo innerClass = outer.innerClass(simpleName);
    innerClassesAccessModifiers.put(innerClass, accessModifiers);
    return innerClass;
  }
  
  /** 
   * Adds the data for an inner class.
   * 
   * @throws java.lang.IllegalArgumentException if the class wasn't previous registered via 
   *     {@link #registerInnerClass(String, int)} or
   *     {@link #registerInnerClassWithGeneratedName(String, int)}.
   */
  void add(ClassData classData) {
    checkRegistered(classData.type());
    innerClasses.put(classData.type(), classData);
  }

  private void checkRegistered(TypeInfo type) {
    if (!classNames.hasName(type.simpleName())) {
      throw new IllegalArgumentException(type + " wasn't registered");
    }
  }
  
  /**
   * Registers this factory as an inner class on the given class writer.
   * 
   * <p>Registering an inner class is confusing.  The inner class needs to call this and so does
   * the outer class.  Confirmed by running ASMIfier.  Also, failure to call visitInnerClass on both
   * classes either breaks reflective apis (like class.getSimpleName()/getEnclosingClass), or 
   * causes verifier errors (like IncompatibleClassChangeError).
   */
  void registerAsInnerClass(ClassVisitor visitor, TypeInfo innerClass) {
    checkRegistered(innerClass);
    doRegister(visitor, innerClass);
  }
  
  /**
   * Registers all inner classes to the given outer class.
   */
  void registerAllInnerClasses(ClassVisitor visitor) {
    for (Map.Entry<TypeInfo, Integer> entry : innerClassesAccessModifiers.entrySet()) {
      TypeInfo innerClass = entry.getKey();
      doRegister(visitor, innerClass);
    }
  }

  private void doRegister(ClassVisitor visitor, TypeInfo innerClass) {
    visitor.visitInnerClass(
        innerClass.internalName(), 
        outer.internalName(), 
        innerClass.simpleName(),
        innerClassesAccessModifiers.get(innerClass));
  }
}
