// 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.entities;

import static com.google.common.collect.ImmutableList.toImmutableList;

import com.google.common.collect.ImmutableList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;

/** Describes the state and edits required to submit a change. */
public class SubmitRecord {
  public static boolean allRecordsOK(Collection<SubmitRecord> in) {
    if (in == null || in.isEmpty()) {
      // If the list is null or empty, it means that this Gerrit installation does not
      // have any form of validation rules.
      // Hence, the permission system should be used to determine if the change can be merged
      // or not.
      return true;
    }

    // The change can be submitted, unless at least one plugin prevents it.
    return in.stream().map(SubmitRecord::status).allMatch(SubmitRecord.Status::allowsSubmission);
  }

  public enum Status {
    // NOTE: These values are persisted in the index, so deleting or changing
    // the name of any values requires a schema upgrade.

    /** The change is ready for submission. */
    OK,

    /** Something is preventing this change from being submitted. */
    NOT_READY,

    /** The change has been closed. */
    CLOSED,

    /** The change was submitted bypassing submit rules. */
    FORCED,

    /**
     * A rule error caused by user misconfiguration.
     *
     * <p>This status should only be used to signal that the user has misconfigured the submit rule.
     * In case plugins encounter server exceptions while evaluating the rule, they should throw a
     * {@link RuntimeException} such as {@link IllegalStateException}.
     *
     * <p>Additional detail may be available in {@link SubmitRecord#errorMessage}.
     */
    RULE_ERROR;

    private boolean allowsSubmission() {
      return this == OK || this == FORCED;
    }
  }

  public Status status;
  public List<Label> labels;
  public List<LegacySubmitRequirement> requirements;
  public String errorMessage;

  public static class Label {
    public enum Status {
      // NOTE: These values are persisted in the index, so deleting or changing
      // the name of any values requires a schema upgrade.

      /**
       * This label provides what is necessary for submission.
       *
       * <p>If provided, {@link Label#appliedBy} describes the user account that applied this label
       * to the change.
       */
      OK,

      /**
       * This label prevents the change from being submitted.
       *
       * <p>If provided, {@link Label#appliedBy} describes the user account that applied this label
       * to the change.
       */
      REJECT,

      /** The label is required for submission, but has not been satisfied. */
      NEED,

      /**
       * The label may be set, but it's neither necessary for submission nor does it block
       * submission if set.
       */
      MAY,

      /**
       * The label is required for submission, but is impossible to complete. The likely cause is
       * access has not been granted correctly by the project owner or site administrator.
       */
      IMPOSSIBLE
    }

    public String label;
    public Status status;
    public Account.Id appliedBy;

    /**
     * Returns a new instance of {@link Label} that contains a new instance for each mutable field.
     */
    public Label deepCopy() {
      Label copy = new Label();
      copy.label = label;
      copy.status = status;
      copy.appliedBy = appliedBy;
      return copy;
    }

    @Override
    public String toString() {
      StringBuilder sb = new StringBuilder();
      sb.append(label).append(": ").append(status);
      if (appliedBy != null) {
        sb.append(" by ").append(appliedBy);
      }
      return sb.toString();
    }

    @Override
    public boolean equals(Object o) {
      if (o instanceof Label) {
        Label l = (Label) o;
        return Objects.equals(label, l.label)
            && Objects.equals(status, l.status)
            && Objects.equals(appliedBy, l.appliedBy);
      }
      return false;
    }

    @Override
    public int hashCode() {
      return Objects.hash(label, status, appliedBy);
    }
  }

  /**
   * Returns a new instance of {@link SubmitRecord} that contains a new instance for each mutable
   * field.
   */
  public SubmitRecord deepCopy() {
    SubmitRecord copy = new SubmitRecord();
    copy.status = status;
    copy.errorMessage = errorMessage;
    if (labels != null) {
      copy.labels = labels.stream().map(Label::deepCopy).collect(toImmutableList());
    }
    if (requirements != null) {
      copy.requirements = ImmutableList.copyOf(requirements);
    }
    return copy;
  }

  @Override
  public String toString() {
    StringBuilder sb = new StringBuilder();
    sb.append(status);
    if (status == Status.RULE_ERROR && errorMessage != null) {
      sb.append('(').append(errorMessage).append(')');
    }
    sb.append('[');
    if (labels != null) {
      String delimiter = "";
      for (Label label : labels) {
        sb.append(delimiter).append(label);
        delimiter = ", ";
      }
    }
    sb.append("],[");
    if (requirements != null) {
      String delimiter = "";
      for (LegacySubmitRequirement requirement : requirements) {
        sb.append(delimiter).append(requirement);
        delimiter = ", ";
      }
    }
    sb.append(']');
    return sb.toString();
  }

  @Override
  public boolean equals(Object o) {
    if (o instanceof SubmitRecord) {
      SubmitRecord r = (SubmitRecord) o;
      return Objects.equals(status, r.status)
          && Objects.equals(labels, r.labels)
          && Objects.equals(errorMessage, r.errorMessage)
          && Objects.equals(requirements, r.requirements);
    }
    return false;
  }

  @Override
  public int hashCode() {
    return Objects.hash(status, labels, errorMessage, requirements);
  }

  private Status status() {
    return status;
  }
}
