/*
 * MIT License
 *
 * Copyright (c) 2019 Choko ([email protected])
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
/*
 * Copyright (c) 2016 Google, Inc.
 *
 * 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.curioswitch.common.testing.assertj.proto;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static org.curioswitch.common.testing.assertj.proto.FieldScopeUtil.join;

import com.google.common.base.Optional;
import com.google.common.base.Verify;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.errorprone.annotations.ForOverride;
import com.google.protobuf.Descriptors.Descriptor;
import com.google.protobuf.Descriptors.FieldDescriptor;
import com.google.protobuf.Message;
import java.util.List;

/**
 * Implementations of all variations of {@link FieldScope} logic.
 *
 * <p>{@code FieldScopeLogic} is the abstract base class which provides common functionality to all
 * sub-types. There are two classes of sub-types:
 *
 * <ul>
 *   <li>Concrete subtypes, which implements specific rules and perform no delegation.
 *   <li>Compound subtypes, which combine one or more {@code FieldScopeLogic}s with specific
 *       operations.
 * </ul>
 */
abstract class FieldScopeLogic implements FieldScopeLogicContainer<FieldScopeLogic> {

  /**
   * Returns whether the given field is included in this FieldScopeLogic, along with whether it's
   * included recursively or not.
   */
  abstract FieldScopeResult policyFor(
      Descriptor rootDescriptor, FieldDescriptorOrUnknown fieldDescriptorOrUnknown);

  /** Returns whether the given field is included in this FieldScopeLogic. */
  final boolean contains(
      Descriptor rootDescriptor, FieldDescriptorOrUnknown fieldDescriptorOrUnknown) {
    return policyFor(rootDescriptor, fieldDescriptorOrUnknown).included();
  }

  /**
   * Returns a {@code FieldScopeLogic} to handle the message pointed to by this descriptor.
   *
   * <p>Subclasses which can return non-recursive {@link FieldScopeResult}s must override {@link
   * #subScopeImpl} to implement those cases.
   */
  @Override
  public final FieldScopeLogic subScope(
      Descriptor rootDescriptor, FieldDescriptorOrUnknown fieldDescriptorOrUnknown) {
    FieldScopeResult result = policyFor(rootDescriptor, fieldDescriptorOrUnknown);
    if (result.recursive()) {
      return result.included() ? all() : none();
    } else {
      return subScopeImpl(rootDescriptor, fieldDescriptorOrUnknown);
    }
  }

  /**
   * Returns {@link #subScope} for {@code NONRECURSIVE} results.
   *
   * <p>Throws an {@link UnsupportedOperationException} by default. Subclasses which can return
   * {@code NONRECURSIVE} results must override this method.
   */
  @ForOverride
  FieldScopeLogic subScopeImpl(
      Descriptor rootDescriptor, FieldDescriptorOrUnknown fieldDescriptorOrUnknown) {
    throw new UnsupportedOperationException("subScopeImpl not implemented for " + getClass());
  }

  /**
   * Returns an accurate description for debugging purposes.
   *
   * <p>Compare to {@link FieldScope#usingCorrespondenceString(Optional)}, which returns a beautiful
   * error message that makes as much sense to the user as possible.
   *
   * <p>Abstract so subclasses must implement.
   */
  @Override
  public abstract String toString();

  @Override
  public void validate(
      Descriptor rootDescriptor, FieldDescriptorValidator fieldDescriptorValidator) {}

  private static boolean isEmpty(Iterable<?> container) {
    boolean isEmpty = true;
    for (Object element : container) {
      checkNotNull(element);
      isEmpty = false;
    }

    return isEmpty;
  }

  // TODO(user): Rename these 'ignoring' and 'allowing' methods to 'plus' and 'minus', or
  // something else that doesn't tightly couple FieldScopeLogic to the 'ignore' concept.
  FieldScopeLogic ignoringFields(Iterable<Integer> fieldNumbers) {
    if (isEmpty(fieldNumbers)) {
      return this;
    }
    return and(
        this,
        new NegationFieldScopeLogic(new FieldNumbersLogic(fieldNumbers, /* isRecursive = */ true)));
  }

  FieldScopeLogic ignoringFieldDescriptors(Iterable<FieldDescriptor> fieldDescriptors) {
    if (isEmpty(fieldDescriptors)) {
      return this;
    }
    return and(
        this,
        new NegationFieldScopeLogic(
            new FieldDescriptorsLogic(fieldDescriptors, /* isRecursive = */ true)));
  }

  FieldScopeLogic allowingFields(Iterable<Integer> fieldNumbers) {
    if (isEmpty(fieldNumbers)) {
      return this;
    }
    return or(this, new FieldNumbersLogic(fieldNumbers, /* isRecursive = */ true));
  }

  FieldScopeLogic allowingFieldsNonRecursive(Iterable<Integer> fieldNumbers) {
    if (isEmpty(fieldNumbers)) {
      return this;
    }
    return or(this, new FieldNumbersLogic(fieldNumbers, /* isRecursive = */ false));
  }

  FieldScopeLogic allowingFieldDescriptors(Iterable<FieldDescriptor> fieldDescriptors) {
    if (isEmpty(fieldDescriptors)) {
      return this;
    }
    return or(this, new FieldDescriptorsLogic(fieldDescriptors, /* isRecursive = */ true));
  }

  FieldScopeLogic allowingFieldDescriptorsNonRecursive(Iterable<FieldDescriptor> fieldDescriptors) {
    if (isEmpty(fieldDescriptors)) {
      return this;
    }
    return or(this, new FieldDescriptorsLogic(fieldDescriptors, /* isRecursive = */ false));
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // CONCRETE SUBTYPES
  //////////////////////////////////////////////////////////////////////////////////////////////////

  /** Returns whether this is equivalent to {@code FieldScopeLogic.all()}. */
  boolean isAll() {
    return false;
  }

  private static final FieldScopeLogic ALL =
      new FieldScopeLogic() {
        @Override
        public String toString() {
          return "FieldScopes.all()";
        }

        @Override
        final FieldScopeResult policyFor(
            Descriptor rootDescriptor, FieldDescriptorOrUnknown fieldDescriptorOrUnknown) {
          return FieldScopeResult.INCLUDED_RECURSIVELY;
        }

        @Override
        final boolean isAll() {
          return true;
        }
      };

  private static final FieldScopeLogic NONE =
      new FieldScopeLogic() {
        @Override
        public String toString() {
          return "FieldScopes.none()";
        }

        @Override
        final FieldScopeResult policyFor(
            Descriptor rootDescriptor, FieldDescriptorOrUnknown fieldDescriptorOrUnknown) {
          return FieldScopeResult.EXCLUDED_RECURSIVELY;
        }
      };

  static FieldScopeLogic all() {
    return ALL;
  }

  static FieldScopeLogic none() {
    return NONE;
  }

  private static class PartialScopeLogic extends FieldScopeLogic {
    private static final PartialScopeLogic EMPTY = new PartialScopeLogic(FieldNumberTree.empty());

    private final FieldNumberTree fieldNumberTree;

    PartialScopeLogic(FieldNumberTree fieldNumberTree) {
      this.fieldNumberTree = fieldNumberTree;
    }

    @Override
    public String toString() {
      return String.format("PartialScopeLogic(%s)", fieldNumberTree);
    }

    @Override
    final FieldScopeResult policyFor(
        Descriptor rootDescriptor, FieldDescriptorOrUnknown fieldDescriptorOrUnknown) {
      return fieldNumberTree.hasChild(fieldDescriptorOrUnknown)
          ? FieldScopeResult.INCLUDED_NONRECURSIVELY
          : FieldScopeResult.EXCLUDED_RECURSIVELY;
    }

    @Override
    final FieldScopeLogic subScopeImpl(
        Descriptor rootDescriptor, FieldDescriptorOrUnknown fieldDescriptorOrUnknown) {
      return newPartialScopeLogic(fieldNumberTree.child(fieldDescriptorOrUnknown));
    }

    private static PartialScopeLogic newPartialScopeLogic(FieldNumberTree fieldNumberTree) {
      return fieldNumberTree.isEmpty() ? EMPTY : new PartialScopeLogic(fieldNumberTree);
    }
  }

  private static final class RootPartialScopeLogic extends PartialScopeLogic {
    private final Message message;
    private final Descriptor expectedDescriptor;

    RootPartialScopeLogic(Message message) {
      super(FieldNumberTree.fromMessage(message));
      this.message = message;
      this.expectedDescriptor = message.getDescriptorForType();
    }

    @Override
    public void validate(
        Descriptor rootDescriptor, FieldDescriptorValidator fieldDescriptorValidator) {
      Verify.verify(
          fieldDescriptorValidator == FieldDescriptorValidator.ALLOW_ALL,
          "PartialScopeLogic doesn't support custom field validators.");

      checkArgument(
          expectedDescriptor.equals(rootDescriptor),
          "Message given to FieldScopes.fromSetFields() does not have the same descriptor as the "
              + "message being tested. Expected %s, got %s.",
          expectedDescriptor.getFullName(),
          rootDescriptor.getFullName());
    }

    @Override
    public String toString() {
      return String.format("FieldScopes.fromSetFields(%s)", message);
    }
  }

  static FieldScopeLogic partialScope(Message message) {
    return new RootPartialScopeLogic(message);
  }

  // TODO(user): Performance: Optimize FieldNumbersLogic and FieldDescriptorsLogic for
  // adding / ignoring field numbers and descriptors, respectively, to eliminate high recursion
  // costs for long chains of allows/ignores.

  // Common functionality for FieldNumbersLogic and FieldDescriptorsLogic.
  private abstract static class FieldMatcherLogicBase extends FieldScopeLogic {

    private final boolean isRecursive;

    protected FieldMatcherLogicBase(boolean isRecursive) {
      this.isRecursive = isRecursive;
    }

    /**
     * Determines whether the FieldDescriptor is equal to one of the explicitly defined components
     * of this FieldScopeLogic.
     *
     * @param descriptor Descriptor of the message being tested.
     * @param fieldDescriptor FieldDescriptor being inspected for a direct match to the scope's
     *     definition.
     */
    abstract boolean matchesFieldDescriptor(Descriptor descriptor, FieldDescriptor fieldDescriptor);

    @Override
    final FieldScopeResult policyFor(
        Descriptor rootDescriptor, FieldDescriptorOrUnknown fieldDescriptorOrUnknown) {
      if (fieldDescriptorOrUnknown.unknownFieldDescriptor().isPresent()) {
        return FieldScopeResult.EXCLUDED_RECURSIVELY;
      }

      FieldDescriptor fieldDescriptor = fieldDescriptorOrUnknown.fieldDescriptor().get();
      if (matchesFieldDescriptor(rootDescriptor, fieldDescriptor)) {
        return FieldScopeResult.of(/* included = */ true, isRecursive);
      }

      // We return 'EXCLUDED_NONRECURSIVELY' for both field descriptor scopes and field number
      // scopes. In the former case, the field descriptors are arbitrary, so it's always possible we
      // find a hit on a sub-message somewhere.  In the latter case, the message definition may be
      // cyclic, so we need to return 'EXCLUDED_NONRECURSIVELY' even if the top level field number
      // doesn't match.
      return FieldScopeResult.EXCLUDED_NONRECURSIVELY;
    }

    @Override
    final FieldScopeLogic subScopeImpl(
        Descriptor rootDescriptor, FieldDescriptorOrUnknown fieldDescriptorOrUnknown) {
      return this;
    }

    @Override
    public void validate(
        Descriptor rootDescriptor, FieldDescriptorValidator fieldDescriptorValidator) {
      if (isRecursive) {
        Verify.verify(
            fieldDescriptorValidator == FieldDescriptorValidator.ALLOW_ALL,
            "Field descriptor validators are not supported "
                + "for non-recursive field matcher logics.");
      }
    }
  }

  // Matches any specific fields which fall under a sub-message field (or root) matching the root
  // message type and one of the specified field numbers.
  private static final class FieldNumbersLogic extends FieldMatcherLogicBase {
    private final ImmutableSet<Integer> fieldNumbers;

    FieldNumbersLogic(Iterable<Integer> fieldNumbers, boolean isRecursive) {
      super(isRecursive);
      this.fieldNumbers = ImmutableSet.copyOf(fieldNumbers);
    }

    @Override
    public void validate(
        Descriptor rootDescriptor, FieldDescriptorValidator fieldDescriptorValidator) {
      super.validate(rootDescriptor, fieldDescriptorValidator);
      for (int fieldNumber : fieldNumbers) {
        FieldDescriptor fieldDescriptor = rootDescriptor.findFieldByNumber(fieldNumber);
        checkArgument(
            fieldDescriptor != null,
            "Message type %s has no field with number %s.",
            rootDescriptor.getFullName(),
            fieldNumber);
        fieldDescriptorValidator.validate(fieldDescriptor);
      }
    }

    @Override
    boolean matchesFieldDescriptor(Descriptor descriptor, FieldDescriptor fieldDescriptor) {
      return fieldDescriptor.getContainingType() == descriptor
          && fieldNumbers.contains(fieldDescriptor.getNumber());
    }

    @Override
    public String toString() {
      return String.format("FieldScopes.allowingFields(%s)", join(fieldNumbers));
    }
  }

  // Matches any specific fields which fall under one of the specified FieldDescriptors.
  private static final class FieldDescriptorsLogic extends FieldMatcherLogicBase {
    private final ImmutableSet<FieldDescriptor> fieldDescriptors;

    FieldDescriptorsLogic(Iterable<FieldDescriptor> fieldDescriptors, boolean isRecursive) {
      super(isRecursive);
      this.fieldDescriptors = ImmutableSet.copyOf(fieldDescriptors);
    }

    @Override
    boolean matchesFieldDescriptor(Descriptor descriptor, FieldDescriptor fieldDescriptor) {
      return fieldDescriptors.contains(fieldDescriptor);
    }

    @Override
    public void validate(
        Descriptor rootDescriptor, FieldDescriptorValidator fieldDescriptorValidator) {
      super.validate(rootDescriptor, fieldDescriptorValidator);
      for (FieldDescriptor fieldDescriptor : fieldDescriptors) {
        fieldDescriptorValidator.validate(fieldDescriptor);
      }
    }

    @Override
    public String toString() {
      return String.format("FieldScopes.allowingFieldDescriptors(%s)", join(fieldDescriptors));
    }
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // COMPOUND SUBTYPES
  //////////////////////////////////////////////////////////////////////////////////////////////////

  private abstract static class CompoundFieldScopeLogic<T extends CompoundFieldScopeLogic<T>>
      extends FieldScopeLogic {
    final ImmutableList<FieldScopeLogic> elements;

    CompoundFieldScopeLogic(FieldScopeLogic singleElem) {
      this.elements = ImmutableList.of(singleElem);
    }

    CompoundFieldScopeLogic(FieldScopeLogic firstElem, FieldScopeLogic secondElem) {
      this.elements = ImmutableList.of(firstElem, secondElem);
    }

    @Override
    public final void validate(
        Descriptor rootDescriptor, FieldDescriptorValidator fieldDescriptorValidator) {
      for (FieldScopeLogic elem : elements) {
        elem.validate(rootDescriptor, fieldDescriptorValidator);
      }
    }

    /** Helper to produce a new {@code CompoundFieldScopeLogic} of the same type as the subclass. */
    abstract T newLogicOfSameType(List<FieldScopeLogic> newElements);

    @Override
    final FieldScopeLogic subScopeImpl(
        Descriptor rootDescriptor, FieldDescriptorOrUnknown fieldDescriptorOrUnknown) {
      ImmutableList.Builder<FieldScopeLogic> builder =
          ImmutableList.builderWithExpectedSize(elements.size());
      for (FieldScopeLogic elem : elements) {
        builder.add(elem.subScope(rootDescriptor, fieldDescriptorOrUnknown));
      }
      return newLogicOfSameType(builder.build());
    }
  }

  private static final class IntersectionFieldScopeLogic
      extends CompoundFieldScopeLogic<IntersectionFieldScopeLogic> {
    IntersectionFieldScopeLogic(FieldScopeLogic subject1, FieldScopeLogic subject2) {
      super(subject1, subject2);
    }

    @Override
    IntersectionFieldScopeLogic newLogicOfSameType(List<FieldScopeLogic> newElements) {
      checkArgument(newElements.size() == 2, "Expected 2 elements: %s", newElements);
      return new IntersectionFieldScopeLogic(newElements.get(0), newElements.get(1));
    }

    @Override
    FieldScopeResult policyFor(
        Descriptor rootDescriptor, FieldDescriptorOrUnknown fieldDescriptorOrUnknown) {
      // The intersection of two scopes is ignorable if either scope is itself ignorable.
      return intersection(
          elements.get(0).policyFor(rootDescriptor, fieldDescriptorOrUnknown),
          elements.get(1).policyFor(rootDescriptor, fieldDescriptorOrUnknown));
    }

    private static FieldScopeResult intersection(
        FieldScopeResult result1, FieldScopeResult result2) {
      if (result1 == FieldScopeResult.EXCLUDED_RECURSIVELY
          || result2 == FieldScopeResult.EXCLUDED_RECURSIVELY) {
        // If either argument is excluded recursively, the result is too.
        return FieldScopeResult.EXCLUDED_RECURSIVELY;
      } else if (!result1.included() || !result2.included()) {
        // Otherwise, we exclude non-recursively if either result is an exclusion.
        return FieldScopeResult.EXCLUDED_NONRECURSIVELY;
      } else if (result1.recursive() && result2.recursive()) {
        // We include recursively if both arguments are recursive.
        return FieldScopeResult.INCLUDED_RECURSIVELY;
      } else {
        // Otherwise, we include non-recursively.
        return FieldScopeResult.INCLUDED_NONRECURSIVELY;
      }
    }

    @Override
    public String toString() {
      return String.format("(%s && %s)", elements.get(0), elements.get(1));
    }
  }

  private static final class UnionFieldScopeLogic
      extends CompoundFieldScopeLogic<UnionFieldScopeLogic> {
    UnionFieldScopeLogic(FieldScopeLogic subject1, FieldScopeLogic subject2) {
      super(subject1, subject2);
    }

    @Override
    UnionFieldScopeLogic newLogicOfSameType(List<FieldScopeLogic> newElements) {
      checkArgument(newElements.size() == 2, "Expected 2 elements: %s", newElements);
      return new UnionFieldScopeLogic(newElements.get(0), newElements.get(1));
    }

    @Override
    FieldScopeResult policyFor(
        Descriptor rootDescriptor, FieldDescriptorOrUnknown fieldDescriptorOrUnknown) {
      // The union of two scopes is ignorable only if both scopes are themselves ignorable.
      return union(
          elements.get(0).policyFor(rootDescriptor, fieldDescriptorOrUnknown),
          elements.get(1).policyFor(rootDescriptor, fieldDescriptorOrUnknown));
    }

    private static FieldScopeResult union(FieldScopeResult result1, FieldScopeResult result2) {
      if (result1 == FieldScopeResult.INCLUDED_RECURSIVELY
          || result2 == FieldScopeResult.INCLUDED_RECURSIVELY) {
        // If either argument is included recursively, the result is too.
        return FieldScopeResult.INCLUDED_RECURSIVELY;
      } else if (result1.included() || result2.included()) {
        // Otherwise, if either is included, we include non-recursively.
        return FieldScopeResult.INCLUDED_NONRECURSIVELY;
      } else if (result1.recursive() && result2.recursive()) {
        // If both arguments are recursive, we exclude recursively.
        return FieldScopeResult.EXCLUDED_RECURSIVELY;
      } else {
        // Otherwise, we exclude exclude non-recursively.
        return FieldScopeResult.EXCLUDED_NONRECURSIVELY;
      }
    }

    @Override
    public String toString() {
      return String.format("(%s || %s)", elements.get(0), elements.get(1));
    }
  }

  private static final class NegationFieldScopeLogic
      extends CompoundFieldScopeLogic<NegationFieldScopeLogic> {
    NegationFieldScopeLogic(FieldScopeLogic subject) {
      super(subject);
    }

    @Override
    NegationFieldScopeLogic newLogicOfSameType(List<FieldScopeLogic> newElements) {
      checkArgument(newElements.size() == 1, "Expected 1 element: %s", newElements);
      return new NegationFieldScopeLogic(newElements.get(0));
    }

    @Override
    FieldScopeResult policyFor(
        Descriptor rootDescriptor, FieldDescriptorOrUnknown fieldDescriptorOrUnknown) {
      FieldScopeResult result = elements.get(0).policyFor(rootDescriptor, fieldDescriptorOrUnknown);
      return FieldScopeResult.of(!result.included(), result.recursive());
    }

    @Override
    public String toString() {
      return String.format("!(%s)", elements.get(0));
    }
  }

  static FieldScopeLogic and(FieldScopeLogic fieldScopeLogic1, FieldScopeLogic fieldScopeLogic2) {
    return new IntersectionFieldScopeLogic(fieldScopeLogic1, fieldScopeLogic2);
  }

  static FieldScopeLogic or(FieldScopeLogic fieldScopeLogic1, FieldScopeLogic fieldScopeLogic2) {
    return new UnionFieldScopeLogic(fieldScopeLogic1, fieldScopeLogic2);
  }

  static FieldScopeLogic not(FieldScopeLogic fieldScopeLogic) {
    return new NegationFieldScopeLogic(fieldScopeLogic);
  }
}