// Copyright (C) 2011 The Android Open Source Project
//
// 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.gerrit.server.rules.prolog;

import com.google.common.flogger.FluentLogger;
import com.google.gerrit.server.AnonymousUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.account.Emails;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.PluginConfigFactory;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.patch.DiffOperations;
import com.google.gerrit.server.patch.PatchSetInfoFactory;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.project.ProjectCache;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import com.google.inject.assistedinject.Assisted;
import com.googlecode.prolog_cafe.lang.BufferingPrologControl;
import com.googlecode.prolog_cafe.lang.Predicate;
import com.googlecode.prolog_cafe.lang.PredicateEncoder;
import com.googlecode.prolog_cafe.lang.Prolog;
import com.googlecode.prolog_cafe.lang.PrologMachineCopy;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.eclipse.jgit.lib.Config;

/**
 * Per-thread Prolog interpreter.
 *
 * <p>This class is not thread safe.
 *
 * <p>A single copy of the Prolog interpreter, for the current thread.
 */
public class PrologEnvironment extends BufferingPrologControl {
  private static final FluentLogger logger = FluentLogger.forEnclosingClass();

  public interface Factory {
    /**
     * Construct a new Prolog interpreter.
     *
     * @param src the machine to template the new environment from.
     * @return the new interpreter.
     */
    PrologEnvironment create(PrologMachineCopy src);
  }

  private final Args args;
  private final Map<StoredValue<Object>, Object> storedValues;
  private List<Runnable> cleanup;

  @Inject
  PrologEnvironment(Args a, @Assisted PrologMachineCopy src) {
    super(src);
    setEnabled(EnumSet.allOf(Prolog.Feature.class), false);
    args = a;
    storedValues = new HashMap<>();
    cleanup = new ArrayList<>();
  }

  public Args getArgs() {
    return args;
  }

  @Override
  public void setPredicate(Predicate goal) {
    super.setPredicate(goal);
    int reductionLimit = args.reductionLimit(goal);
    setReductionLimit(reductionLimit);
  }

  /**
   * Lookup a stored value in the interpreter's hash manager.
   *
   * @param <T> type of stored Java object.
   * @param sv unique key.
   * @return the value; null if not stored.
   */
  @SuppressWarnings("unchecked")
  public <T> T get(StoredValue<T> sv) {
    return (T) storedValues.get(sv);
  }

  /**
   * Set a stored value on the interpreter's hash manager.
   *
   * @param <T> type of stored Java object.
   * @param sv unique key.
   * @param obj the value to store under {@code sv}.
   */
  @SuppressWarnings("unchecked")
  public <T> void set(StoredValue<T> sv, T obj) {
    storedValues.put((StoredValue<Object>) sv, obj);
  }

  /**
   * Copy the stored values from another interpreter to this one. Also gets the cleanup from the
   * child interpreter
   */
  public void copyStoredValues(PrologEnvironment child) {
    storedValues.putAll(child.storedValues);
    setCleanup(child.cleanup);
  }

  /**
   * Assign the environment a cleanup list (in order to use a centralized list) If this
   * enivronment's list is non-empty, append its cleanup tasks to the assigning list.
   */
  public void setCleanup(List<Runnable> newCleanupList) {
    newCleanupList.addAll(cleanup);
    cleanup = newCleanupList;
  }

  /**
   * Adds cleanup task to run when close() is called
   *
   * @param task is run when close() is called
   */
  public void addToCleanup(Runnable task) {
    cleanup.add(task);
  }

  /** Release resources stored in interpreter's hash manager. */
  public void close() {
    for (Iterator<Runnable> i = cleanup.iterator(); i.hasNext(); ) {
      try {
        i.next().run();
      } catch (Exception err) {
        logger.atSevere().withCause(err).log("Failed to execute cleanup for PrologEnvironment");
      }
      i.remove();
    }
  }

  @Singleton
  public static class Args {
    private static final Class<Predicate> CONSULT_STREAM_2;

    static {
      try {
        @SuppressWarnings("unchecked")
        Class<Predicate> c =
            (Class<Predicate>)
                Class.forName(
                    PredicateEncoder.encode(Prolog.BUILTIN, "consult_stream", 2),
                    false,
                    RulesCache.class.getClassLoader());
        CONSULT_STREAM_2 = c;
      } catch (ClassNotFoundException e) {
        throw new LinkageError("cannot find predicate consult_stream", e);
      }
    }

    private final ProjectCache projectCache;
    private final PermissionBackend permissionBackend;
    private final GitRepositoryManager repositoryManager;
    private final PluginConfigFactory pluginConfigFactory;
    private final DiffOperations diffOperations;
    private final PatchSetInfoFactory patchSetInfoFactory;
    private final IdentifiedUser.GenericFactory userFactory;
    private final Provider<AnonymousUser> anonymousUser;
    private final int reductionLimit;
    private final int compileLimit;
    private final PatchSetUtil patchsetUtil;
    private Emails emails;

    @Inject
    Args(
        ProjectCache projectCache,
        PermissionBackend permissionBackend,
        GitRepositoryManager repositoryManager,
        PluginConfigFactory pluginConfigFactory,
        DiffOperations diffOperations,
        PatchSetInfoFactory patchSetInfoFactory,
        IdentifiedUser.GenericFactory userFactory,
        Provider<AnonymousUser> anonymousUser,
        @GerritServerConfig Config config,
        PatchSetUtil patchsetUtil,
        Emails emails) {
      this.projectCache = projectCache;
      this.permissionBackend = permissionBackend;
      this.repositoryManager = repositoryManager;
      this.pluginConfigFactory = pluginConfigFactory;
      this.diffOperations = diffOperations;
      this.patchSetInfoFactory = patchSetInfoFactory;
      this.userFactory = userFactory;
      this.anonymousUser = anonymousUser;
      this.patchsetUtil = patchsetUtil;
      this.emails = emails;
      this.reductionLimit = RuleUtil.reductionLimit(config);
      this.compileLimit = RuleUtil.compileReductionLimit(config);

      logger.atInfo().log("reductionLimit: %d, compileLimit: %d", reductionLimit, compileLimit);
    }

    private int reductionLimit(Predicate goal) {
      if (goal.getClass() == CONSULT_STREAM_2) {
        logger.atFine().log(
            "predicate class is CONSULT_STREAM_2: override reductionLimit with compileLimit (%d)",
            compileLimit);
        return compileLimit;
      }
      return reductionLimit;
    }

    public ProjectCache getProjectCache() {
      return projectCache;
    }

    public PermissionBackend getPermissionBackend() {
      return permissionBackend;
    }

    public GitRepositoryManager getGitRepositoryManager() {
      return repositoryManager;
    }

    public PluginConfigFactory getPluginConfigFactory() {
      return pluginConfigFactory;
    }

    public DiffOperations getDiffOperations() {
      return diffOperations;
    }

    public PatchSetInfoFactory getPatchSetInfoFactory() {
      return patchSetInfoFactory;
    }

    public IdentifiedUser.GenericFactory getUserFactory() {
      return userFactory;
    }

    public AnonymousUser getAnonymousUser() {
      return anonymousUser.get();
    }

    public PatchSetUtil getPatchsetUtil() {
      return patchsetUtil;
    }

    public Emails getEmails() {
      return emails;
    }
  }
}
