/*
 * Copyright (C) 2015 Red Hat, Inc. and/or its affiliates.
 *
 * 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 org.jboss.errai.codegen;

import static org.jboss.errai.codegen.util.PrettyPrinter.prettyPrintJava;

import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Set;

import org.jboss.errai.codegen.builder.AnonymousClassStructureBuilder;
import org.jboss.errai.codegen.builder.ClassStructureBuilder;
import org.jboss.errai.codegen.builder.impl.ObjectBuilder;
import org.jboss.errai.codegen.exception.CyclicalObjectGraphException;
import org.jboss.errai.codegen.exception.GenerationException;
import org.jboss.errai.codegen.exception.NotLiteralizableException;
import org.jboss.errai.codegen.literal.NullLiteral;
import org.jboss.errai.codegen.meta.MetaClass;
import org.jboss.errai.codegen.meta.MetaClassFactory;
import org.jboss.errai.codegen.meta.MetaMethod;
import org.jboss.errai.codegen.util.Stmt;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

//import com.google.gwt.dev.util.collect.IdentityHashSet;

/**
 * Utility class for creating code-generated snapshots of certain types of live value objects.
 * The classes and interfaces that SnapshotMaker works with have the following characteristics:
 * <ul>
 *  <li>It must be an interface or a non-final class with a public no-args constructor.
 *  <li>None of the public methods take arguments (except {@code equals(Object)}, which is always ignored)
 *  <li>Each public method must be handled by the given MethodBodyCallback, or return one
 *      of the following values for which a snapshot can be generated automatically:
 *    <ul>
 *      <li>{@code void}
 *      <li>a Java primitive type
 *      <li>a {@link Context#addLiteralizableClass(Class) literalizable type} in the current code generator context
 *      <li>a type that is explicitly mentioned as a "type to recurse on" (these types must in turn follow this set of rules)
 *    </ul>
 *  </ul>
 *
 * @author Jonathan Fuerth <jfuerth@gmail.com>
 * @author Mike Brock
 */
public final class SnapshotMaker {

  final static Logger logger = LoggerFactory.getLogger(SnapshotMaker.class);

  /**
   * Callback interface for providing custom method bodies in snapshots. There are three major use cases:
   * <ol>
   *  <li>To implement methods that take parameters (snapshots of
   *      these methods cannot be generated automatically)
   *  <li>To return a reference to some object that's already in the
   *      scope of the snapshot (such as a reference to a parent object
   *      from a getParent() method)
   *  <li>To implement additional methods in the case that the snapshot
   *      type is not the same as the type to extend.
   * </ol>
   *
   * @author Jonathan Fuerth <jfuerth@gmail.com>
   */
  public interface MethodBodyCallback {

    /**
     * Optionally returns the statement that should be used as the body of the
     * given method for the given object's snapshot. If the default snapshot
     * behaviour provided by SnapshotMaker is sufficient for the given method,
     * this callback can simply return null.
     *
     * @param method
     *          The method to provide the body for.
     * @param o
     *          The instance object that we are taking the snapshot of. You can
     *          use this reference if you need to invoke {@code method}.
     * @param containingClass
     *          The class that will contain the generated method. During the
     *          callback, you can generate additional methods and fields within
     *          this class if you like.
     * @return The Statement to use as the method body (must return a type
     *         compatible with {@code method}'s return type), or null if the
     *         snapshot maker should generate the method body by invoking method
     *         on {@code o} and returning a Literal of its value.
     */
    Statement generateMethodBody(MetaMethod method, Object o, ClassStructureBuilder<?> containingClass);

  }

  /** This class should not be instantiated. */
  private SnapshotMaker() {}

  /**
   * Code-generates an object whose methods return (snapshots of) the same
   * values as the given object.
   *
   * @param o
   *          The object to snapshot.
   * @param typeToSnapshot
   *          The type to read the snapshot attributes from. Must be a
   *          superclass of o or an interface implemented by o, and methods not
   *          supplied by {@code methodBodyCallback} must meed the requirements
   *          laid out in the class-level SnapshotMaker documentation.
   * @param typeToExtend
   *          The type of the snapshot to produce. Must be a subclass or
   *          subinterface of typeToSnapshot, and the additional methods present
   *          in typeToExtend vs. typeToSnapshot must be provided by the
   *          MethodMaker callback, since they can't be generated from o.
   * @param methodBodyCallback
   *          A callback that can provide method bodies, preventing the standard
   *          snapshot behaviour for those methods. This callback is optional;
   *          null is acceptable as "no callback."
   * @param typesToRecurseOn
   *          The types for which the snapshot maker should be applied
   *          recursively.
   * @return A Statement representing the value of the object
   * @throws CyclicalObjectGraphException
   *           if any objects reachable from {@code o} form a reference cycle.
   *           The simplest example of this would be a method on {@code o} that
   *           returns {@code o} itself. You may be able to work around such a
   *           problem by supplying a canned representation of one of the
   *           objects in the cycle.
   */
  public static Statement makeSnapshotAsSubclass(
      final Object o,
      final Class<?> typeToSnapshot,
      final Class<?> typeToExtend,
      final MethodBodyCallback methodBodyCallback,
      final Class<?> ... typesToRecurseOn) {
    final MetaClass metaTypeToSnapshot = MetaClassFactory.get(typeToSnapshot);
    final MetaClass metaTypeToExtend = MetaClassFactory.get(typeToExtend);
    final MetaClass[] metaTypesToRecurseOn = new MetaClass[typesToRecurseOn.length];
    for (int i = 0; i < typesToRecurseOn.length; i++) {
      metaTypesToRecurseOn[i] = MetaClassFactory.get(typesToRecurseOn[i]);
    }
    return makeSnapshotAsSubclass(o, metaTypeToSnapshot, metaTypeToExtend, methodBodyCallback, metaTypesToRecurseOn);
  }

  /**
   * Code-generates an object whose methods return (snapshots of) the same
   * values as the given object.
   *
   * @param o
   *          The object to snapshot.
   * @param typeToSnapshot
   *          The type to read the snapshot attributes from. Must be a
   *          superclass of o or an interface implemented by o, and methods not
   *          supplied by {@code methodBodyCallback} must meed the requirements
   *          laid out in the class-level SnapshotMaker documentation.
   * @param typeToExtend
   *          The type of the snapshot to produce. Must be a subclass or
   *          subinterface of typeToSnapshot, and the additional methods present
   *          in typeToExtend vs. typeToSnapshot must be provided by the
   *          MethodMaker callback, since they can't be generated from o.
   * @param methodBodyCallback
   *          A callback that can provide method bodies, preventing the standard
   *          snapshot behaviour for those methods. This callback is optional;
   *          null is acceptable as "no callback."
   * @param typesToRecurseOn
   *          The types for which the snapshot maker should be applied
   *          recursively.
   * @return A Statement representing the value of the object
   * @throws CyclicalObjectGraphException
   *           if any objects reachable from {@code o} form a reference cycle.
   *           The simplest example of this would be a method on {@code o} that
   *           returns {@code o} itself. You may be able to work around such a
   *           problem by supplying a canned representation of one of the objects
   *           in the cycle.
   */
  public static Statement makeSnapshotAsSubclass(
      final Object o,
      final MetaClass typeToSnapshot,
      final MetaClass typeToExtend,
      final MethodBodyCallback methodBodyCallback,
      final MetaClass ... typesToRecurseOn) {

    return makeSnapshotAsSubclass(
        o,
        typeToSnapshot,
        typeToExtend,
        methodBodyCallback,
        new HashSet<MetaClass>(Arrays.asList(typesToRecurseOn)),
        new IdentityHashMap<Object, Statement>(),
        Collections.newSetFromMap(new IdentityHashMap<Object, Boolean>()));
  }

  /**
   * Implementation for the same-named public methods.
   *
   * @param o
   *          The object to snapshot.
   * @param typeToSnapshot
   *          The type to read the snapshot attributes from. Must be a
   *          superclass of o or an interface implemented by o, and methods not
   *          supplied by {@code methodBodyCallback} must meed the requirements
   *          laid out in the class-level SnapshotMaker documentation.
   * @param typeToExtend
   *          The type of the snapshot to produce. Must be a subclass or
   *          subinterface of typeToSnapshot, and the additional methods present
   *          in typeToExtend vs. typeToSnapshot must be provided by the
   *          MethodMaker callback, since they can't be generated from o.
   * @param typesToRecurseOn
   *          Types for which this method should be called recursively.
   * @param methodBodyCallback
   *          A callback that can provide method bodies, preventing the standard
   *          snapshot behaviour for those methods. This callback is optional;
   *          null is acceptable as "no callback."
   * @param existingSnapshots
   *          Object instances for which a snapshot has already been completed.
   *          Bootstrap this with an empty IdentityHashMap.
   * @param unfinishedSnapshots
   *          Object instances for which a partially-completed snapshot exists.
   *          If one of these objects is returned by a method in {@code o}, this
   *          causes a CyclicalObjectGraphException.
   * @return A Statement of type {@code typeToExtend} that represents the
   *         current publicly visible state of {@code o}.
   */
  private static Statement makeSnapshotAsSubclass(
      final Object o,
      final MetaClass typeToSnapshot,
      final MetaClass typeToExtend,
      final MethodBodyCallback methodBodyCallback,
      final Set<MetaClass> typesToRecurseOn,
      final IdentityHashMap<Object, Statement> existingSnapshots,
      final Set<Object> unfinishedSnapshots) {

    if (o == null) {
      return NullLiteral.INSTANCE;
    }

    if (!typeToSnapshot.isAssignableFrom(o.getClass())) {
      throw new IllegalArgumentException(
          "Given object (of type " + o.getClass().getName() +
              ") is not an instance of requested type to snapshot " + typeToSnapshot.getName());
    }

    if (logger.isDebugEnabled()) {
      logger.debug("** Making snapshot of " + o);
      logger.debug("   Existing snapshots: " + existingSnapshots);
    }

    final List<MetaMethod> sortedMethods = Arrays.asList(typeToExtend.getMethods());
    Collections.sort(sortedMethods, new Comparator<MetaMethod>() {
      @Override
      public int compare(MetaMethod m1, MetaMethod m2) {
        return m1.getName().compareTo(m2.getName());
      }
    });

    logger.debug("   Creating a new statement");
    return new Statement() {
      String generatedCache;

      /**
       * We retain a mapping of return values to the methods that returned them,
       * in case we need to provide diagnostic information when an exception is
       * thrown.
       */
      IdentityHashMap<Object, MetaMethod> methodReturnVals = new IdentityHashMap<Object, MetaMethod>();

      @Override
      public String generate(Context context) {
        if (logger.isDebugEnabled()) {
          logger.debug("++ Statement.generate() for " + o);
        }

        if (generatedCache != null) return generatedCache;

        // create a subcontext and record the types we will allow the LiteralFactory to create automatic
        // snapshots for.
        final Context subContext = Context.create(context);
        subContext.addLiteralizableMetaClasses(typesToRecurseOn);

        final AnonymousClassStructureBuilder builder = ObjectBuilder.newInstanceOf(typeToExtend.getErased(), context)
            .extend();
        unfinishedSnapshots.add(o);
        for (final MetaMethod method : sortedMethods) {
          if (method.isFinal() || method.getName().equals("toString")) continue;

          if (logger.isDebugEnabled()) {
            logger.debug("  method " + method.getName());
            logger.debug("    return type " + method.getReturnType());
          }

          if (methodBodyCallback != null) {
            final Statement providedMethod = methodBodyCallback.generateMethodBody(method, o, builder);
            if (providedMethod != null) {
              logger.debug("    body provided by callback");
              builder
                  .publicOverridesMethod(method.getName(), Parameter.of(method.getParameters()))
                  .append(providedMethod)
                  .finish();
              continue;
            }
          }

          if (method.getName().equals("equals") || method.getName().equals("hashCode")
              || method.getName().equals("clone") || method.getName().equals("finalize")) {
            // we skip these if not provided by the callback
            if (logger.isDebugEnabled()) {
              logger.debug("    skipping special-case method " + method.getName());
            }
            continue;
          }

          if (method.getParameters().length > 0) {
            throw new GenerationException("Method " + method + " in " + typeToSnapshot +
                    " takes parameters. Such methods must be handled by the MethodBodyCallback," +
                    " because they cannot be snapshotted.");
          }

          if (method.getReturnType().equals(MetaClassFactory.get(void.class))) {
            builder.publicOverridesMethod(method.getName()).finish();
            if (logger.isDebugEnabled()) {
              logger.debug("  finished method " + method.getName());
            }
            continue;
          }
          try {

            final Object retval = typeToExtend.asClass().getMethod(method.getName()).invoke(o);
            methodReturnVals.put(retval, method);

            if (logger.isDebugEnabled()) {
              logger.debug("    retval=" + retval);
            }

            Statement methodBody;
            if (existingSnapshots.containsKey(retval)) {
              logger.debug("    using existing snapshot");
              methodBody = existingSnapshots.get(retval);
            }
            else if (subContext.isLiteralizableClass(method.getReturnType().getErased())) {
              if (unfinishedSnapshots.contains(retval)) {
                throw new CyclicalObjectGraphException(unfinishedSnapshots);
              }

              // use Stmt.create(context) to pass the context along.
              if (logger.isDebugEnabled()) {
                logger.debug("    >> recursing for " + retval);
              }

              methodBody = Stmt.create(subContext).nestedCall(makeSnapshotAsSubclass(
                  retval, method.getReturnType(), method.getReturnType(),
                  methodBodyCallback, typesToRecurseOn, existingSnapshots, unfinishedSnapshots)).returnValue();
            }
            else {
              logger.debug("    relying on literal factory");
              methodBody = Stmt.load(retval).returnValue();
            }

            if (logger.isDebugEnabled()) {
              logger.debug("  finished method " + method.getName());
            }

            builder.publicOverridesMethod(method.getName()).append(methodBody).finish();
            existingSnapshots.put(retval, methodBody);
          }
          catch (final GenerationException e) {
            e.appendFailureInfo("In attempt to snapshot return value of "
                + typeToExtend.getFullyQualifiedName() + "." + method.getName() + "()");
            throw e;
          }
          catch (final RuntimeException e) {
            throw e;
          }
          catch (final Exception e) {
            throw new GenerationException("Failed to extract value for snapshot", e);
          }
        }

        if (logger.isDebugEnabled()) {
          logger.debug("    finished: " + builder);
        }

        try {
          generatedCache = prettyPrintJava(builder.finish().toJavaString());
        } catch (final NotLiteralizableException e) {
          final MetaMethod m = methodReturnVals.get(e.getNonLiteralizableObject());
          if (m != null) {
            e.appendFailureInfo("This value came from method " +
                  m.getDeclaringClass().getFullyQualifiedNameWithTypeParms() + "." + m.getName() +
                  ", which has return type " + m.getReturnType());
          }
          throw e;
        } catch (final GenerationException e) {
          e.appendFailureInfo("While generating a snapshot of " + o.toString() +
              " (actual type: " + o.getClass().getName() +
              "; type to extend: " + typeToExtend.getFullyQualifiedName() + ")");
          throw e;
        }
        unfinishedSnapshots.remove(o);
        return generatedCache;
      }

      @Override
      public MetaClass getType() {
        return typeToExtend;
      }
    };
  }

}
