// Copyright (C) 2016 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.query.account;

import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;

import com.google.common.base.Joiner;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import com.google.gerrit.index.IndexConfig;
import com.google.gerrit.index.query.InternalQuery;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.index.account.AccountField;
import com.google.gerrit.server.index.account.AccountIndexCollection;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Query wrapper for the account index.
 *
 * <p>Instances are one-time-use. Other singleton classes should inject a Provider rather than
 * holding on to a single instance.
 */
public class InternalAccountQuery extends InternalQuery<AccountState> {
  private static final Logger log = LoggerFactory.getLogger(InternalAccountQuery.class);

  @Inject
  InternalAccountQuery(
      AccountQueryProcessor queryProcessor,
      AccountIndexCollection indexes,
      IndexConfig indexConfig) {
    super(queryProcessor, indexes, indexConfig);
  }

  @Override
  public InternalAccountQuery setLimit(int n) {
    super.setLimit(n);
    return this;
  }

  @Override
  public InternalAccountQuery enforceVisibility(boolean enforce) {
    super.enforceVisibility(enforce);
    return this;
  }

  @Override
  public InternalAccountQuery setRequestedFields(Set<String> fields) {
    super.setRequestedFields(fields);
    return this;
  }

  @Override
  public InternalAccountQuery noFields() {
    super.noFields();
    return this;
  }

  public List<AccountState> byDefault(String query) throws OrmException {
    return query(AccountPredicates.defaultPredicate(query));
  }

  public List<AccountState> byExternalId(String scheme, String id) throws OrmException {
    return byExternalId(ExternalId.Key.create(scheme, id));
  }

  public List<AccountState> byExternalId(ExternalId.Key externalId) throws OrmException {
    return query(AccountPredicates.externalId(externalId.toString()));
  }

  public AccountState oneByExternalId(String externalId) throws OrmException {
    return oneByExternalId(ExternalId.Key.parse(externalId));
  }

  public AccountState oneByExternalId(String scheme, String id) throws OrmException {
    return oneByExternalId(ExternalId.Key.create(scheme, id));
  }

  public AccountState oneByExternalId(ExternalId.Key externalId) throws OrmException {
    List<AccountState> accountStates = byExternalId(externalId);
    if (accountStates.size() == 1) {
      return accountStates.get(0);
    } else if (accountStates.size() > 0) {
      StringBuilder msg = new StringBuilder();
      msg.append("Ambiguous external ID ").append(externalId).append("for accounts: ");
      Joiner.on(", ")
          .appendTo(msg, Lists.transform(accountStates, AccountState.ACCOUNT_ID_FUNCTION));
      log.warn(msg.toString());
    }
    return null;
  }

  public List<AccountState> byFullName(String fullName) throws OrmException {
    return query(AccountPredicates.fullName(fullName));
  }

  /**
   * Queries for accounts that have a preferred email that exactly matches the given email.
   *
   * @param email preferred email by which accounts should be found
   * @return list of accounts that have a preferred email that exactly matches the given email
   * @throws OrmException if query cannot be parsed
   */
  public List<AccountState> byPreferredEmail(String email) throws OrmException {
    if (schema().hasField(AccountField.PREFERRED_EMAIL_EXACT)) {
      return query(AccountPredicates.preferredEmailExact(email));
    }

    return query(AccountPredicates.preferredEmail(email))
        .stream()
        .filter(a -> a.getAccount().getPreferredEmail().equals(email))
        .collect(toList());
  }

  /**
   * Makes multiple queries for accounts by preferred email (exact match).
   *
   * @param emails preferred emails by which accounts should be found
   * @return multimap of the given emails to accounts that have a preferred email that exactly
   *     matches this email
   * @throws OrmException if query cannot be parsed
   */
  public Multimap<String, AccountState> byPreferredEmail(String... emails) throws OrmException {
    List<String> emailList = Arrays.asList(emails);

    if (schema().hasField(AccountField.PREFERRED_EMAIL_EXACT)) {
      List<List<AccountState>> r =
          query(
              emailList
                  .stream()
                  .map(e -> AccountPredicates.preferredEmailExact(e))
                  .collect(toList()));
      Multimap<String, AccountState> accountsByEmail = ArrayListMultimap.create();
      for (int i = 0; i < emailList.size(); i++) {
        accountsByEmail.putAll(emailList.get(i), r.get(i));
      }
      return accountsByEmail;
    }

    List<List<AccountState>> r =
        query(emailList.stream().map(e -> AccountPredicates.preferredEmail(e)).collect(toList()));
    Multimap<String, AccountState> accountsByEmail = ArrayListMultimap.create();
    for (int i = 0; i < emailList.size(); i++) {
      String email = emailList.get(i);
      Set<AccountState> matchingAccounts =
          r.get(i)
              .stream()
              .filter(a -> a.getAccount().getPreferredEmail().equals(email))
              .collect(toSet());
      accountsByEmail.putAll(email, matchingAccounts);
    }
    return accountsByEmail;
  }

  public List<AccountState> byWatchedProject(Project.NameKey project) throws OrmException {
    return query(AccountPredicates.watchedProject(project));
  }
}

@@ -968,16 +968,55 @@
 Config;%0A
+import com.google.gerrit.index.Schema;%0A
 import c
@@ -4489,60 +4489,31 @@
 if (
-schema().hasField(AccountField.PREFERRED_EMAIL_EXACT
+hasPreferredEmailExact(
 )) %7B
@@ -5265,60 +5265,31 @@
 if (
-schema().hasField(AccountField.PREFERRED_EMAIL_EXACT
+hasPreferredEmailExact(
 )) %7B
@@ -6488,11 +6488,174 @@
 t));%0A  %7D
+%0A%0A  private boolean hasPreferredEmailExact() %7B%0A    Schema%3CAccountState%3E s = schema();%0A    return (s != null && s.hasField(AccountField.PREFERRED_EMAIL_EXACT));%0A  %7D
 %0A%7D%0A

Check null before use of schema().