/*
 * Copyright (C) 2012 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.config.rebind;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.jboss.errai.codegen.meta.MetaClass;
import org.jboss.errai.codegen.meta.MetaClassCache;
import org.jboss.errai.codegen.meta.MetaClassFactory;
import org.jboss.errai.codegen.meta.impl.gwt.GWTClass;
import org.jboss.errai.codegen.meta.impl.java.JavaReflectionClass;
import org.jboss.errai.common.metadata.RebindUtils;
import org.jboss.errai.common.rebind.CacheStore;
import org.jboss.errai.common.rebind.CacheUtil;

import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.TypeOracle;

/**
 * @author Mike Brock
 */
public abstract class MetaClassBridgeUtil {
  private MetaClassBridgeUtil() {}

  /**
   * Erases the {@link org.jboss.errai.codegen.meta.MetaClassFactory} cache, then populates it with types
   * discovered via GWT's TypeOracle. The reason for the initial flush of the
   * MetaClassFactory is to support hot redeploy in Dev Mode. The reason for doing
   * this operation at all is so that the overridden class definitions
   * (super-source classes) are used in preference to the Java reflection based
   * class definitions.
   *
   * @param context
   *     The GeneratorContext supplied by the GWT compiler. Not null.
   * @param logger
   *     The TreeLogger supplied by the GWT compiler. Not null.
   */
  public synchronized static void populateMetaClassFactoryFromTypeOracle(final GeneratorContext context,
                                                                         final TreeLogger logger) {

    final GWTTypeOracleCacheStore tOCache = CacheUtil.getCache(GWTTypeOracleCacheStore.class);

    // if we're in production mode -- it means we're compiling, and we do not need to accommodate dynamically
    // changing classes. Therefore, do a NOOP after the first successful call.
    if (context.equals(tOCache.populatedFrom)) {
      return;
    }

    final TypeOracle typeOracle = context.getTypeOracle();
    final MetaClassCache cache = MetaClassFactory.getMetaClassCache();

    if (typeOracle != null) {
      final Map<String, MetaClass> classesToPush = new HashMap<String, MetaClass>(typeOracle.getTypes().length);
      final Set<String> translatable = new HashSet<String>(RebindUtils.findTranslatablePackages(context));
      // Need to remove these or else we get issues from annotations and loading
      // of emulated Object and Class instead of real ones.
      translatable.remove("java.lang");
      translatable.remove("java.lang.annotation");
      final Set<String> reloadable = RebindUtils.getReloadablePackageNames(context);

      for (final JClassType type : typeOracle.getTypes()) {
        if (!translatable.contains(type.getPackage().getName())) {
          continue;
        }

        if (type.isAnnotation() != null || type.getQualifiedSourceName().equals("java.lang.annotation.Annotation")) {

          if (!MetaClassFactory.canLoadClass(type.getQualifiedBinaryName())) {
            throw new RuntimeException("a new annotation has been introduced (" + type.getQualifiedSourceName() + "); "
                + "you cannot currently introduce new annotations in devmode. Please restart.");
          }

          final MetaClass clazz = JavaReflectionClass
              .newUncachedInstance(MetaClassFactory.loadClass(type.getQualifiedBinaryName()));

          if (isReloadable(clazz, reloadable)) {
            classesToPush.put(clazz.getFullyQualifiedName(), clazz);
          } else if (!cache.isKnownType(clazz.getFullyQualifiedName())) {
            cache.pushCache(clazz);
          }
        }
        else {
          logger.log(TreeLogger.Type.DEBUG, "Caching translatable type " + type.getQualifiedSourceName());
          final MetaClass clazz = GWTClass.newUncachedInstance(typeOracle, type);

          if (isReloadable(clazz, reloadable)) {
            classesToPush.put(clazz.getFullyQualifiedName(), clazz);
          } else if (!cache.isKnownType(clazz.getFullyQualifiedName())) {
            cache.pushCache(clazz);
          }
        }
      }

      cache.updateCache(classesToPush);
    }
    tOCache.populatedFrom = context;

    CacheUtil.getCache(EnvUtil.EnvironmentConfigCache.class).clear();
  }

  private static boolean isReloadable(final MetaClass clazz, final Set<String> reloadablePacakges) {
    for (final String packageName : reloadablePacakges) {
      if (clazz.getPackageName().startsWith(packageName))
        return true;
    }

    return false;
  }

  public static class GWTTypeOracleCacheStore implements CacheStore {
    volatile GeneratorContext populatedFrom;

    @Override
    public void clear() {
      populatedFrom = null;
    }
  }
}
