content: /*
   * Copyright (C) 2015 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 androidx.constraintlayout.core.widgets;
  
  import androidx.constraintlayout.core.Cache;
  import androidx.constraintlayout.core.SolverVariable;
  import androidx.constraintlayout.core.widgets.analyzer.Grouping;
  import androidx.constraintlayout.core.widgets.analyzer.WidgetGroup;
  
  import java.util.ArrayList;
  import java.util.HashMap;
  import java.util.HashSet;
  
  /**
   * Model a constraint relation. Widgets contains anchors, and a constraint relation between
   * two widgets is made by connecting one anchor to another. The anchor will contains a pointer
   * to the target anchor if it is connected.
   */
  public class ConstraintAnchor {
  
      private static final boolean ALLOW_BINARY = false;
  
      private HashSet<ConstraintAnchor> mDependents = null;
      private int mFinalValue;
      private boolean mHasFinalValue;
  
      // @TODO: add description
      public void findDependents(int orientation, ArrayList<WidgetGroup> list, WidgetGroup group) {
          if (mDependents != null) {
              for (ConstraintAnchor anchor : mDependents) {
                  Grouping.findDependents(anchor.mOwner, orientation, list, group);
              }
          }
      }
  
      public HashSet<ConstraintAnchor> getDependents() {
          return mDependents;
      }
  
      // @TODO: add description
      public boolean hasDependents() {
          if (mDependents == null) {
              return false;
          }
          return mDependents.size() > 0;
      }
  
      // @TODO: add description
      public boolean hasCenteredDependents() {
          if (mDependents == null) {
              return false;
          }
          for (ConstraintAnchor anchor : mDependents) {
              ConstraintAnchor opposite = anchor.getOpposite();
              if (opposite.isConnected()) {
                  return true;
              }
          }
          return false;
      }
  
      // @TODO: add description
      public void setFinalValue(int finalValue) {
          this.mFinalValue = finalValue;
          this.mHasFinalValue = true;
      }
  
      // @TODO: add description
      public int getFinalValue() {
          if (!mHasFinalValue) {
              return 0;
          }
          return mFinalValue;
      }
  
      // @TODO: add description
      public void resetFinalResolution() {
          mHasFinalValue = false;
          mFinalValue = 0;
      }
  
      // @TODO: add description
      public boolean hasFinalValue() {
          return mHasFinalValue;
      }
  
      /**
       * Define the type of anchor
       */
      public enum Type {NONE, LEFT, TOP, RIGHT, BOTTOM, BASELINE, CENTER, CENTER_X, CENTER_Y}
  
      private static final int UNSET_GONE_MARGIN = Integer.MIN_VALUE;
  
      public final ConstraintWidget mOwner;
      public final Type mType;
      public ConstraintAnchor mTarget;
      public int mMargin = 0;
      int mGoneMargin = UNSET_GONE_MARGIN;
  
      SolverVariable mSolverVariable;
  
      // @TODO: add description
      public void copyFrom(ConstraintAnchor source, HashMap<ConstraintWidget, ConstraintWidget> map) {
          if (mTarget != null) {
              if (mTarget.mDependents != null) {
                  mTarget.mDependents.remove(this);
              }
          }
          if (source.mTarget != null) {
              Type type = source.mTarget.getType();
              ConstraintWidget owner = map.get(source.mTarget.mOwner);
              mTarget = owner.getAnchor(type);
          } else {
              mTarget = null;
          }
          if (mTarget != null) {
              if (mTarget.mDependents == null) {
                  mTarget.mDependents = new HashSet<>();
              }
              mTarget.mDependents.add(this);
          }
          mMargin = source.mMargin;
          mGoneMargin = source.mGoneMargin;
      }
  
      /**
       * Constructor
       *
       * @param owner the widget owner of this anchor.
       * @param type  the anchor type.
       */
      public ConstraintAnchor(ConstraintWidget owner, Type type) {
          mOwner = owner;
          mType = type;
      }
  
      /**
       * Return the solver variable for this anchor
       */
      public SolverVariable getSolverVariable() {
          return mSolverVariable;
      }
  
      /**
       * Reset the solver variable
       */
      public void resetSolverVariable(Cache cache) {
          if (mSolverVariable == null) {
              mSolverVariable = new SolverVariable(SolverVariable.Type.UNRESTRICTED, null);
          } else {
              mSolverVariable.reset();
          }
      }
  
      /**
       * Return the anchor's owner
       *
       * @return the Widget owning the anchor
       */
      public ConstraintWidget getOwner() {
          return mOwner;
      }
  
      /**
       * Return the type of the anchor
       *
       * @return type of the anchor.
       */
      public Type getType() {
          return mType;
      }
  
      /**
       * Return the connection's margin from this anchor to its target.
       *
       * @return the margin value. 0 if not connected.
       */
      public int getMargin() {
          if (mOwner.getVisibility() == ConstraintWidget.GONE) {
              return 0;
          }
          if (mGoneMargin != UNSET_GONE_MARGIN && mTarget != null
                  && mTarget.mOwner.getVisibility() == ConstraintWidget.GONE) {
              return mGoneMargin;
          }
          return mMargin;
      }
  
      /**
       * Return the connection's target (null if not connected)
       *
       * @return the ConstraintAnchor target
       */
      public ConstraintAnchor getTarget() {
          return mTarget;
      }
  
      /**
       * Resets the anchor's connection.
       */
      public void reset() {
          if (mTarget != null && mTarget.mDependents != null) {
              mTarget.mDependents.remove(this);
              if (mTarget.mDependents.size() == 0) {
                  mTarget.mDependents = null;
              }
          }
          mDependents = null;
          mTarget = null;
          mMargin = 0;
          mGoneMargin = UNSET_GONE_MARGIN;
          mHasFinalValue = false;
          mFinalValue = 0;
      }
  
      /**
       * Connects this anchor to another one.
       *
       * @return true if the connection succeeds.
       */
      public boolean connect(ConstraintAnchor toAnchor, int margin, int goneMargin,
              boolean forceConnection) {
          if (toAnchor == null) {
              reset();
              return true;
          }
          if (!forceConnection && !isValidConnection(toAnchor)) {
              return false;
          }
          mTarget = toAnchor;
          if (mTarget.mDependents == null) {
              mTarget.mDependents = new HashSet<>();
          }
          if (mTarget.mDependents != null) {
              mTarget.mDependents.add(this);
          }
          mMargin = margin;
          mGoneMargin = goneMargin;
          return true;
      }
  
  
      /**
       * Connects this anchor to another one.
       *
       * @return true if the connection succeeds.
       */
      public boolean connect(ConstraintAnchor toAnchor, int margin) {
          return connect(toAnchor, margin, UNSET_GONE_MARGIN, false);
      }
  
      /**
       * Returns the connection status of this anchor
       *
       * @return true if the anchor is connected to another one.
       */
      public boolean isConnected() {
          return mTarget != null;
      }
  
      /**
       * Checks if the connection to a given anchor is valid.
       *
       * @param anchor the anchor we want to connect to
       * @return true if it's a compatible anchor
       */
      public boolean isValidConnection(ConstraintAnchor anchor) {
          if (anchor == null) {
              return false;
          }
          Type target = anchor.getType();
          if (target == mType) {
              if (mType == Type.BASELINE
                      && (!anchor.getOwner().hasBaseline() || !getOwner().hasBaseline())) {
                  return false;
              }
              return true;
          }
          switch (mType) {
              case CENTER: {
                  // allow everything but baseline and center_x/center_y
                  return target != Type.BASELINE && target != Type.CENTER_X
                          && target != Type.CENTER_Y;
              }
              case LEFT:
              case RIGHT: {
                  boolean isCompatible = target == Type.LEFT || target == Type.RIGHT;
                  if (anchor.getOwner() instanceof Guideline) {
                      isCompatible = isCompatible || target == Type.CENTER_X;
                  }
                  return isCompatible;
              }
              case TOP:
              case BOTTOM: {
                  boolean isCompatible = target == Type.TOP || target == Type.BOTTOM;
                  if (anchor.getOwner() instanceof Guideline) {
                      isCompatible = isCompatible || target == Type.CENTER_Y;
                  }
                  return isCompatible;
              }
              case BASELINE: {
                  if (target == Type.LEFT || target == Type.RIGHT) {
                      return false;
                  }
                  return true;
              }
              case CENTER_X:
              case CENTER_Y:
              case NONE:
                  return false;
          }
          throw new AssertionError(mType.name());
      }
  
      /**
       * Return true if this anchor is a side anchor
       *
       * @return true if side anchor
       */
      public boolean isSideAnchor() {
          switch (mType) {
              case LEFT:
              case RIGHT:
              case TOP:
              case BOTTOM:
                  return true;
              case BASELINE:
              case CENTER:
              case CENTER_X:
              case CENTER_Y:
              case NONE:
                  return false;
          }
          throw new AssertionError(mType.name());
      }
  
      /**
       * Return true if the connection to the given anchor is in the
       * same dimension (horizontal or vertical)
       *
       * @param anchor the anchor we want to connect to
       * @return true if it's an anchor on the same dimension
       */
      public boolean isSimilarDimensionConnection(ConstraintAnchor anchor) {
          Type target = anchor.getType();
          if (target == mType) {
              return true;
          }
          switch (mType) {
              case CENTER: {
                  return target != Type.BASELINE;
              }
              case LEFT:
              case RIGHT:
              case CENTER_X: {
                  return target == Type.LEFT || target == Type.RIGHT || target == Type.CENTER_X;
              }
              case TOP:
              case BOTTOM:
              case CENTER_Y:
              case BASELINE: {
                  return target == Type.TOP || target == Type.BOTTOM
                          || target == Type.CENTER_Y || target == Type.BASELINE;
              }
              case NONE:
                  return false;
          }
          throw new AssertionError(mType.name());
      }
  
      /**
       * Set the margin of the connection (if there's one)
       *
       * @param margin the new margin of the connection
       */
      public void setMargin(int margin) {
          if (isConnected()) {
              mMargin = margin;
          }
      }
  
      /**
       * Set the gone margin of the connection (if there's one)
       *
       * @param margin the new margin of the connection
       */
      public void setGoneMargin(int margin) {
          if (isConnected()) {
              mGoneMargin = margin;
          }
      }
  
      /**
       * Utility function returning true if this anchor is a vertical one.
       *
       * @return true if vertical anchor, false otherwise
       */
      public boolean isVerticalAnchor() {
          switch (mType) {
              case LEFT:
              case RIGHT:
              case CENTER:
              case CENTER_X:
                  return false;
              case CENTER_Y:
              case TOP:
              case BOTTOM:
              case BASELINE:
              case NONE:
                  return true;
          }
          throw new AssertionError(mType.name());
      }
  
      /**
       * Return a string representation of this anchor
       *
       * @return string representation of the anchor
       */
      @Override
      public String toString() {
          return mOwner.getDebugName() + ":" + mType.toString();
      }
  
      /**
       * Return true if we can connect this anchor to this target.
       * We recursively follow connections in order to detect eventual cycles; if we
       * do we disallow the connection.
       * We also only allow connections to direct parent, siblings, and descendants.
       *
       * @param target the ConstraintWidget we are trying to connect to
       * @param anchor Allow anchor if it loops back to me directly
       * @return if the connection is allowed, false otherwise
       */
      public boolean isConnectionAllowed(ConstraintWidget target, ConstraintAnchor anchor) {
          if (ALLOW_BINARY) {
              if (anchor != null && anchor.getTarget() == this) {
                  return true;
              }
          }
          return isConnectionAllowed(target);
      }
  
      /**
       * Return true if we can connect this anchor to this target.
       * We recursively follow connections in order to detect eventual cycles; if we
       * do we disallow the connection.
       * We also only allow connections to direct parent, siblings, and descendants.
       *
       * @param target the ConstraintWidget we are trying to connect to
       * @return true if the connection is allowed, false otherwise
       */
      public boolean isConnectionAllowed(ConstraintWidget target) {
          HashSet<ConstraintWidget> checked = new HashSet<>();
          if (isConnectionToMe(target, checked)) {
              return false;
          }
          ConstraintWidget parent = getOwner().getParent();
          if (parent == target) { // allow connections to parent
              return true;
          }
          if (target.getParent() == parent) { // allow if we share the same parent
              return true;
          }
          return false;
      }
  
      /**
       * Recursive with check for loop
       *
       * @param checked set of things already checked
       * @return true if it is connected to me
       */
      private boolean isConnectionToMe(ConstraintWidget target, HashSet<ConstraintWidget> checked) {
          if (checked.contains(target)) {
              return false;
          }
          checked.add(target);
  
          if (target == getOwner()) {
              return true;
          }
          ArrayList<ConstraintAnchor> targetAnchors = target.getAnchors();
          for (int i = 0, targetAnchorsSize = targetAnchors.size(); i < targetAnchorsSize; i++) {
              ConstraintAnchor anchor = targetAnchors.get(i);
              if (anchor.isSimilarDimensionConnection(this) && anchor.isConnected()) {
                  if (isConnectionToMe(anchor.getTarget().getOwner(), checked)) {
                      return true;
                  }
              }
          }
          return false;
      }
  
      /**
       * Returns the opposite anchor to this one
       *
       * @return opposite anchor
       */
      public final ConstraintAnchor getOpposite() {
          switch (mType) {
              case LEFT: {
                  return mOwner.mRight;
              }
              case RIGHT: {
                  return mOwner.mLeft;
              }
              case TOP: {
                  return mOwner.mBottom;
              }
              case BOTTOM: {
                  return mOwner.mTop;
              }
              case BASELINE:
              case CENTER:
              case CENTER_X:
              case CENTER_Y:
              case NONE:
                  return null;
          }
          throw new AssertionError(mType.name());
      }
  }
  