content: /*
   * Copyright (C) 2018 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.motion.widget;
  
  import android.content.Context;
  import android.content.res.TypedArray;
  import android.graphics.RectF;
  import android.util.AttributeSet;
  import android.util.Log;
  import android.util.SparseIntArray;
  import android.util.TypedValue;
  import android.view.View;
  import android.view.ViewGroup;
  
  import androidx.constraintlayout.motion.utils.ViewSpline;
  import androidx.constraintlayout.widget.ConstraintAttribute;
  import androidx.constraintlayout.widget.R;
  
  import java.lang.reflect.Method;
  import java.util.HashMap;
  import java.util.HashSet;
  import java.util.Locale;
  
  /**
   * Defines container for a key frame of for storing KeyAttributes.
   * KeyAttributes change post layout values of a view.
   *
   *
   */
  
  public class KeyTrigger extends Key {
      public static final String VIEW_TRANSITION_ON_CROSS = "viewTransitionOnCross";
      public static final String VIEW_TRANSITION_ON_POSITIVE_CROSS = "viewTransitionOnPositiveCross";
      public static final String VIEW_TRANSITION_ON_NEGATIVE_CROSS = "viewTransitionOnNegativeCross";
      public static final String POST_LAYOUT = "postLayout";
      public static final String TRIGGER_SLACK = "triggerSlack";
      public static final String TRIGGER_COLLISION_VIEW = "triggerCollisionView";
      public static final String TRIGGER_COLLISION_ID = "triggerCollisionId";
      public static final String TRIGGER_ID = "triggerID";
      public static final String POSITIVE_CROSS = "positiveCross";
      public static final String NEGATIVE_CROSS = "negativeCross";
      public static final String TRIGGER_RECEIVER = "triggerReceiver";
      public static final String CROSS = "CROSS";
      public static final int KEY_TYPE = 5;
      static final String NAME = "KeyTrigger";
      private static final String TAG = "KeyTrigger";
      float mTriggerSlack = .1f;
      int mViewTransitionOnNegativeCross = UNSET;
      int mViewTransitionOnPositiveCross = UNSET;
      int mViewTransitionOnCross = UNSET;
      RectF mCollisionRect = new RectF();
      RectF mTargetRect = new RectF();
      HashMap<String, Method> mMethodHashMap = new HashMap<>();
      private int mCurveFit = -1;
      private String mCross = null;
      private int mTriggerReceiver = UNSET;
      private String mNegativeCross = null;
      private String mPositiveCross = null;
      private int mTriggerID = UNSET;
      private int mTriggerCollisionId = UNSET;
      private View mTriggerCollisionView = null;
      private boolean mFireCrossReset = true;
      private boolean mFireNegativeReset = true;
      private boolean mFirePositiveReset = true;
      private float mFireThreshold = Float.NaN;
      private float mFireLastPos;
      private boolean mPostLayout = false;
  
      {
          mType = KEY_TYPE;
          mCustomConstraints = new HashMap<>();
      }
  
      @Override
      public void load(Context context, AttributeSet attrs) {
          TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.KeyTrigger);
          Loader.read(this, a, context);
      }
  
      /**
       * Gets the curve fit type this drives the interpolation
       *
       * @return
       */
      int getCurveFit() {
          return mCurveFit;
      }
  
      @Override
      public void getAttributeNames(HashSet<String> attributes) {
      }
  
      @Override
      public void addValues(HashMap<String, ViewSpline> splines) {
      }
  
      @Override
      public void setValue(String tag, Object value) {
          switch (tag) {
              case CROSS:
                  mCross = value.toString();
                  break;
              case TRIGGER_RECEIVER:
                  mTriggerReceiver = toInt(value);
                  break;
              case NEGATIVE_CROSS:
                  mNegativeCross = value.toString();
                  break;
              case POSITIVE_CROSS:
                  mPositiveCross = value.toString();
                  break;
              case TRIGGER_ID:
                  mTriggerID = toInt(value);
                  break;
              case TRIGGER_COLLISION_ID:
                  mTriggerCollisionId = toInt(value);
                  break;
              case TRIGGER_COLLISION_VIEW:
                  mTriggerCollisionView = (View) value;
                  break;
              case TRIGGER_SLACK:
                  mTriggerSlack = toFloat(value);
                  break;
              case POST_LAYOUT:
                  mPostLayout = toBoolean(value);
                  break;
              case VIEW_TRANSITION_ON_NEGATIVE_CROSS:
                  mViewTransitionOnNegativeCross = toInt(value);
                  break;
              case VIEW_TRANSITION_ON_POSITIVE_CROSS:
                  mViewTransitionOnPositiveCross = toInt(value);
                  break;
              case VIEW_TRANSITION_ON_CROSS:
                  mViewTransitionOnCross = toInt(value);
                  break;
  
          }
      }
  
      private void setUpRect(RectF rect, View child, boolean postLayout) {
          rect.top = child.getTop();
          rect.bottom = child.getBottom();
          rect.left = child.getLeft();
          rect.right = child.getRight();
          if (postLayout) {
              child.getMatrix().mapRect(rect);
          }
      }
  
      /**
       * This fires the keyTriggers associated with this view at that position
       *
       * @param pos   the progress
       * @param child the view
       */
      public void conditionallyFire(float pos, View child) {
          boolean fireCross = false;
          boolean fireNegative = false;
          boolean firePositive = false;
  
          if (mTriggerCollisionId != UNSET) {
              if (mTriggerCollisionView == null) {
                  mTriggerCollisionView =
                          ((ViewGroup) child.getParent()).findViewById(mTriggerCollisionId);
              }
  
              setUpRect(mCollisionRect, mTriggerCollisionView, mPostLayout);
              setUpRect(mTargetRect, child, mPostLayout);
              boolean in = mCollisionRect.intersect(mTargetRect);
              // TODO scale by mTriggerSlack
              if (in) {
                  if (mFireCrossReset) {
                      fireCross = true;
                      mFireCrossReset = false;
                  }
                  if (mFirePositiveReset) {
                      firePositive = true;
                      mFirePositiveReset = false;
                  }
                  mFireNegativeReset = true;
              } else {
                  if (!mFireCrossReset) {
                      fireCross = true;
                      mFireCrossReset = true;
                  }
                  if (mFireNegativeReset) {
                      fireNegative = true;
                      mFireNegativeReset = false;
                  }
                  mFirePositiveReset = true;
              }
  
          } else {
  
              // Check for crossing
              if (mFireCrossReset) {
  
                  float offset = pos - mFireThreshold;
                  float lastOffset = mFireLastPos - mFireThreshold;
  
                  if (offset * lastOffset < 0) { // just crossed the threshold
                      fireCross = true;
                      mFireCrossReset = false;
                  }
              } else {
                  if (Math.abs(pos - mFireThreshold) > mTriggerSlack) {
                      mFireCrossReset = true;
                  }
              }
  
              // Check for negative crossing
              if (mFireNegativeReset) {
                  float offset = pos - mFireThreshold;
                  float lastOffset = mFireLastPos - mFireThreshold;
                  if (offset * lastOffset < 0 && offset < 0) { // just crossed the threshold
                      fireNegative = true;
                      mFireNegativeReset = false;
                  }
              } else {
                  if (Math.abs(pos - mFireThreshold) > mTriggerSlack) {
                      mFireNegativeReset = true;
                  }
              }
              // Check for positive crossing
              if (mFirePositiveReset) {
                  float offset = pos - mFireThreshold;
                  float lastOffset = mFireLastPos - mFireThreshold;
                  if (offset * lastOffset < 0 && offset > 0) { // just crossed the threshold
                      firePositive = true;
                      mFirePositiveReset = false;
                  }
              } else {
                  if (Math.abs(pos - mFireThreshold) > mTriggerSlack) {
                      mFirePositiveReset = true;
                  }
              }
          }
          mFireLastPos = pos;
  
          if (fireNegative || fireCross || firePositive) {
              ((MotionLayout) child.getParent()).fireTrigger(mTriggerID, firePositive, pos);
          }
          View call = (mTriggerReceiver == UNSET) ? child :
                  ((MotionLayout) child.getParent()).findViewById(mTriggerReceiver);
  
          if (fireNegative) {
              if (mNegativeCross != null) {
                  fire(mNegativeCross, call);
              }
              if (mViewTransitionOnNegativeCross != UNSET) {
                  ((MotionLayout) child.getParent()).viewTransition(mViewTransitionOnNegativeCross,
                          call);
              }
          }
          if (firePositive) {
              if (mPositiveCross != null) {
                  fire(mPositiveCross, call);
              }
              if (mViewTransitionOnPositiveCross != UNSET) {
                  ((MotionLayout) child.getParent()).viewTransition(mViewTransitionOnPositiveCross,
                          call);
              }
          }
          if (fireCross) {
              if (mCross != null) {
                  fire(mCross, call);
              }
              if (mViewTransitionOnCross != UNSET) {
                  ((MotionLayout) child.getParent()).viewTransition(mViewTransitionOnCross, call);
              }
          }
  
      }
  
      private void fire(String str, View call) {
          if (str == null) {
              return;
          }
          if (str.startsWith(".")) {
              fireCustom(str, call);
              return;
          }
          Method method = null;
          if (mMethodHashMap.containsKey(str)) {
              method = mMethodHashMap.get(str);
              if (method == null) { // we looked up and did not find
                  return;
              }
          }
          if (method == null) {
              try {
                  method = call.getClass().getMethod(str);
                  mMethodHashMap.put(str, method);
              } catch (NoSuchMethodException e) {
                  mMethodHashMap.put(str, null); // record that we could not get this method
                  Log.e(TAG, "Could not find method \"" + str + "\"" + "on class "
                          + call.getClass().getSimpleName() + " " + Debug.getName(call));
                  return;
              }
          }
          try {
              method.invoke(call);
          } catch (Exception e) {
              Log.e(TAG, "Exception in call \"" + mCross + "\"" + "on class "
                      + call.getClass().getSimpleName() + " " + Debug.getName(call));
          }
      }
  
      private void fireCustom(String str, View view) {
          boolean callAll = str.length() == 1;
          if (!callAll) {
              str = str.substring(1).toLowerCase(Locale.ROOT);
          }
          for (String name : mCustomConstraints.keySet()) {
              String lowerCase = name.toLowerCase(Locale.ROOT);
              if (callAll || lowerCase.matches(str)) {
                  ConstraintAttribute custom = mCustomConstraints.get(name);
                  if (custom != null) {
                      custom.applyCustom(view);
                  }
              }
          }
      }
  
      /**
       * Copy the key
       *
       * @param src to be copied
       * @return self
       */
      @Override
      public Key copy(Key src) {
          super.copy(src);
          KeyTrigger k = (KeyTrigger) src;
          mCurveFit = k.mCurveFit;
          mCross = k.mCross;
          mTriggerReceiver = k.mTriggerReceiver;
          mNegativeCross = k.mNegativeCross;
          mPositiveCross = k.mPositiveCross;
          mTriggerID = k.mTriggerID;
          mTriggerCollisionId = k.mTriggerCollisionId;
          mTriggerCollisionView = k.mTriggerCollisionView;
          mTriggerSlack = k.mTriggerSlack;
          mFireCrossReset = k.mFireCrossReset;
          mFireNegativeReset = k.mFireNegativeReset;
          mFirePositiveReset = k.mFirePositiveReset;
          mFireThreshold = k.mFireThreshold;
          mFireLastPos = k.mFireLastPos;
          mPostLayout = k.mPostLayout;
          mCollisionRect = k.mCollisionRect;
          mTargetRect = k.mTargetRect;
          mMethodHashMap = k.mMethodHashMap;
          return this;
      }
  
      /**
       * Clone this KeyAttributes
       *
       * @return
       */
      @Override
      public Key clone() {
          return new KeyTrigger().copy(this);
      }
  
      private static class Loader {
          private static final int NEGATIVE_CROSS = 1;
          private static final int POSITIVE_CROSS = 2;
          private static final int CROSS = 4;
          private static final int TRIGGER_SLACK = 5;
          private static final int TRIGGER_ID = 6;
          private static final int TARGET_ID = 7;
          private static final int FRAME_POS = 8;
          private static final int COLLISION = 9;
          private static final int POST_LAYOUT = 10;
          private static final int TRIGGER_RECEIVER = 11;
          private static final int VT_CROSS = 12;
          private static final int VT_NEGATIVE_CROSS = 13;
          private static final int VT_POSITIVE_CROSS = 14;
  
          private static SparseIntArray sAttrMap = new SparseIntArray();
  
          static {
              sAttrMap.append(R.styleable.KeyTrigger_framePosition, FRAME_POS);
              sAttrMap.append(R.styleable.KeyTrigger_onCross, CROSS);
              sAttrMap.append(R.styleable.KeyTrigger_onNegativeCross, NEGATIVE_CROSS);
              sAttrMap.append(R.styleable.KeyTrigger_onPositiveCross, POSITIVE_CROSS);
              sAttrMap.append(R.styleable.KeyTrigger_motionTarget, TARGET_ID);
              sAttrMap.append(R.styleable.KeyTrigger_triggerId, TRIGGER_ID);
              sAttrMap.append(R.styleable.KeyTrigger_triggerSlack, TRIGGER_SLACK);
              sAttrMap.append(R.styleable.KeyTrigger_motion_triggerOnCollision, COLLISION);
              sAttrMap.append(R.styleable.KeyTrigger_motion_postLayoutCollision, POST_LAYOUT);
              sAttrMap.append(R.styleable.KeyTrigger_triggerReceiver, TRIGGER_RECEIVER);
              sAttrMap.append(R.styleable.KeyTrigger_viewTransitionOnCross, VT_CROSS);
              sAttrMap.append(R.styleable.KeyTrigger_viewTransitionOnNegativeCross,
                      VT_NEGATIVE_CROSS);
              sAttrMap.append(R.styleable.KeyTrigger_viewTransitionOnPositiveCross,
                      VT_POSITIVE_CROSS);
          }
  
          public static void read(KeyTrigger c, TypedArray a,
                  @SuppressWarnings("unused") Context context) {
              final int n = a.getIndexCount();
              for (int i = 0; i < n; i++) {
                  int attr = a.getIndex(i);
                  switch (sAttrMap.get(attr)) {
                      case FRAME_POS:
                          c.mFramePosition = a.getInteger(attr, c.mFramePosition);
                          c.mFireThreshold = (c.mFramePosition + .5f) / 100f;
                          break;
                      case TARGET_ID:
                          if (MotionLayout.IS_IN_EDIT_MODE) {
                              c.mTargetId = a.getResourceId(attr, c.mTargetId);
                              if (c.mTargetId == -1) {
                                  c.mTargetString = a.getString(attr);
                              }
                          } else {
                              if (a.peekValue(attr).type == TypedValue.TYPE_STRING) {
                                  c.mTargetString = a.getString(attr);
                              } else {
                                  c.mTargetId = a.getResourceId(attr, c.mTargetId);
                              }
                          }
                          break;
                      case NEGATIVE_CROSS:
                          c.mNegativeCross = a.getString(attr);
                          break;
                      case POSITIVE_CROSS:
                          c.mPositiveCross = a.getString(attr);
                          break;
                      case CROSS:
                          c.mCross = a.getString(attr);
                          break;
                      case TRIGGER_SLACK:
                          c.mTriggerSlack = a.getFloat(attr, c.mTriggerSlack);
                          break;
                      case TRIGGER_ID:
                          c.mTriggerID = a.getResourceId(attr, c.mTriggerID);
                          break;
                      case COLLISION:
                          c.mTriggerCollisionId = a.getResourceId(attr, c.mTriggerCollisionId);
                          break;
                      case POST_LAYOUT:
                          c.mPostLayout = a.getBoolean(attr, c.mPostLayout);
                          break;
                      case TRIGGER_RECEIVER:
                          c.mTriggerReceiver = a.getResourceId(attr, c.mTriggerReceiver);
                          break;
                      case VT_NEGATIVE_CROSS:
                          c.mViewTransitionOnNegativeCross = a.getResourceId(attr,
                                  c.mViewTransitionOnNegativeCross);
                          break;
                      case VT_POSITIVE_CROSS:
                          c.mViewTransitionOnPositiveCross = a.getResourceId(attr,
                                  c.mViewTransitionOnPositiveCross);
                          break;
                      case VT_CROSS:
                          c.mViewTransitionOnCross = a.getResourceId(attr, c.mViewTransitionOnCross);
                          break;
                      default:
                          Log.e(NAME, "unused attribute 0x" + Integer.toHexString(attr)
                                  + "   " + sAttrMap.get(attr));
                          break;
                  }
              }
          }
      }
  }
  