/*
 * Copyright 2009 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.asm;

import com.google.gwt.dev.javac.asm.CollectClassData.AnnotationEnum;
import com.google.gwt.dev.util.StringInterner;

import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.Opcodes;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Collects data from (possibly nested) annotations on a single entity.
 */
public class CollectAnnotationData extends AnnotationVisitor {

  /**
   * Holds annotation fields/values.
   */
  public static class AnnotationData {
    private final String desc;
    private final Map<String, Object> values = new HashMap<String, Object>();
    private final boolean visible;

    protected AnnotationData(String desc, boolean visible) {
      this.desc = StringInterner.get().intern(desc);
      this.visible = visible;
    }

    public void addValue(String name, Object value) {
      values.put(StringInterner.get().intern(name), value);
    }

    /**
     * @return the desc
     */
    public String getDesc() {
      return desc;
    }

    /**
     * @return the values
     */
    public Map<String, Object> getValues() {
      return values;
    }

    /**
     * @return the visible
     */
    public boolean isVisible() {
      return visible;
    }
  }

  /**
   * Collects data inside an array-valued annotation parameter.
   */
  public static class MyAnnotationArrayVisitor extends AnnotationVisitor {

    private final Callback<Object> callback;
    private final List<Object> values = new ArrayList<Object>();

    public MyAnnotationArrayVisitor(Callback<Object> callback) {
      super(Opcodes.ASM9);
      this.callback = callback;
    }

    @Override
    public void visit(String name, Object value) {
      values.add(value);
    }

    @Override
    public AnnotationVisitor visitAnnotation(String name, String desc) {
      // TODO(jat): what should visible be set to?
      return new CollectAnnotationData(desc, true,
          new Callback<CollectAnnotationData.AnnotationData>() {
            @Override
            public void call(CollectAnnotationData.AnnotationData value) {
              values.add(value);
            }
          });
    }

    @Override
    public AnnotationVisitor visitArray(String name) {
      return new MyAnnotationArrayVisitor(new Callback<Object>() {
        @Override
        public void call(Object value) {
          values.add(value);
        }
      });
    }

    @Override
    public void visitEnd() {
      callback.call(values.toArray());
    }

    @Override
    public void visitEnum(String name, String desc, String value) {
      values.add(new AnnotationEnum(desc, value));
    }
  }

  /**
   * Generic callback type taking a parameter.
   *
   * @param <T> type of the argument to the callback
   */
  private interface Callback<T> {

    /**
     * Invoke the callback.
     *
     * @param value value to pass to the callback.
     */
    void call(T value);
  }

  private final CollectAnnotationData.AnnotationData annotation;
  private final Callback<CollectAnnotationData.AnnotationData> callback;

  /**
   * Construct the collector.
   *
   * @param desc class descriptor of the annotation class
   * @param visible true if the annotation is visible at runtime
   */
  public CollectAnnotationData(String desc, boolean visible) {
    this(desc, visible, null);
  }

  /**
   * Construct the collector.
   *
   * @param desc class descriptor of the annotation class
   * @param visible true if the annotation is visible at runtime
   * @param callback callback to be called when the annotation is finished
   */
  CollectAnnotationData(String desc, boolean visible,
      Callback<CollectAnnotationData.AnnotationData> callback) {
    super(Opcodes.ASM9);
    annotation = new AnnotationData(desc, visible);
    this.callback = callback;
  }

  /**
   * @return the annotation data
   */
  public AnnotationData getAnnotation() {
    return annotation;
  }

  @Override
  public void visit(String name, Object value) {
    annotation.addValue(name, value);
  }

  @Override
  public AnnotationVisitor visitAnnotation(final String name, String desc) {
    // TODO(jat): what should visible be set to?
    return new CollectAnnotationData(desc, true,
        new Callback<CollectAnnotationData.AnnotationData>() {
          @Override
          public void call(CollectAnnotationData.AnnotationData value) {
            annotation.addValue(name, value);
          }
        });
  }

  @Override
  public AnnotationVisitor visitArray(final String name) {
    return new MyAnnotationArrayVisitor(new Callback<Object>() {

      /**
       * Called with an array of values.
       *
       * @param value
       */
      @Override
      public void call(Object value) {
        annotation.addValue(name, value);
      }
    });
  }

  @Override
  public void visitEnd() {
    if (callback != null) {
      callback.call(annotation);
    }
  }

  @Override
  public void visitEnum(String name, String desc, String value) {
    annotation.addValue(name, new AnnotationEnum(desc, value));
  }
}
