/*
 * Copyright (C) 2013 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.io.PrintWriter;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Future;

import org.jboss.errai.codegen.meta.MetaClass;
import org.jboss.errai.codegen.meta.MetaClassFactory;
import org.jboss.errai.common.metadata.RebindUtils;
import org.jboss.errai.config.util.ThreadUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.gwt.core.ext.Generator;
import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.TreeLogger;

/**
 * Base class of all asynchronous code generators.
 *
 * @author Christian Sadilek <csadilek@redhat.com>
 */
public abstract class AbstractAsyncGenerator extends Generator implements AsyncCodeGenerator {

  private static final Map<Class<? extends AbstractAsyncGenerator>, String> cacheMap = new ConcurrentHashMap<Class<? extends AbstractAsyncGenerator>, String>();
  private static final Map<Class<? extends AbstractAsyncGenerator>, Set<String>> cacheRelevantClasses =
          new ConcurrentHashMap<Class<? extends AbstractAsyncGenerator>, Set<String>>();

  private static Logger log = LoggerFactory.getLogger(AbstractAsyncGenerator.class);

  @Override
  public Future<String> generateAsync(final TreeLogger logger, final GeneratorContext context) {
    return ThreadUtil.submit(new Callable<String>() {
      @Override
      public String call() throws Exception {
        final String generatedCode;

        if (isCacheEnabled() && isCacheValid()) {
          log.info("Using cached output from " + AbstractAsyncGenerator.this.getClass().getName());
          generatedCode = getGeneratedCache();
        }
        else {
          log.info("Running generator " + AbstractAsyncGenerator.this.getClass().getName());
          clearCacheRelevantClasses();
          generatedCode = generate(logger, context);
          setGeneratedCache(generatedCode);
        }

        return generatedCode;
      }
    });
  }

  private boolean isCacheEnabled() {
    return !RebindUtils.NO_CACHE;
  }

  /**
   * @return True iff there is a cached output for this generator. Useful for subclasses that must override {@link #isCacheValid()}.
   */
  protected final boolean hasGenerationCache() {
    final boolean hasCache = cacheMap.containsKey(getClass());
    if (log.isDebugEnabled()) {
      log.debug("{} {} a generated cache.", getClass().getSimpleName(), (hasCache ? "has" : "does not have"));
    }
    return hasCache;
  }

  protected final String getGeneratedCache() {
    return cacheMap.get(getClass());
  }

  private final void setGeneratedCache(final String generated) {
    cacheMap.put(getClass(), generated);
  }

  /**
   * @return True iff this generator does not need to be run again this refresh.
   */
  protected boolean isCacheValid() {
    return hasGenerationCache() && !(MetaClassFactory.hasAnyChanges() && hasRelevantChanges());
  }

  private boolean hasRelevantChanges() {
    final String generatorName = this.getClass().getSimpleName();
    final Set<String> relevantClasses = cacheRelevantClasses.get(this.getClass());
    if (relevantClasses == null) {
      log.debug("No classes marked as relevant for {}. Assuming there are relevant changes.", generatorName);
      return true;
    }

    for (final MetaClass clazz : MetaClassFactory.getAllNewOrUpdatedClasses()) {
      final boolean previouslyMarkedRelevant = relevantClasses.contains(clazz.getFullyQualifiedName());
      if (previouslyMarkedRelevant || isRelevantClass(clazz)) {
        log.debug("New or updated class {} is {} cache relevant for {}.", clazz.getFullyQualifiedName(),
                (previouslyMarkedRelevant ? "previously marked as" : "now marked as"), generatorName);
        return true;
      } else {
        log.trace("New or updated class {} is not cache relevant for {}.", clazz.getFullyQualifiedName(), generatorName);
      }
    }

    for (final String deleted : MetaClassFactory.getAllDeletedClasses()) {
      if (relevantClasses.contains(deleted)) {
        log.debug("Deleted class {} was cache relevant for {}.", deleted, generatorName);
        return true;
      } else {
        log.trace("Deleted class {} is not cache relevant for {}.", deleted, generatorName);
      }
    }

    return false;
  }

  /**
   * Checks if the provided class is relevant to this generator. This method is
   * invoked to determine whether or not the introduction of the provided class
   * should trigger a rerun of this generator.
   *
   * @param clazz
   *          a newly introduced clazz since the last refresh.
   *
   * @return true if newly introduced class is relevant to this generator,
   *         otherwise false.
   */
  protected boolean isRelevantClass(final MetaClass clazz) {
    return true;
  }

  /**
   * Called by {@link #generateAsync(TreeLogger, GeneratorContext)} to carry out the actual code
   * generation.
   *
   * @param context
   *          the generator context to use.
   *
   * @return the generated code.
   */
  protected abstract String generate(final TreeLogger logger, final GeneratorContext context);

  /**
   * Starts all asynchronous generators if they haven't been started yet and waits for the
   * completion of the generator responsible for the provided interface type.
   *
   * @param interfaceType
   *          the interface for which an implementation should be generated.
   * @param context
   *          the generation context to use.
   * @param logger
   *          the tree logger to use.
   * @param packageName
   *          the package name of the generated class.
   * @param className
   *          the name of the generated class.
   *
   * @return the fully qualified name of the generated class.
   */
  protected String startAsyncGeneratorsAndWaitFor(
      final Class<?> interfaceType,
      final GeneratorContext context,
      final TreeLogger logger,
      final String packageName,
      final String className) {

    try {
      final PrintWriter printWriter = context.tryCreate(logger, packageName, className);
      if (printWriter != null) {
        final Future<String> future = AsyncGenerationJob.createBuilder()
            .treeLogger(logger)
            .generatorContext(context)
            .interfaceType(interfaceType)
            .runIfStarting(new Runnable() {
              @Override
              public void run() {
                final long start = System.currentTimeMillis();
                MetaClassBridgeUtil.populateMetaClassFactoryFromTypeOracle(context, logger);
                log.debug("MetaClassFactory populated in {}ms", System.currentTimeMillis() - start);
              }
            })
            .build()
            // this causes all asynchronous code generators to run if this is the first one that executes.
            .submit();

        final String gen = future.get();
        printWriter.append(gen);

        RebindUtils.writeStringToJavaSourceFileInErraiCacheDir(packageName, className, gen);

        context.commit(logger, printWriter);
      }
    }
    catch (final Throwable e) {
      e.printStackTrace();
      logger.log(TreeLogger.ERROR, "Error generating " + className, e);
    }

    return packageName + "." + className;
  }

  /**
   * Marks the provided class as cache relevant. This means that the provided
   * class will be considered when checking for changes to determine whether or
   * not the generator needs to rerun after a refresh. If no cache relevant
   * classes are added any change to reloadable classes will cause the generator
   * to rerun.
   *
   * @param clazz
   *          the class to consider when checking for changes.
   */
  protected void addCacheRelevantClass(final MetaClass clazz) {
    Set<String> classes = cacheRelevantClasses.get(this.getClass());
    if (classes == null) {
      classes = new HashSet<String>();
      cacheRelevantClasses.put(this.getClass(), classes);
    }
    classes.add(clazz.getFullyQualifiedName());
  }

  /**
   * Marks the provided classes as cache relevant. This means that the provided
   * classes will be considered when checking for changes to determine whether
   * or not the generator needs to rerun after a refresh. If no cache relevant
   * classes are added any change to reloadable classes will cause the generator
   * to rerun.
   *
   * @param classes
   *          the classes to consider when checking for changes.
   */
  protected void addCacheRelevantClasses(final Collection<MetaClass> classes) {
    for (final MetaClass clazz : classes) {
      addCacheRelevantClass(clazz);
    }
  }

  private void clearCacheRelevantClasses() {
    cacheRelevantClasses.remove(this.getClass());
  }
}
