content: /*
   * Copyright (C) 2021 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.helper.widget;
  
  import android.content.Context;
  import android.content.res.TypedArray;
  import android.util.AttributeSet;
  import android.util.Log;
  import android.view.View;
  
  import androidx.constraintlayout.widget.ConstraintLayout;
  import androidx.constraintlayout.widget.ConstraintSet;
  import androidx.constraintlayout.widget.R;
  import androidx.constraintlayout.widget.VirtualLayout;
  
  import java.util.Arrays;
  
  /**
   *
   * CircularFlow virtual layout.
   *
   * Allows positioning of referenced widgets circular.
   *
   * The elements referenced are indicated via constraint_referenced_ids, as with other
   * ConstraintHelper implementations.
   *
   * XML attributes that are needed:
   * <ul>
   *     <li>constraint_referenced_ids = "view2, view3, view4,view5,view6".
   *     It receives id's of the views that will add the references.</li>
   *     <li>circularflow_viewCenter = "view1". It receives the id of the view of the center where
   *     the views received in constraint_referenced_ids will be referenced.</li>
   *     <li>circularflow_angles = "45,90,135,180,225". Receive the angles that you
   *     will assign to each view.</li>
   *     <li>circularflow_radiusInDP = "90,100,110,120,130". Receive the radios in DP that you
   *     will assign to each view.</li>
   * </ul>
   *
   * Example in XML:
   * <androidx.constraintlayout.helper.widget.CircularFlow
   *         android:id="@+id/circularFlow"
   *         android:layout_width="match_parent"
   *         android:layout_height="match_parent"
   *         app:circularflow_angles="0,40,80,120"
   *         app:circularflow_radiusInDP="90,100,110,120"
   *         app:circularflow_viewCenter="@+id/view1"
   *         app:constraint_referenced_ids="view2,view3,view4,view5" />
   *
   * DEFAULT radius - If you add a view and don't set its radius, the default value will be 0.
   * DEFAULT angles - If you add a view and don't set its angle, the default value will be 0.
   *
   * Recommendation - always set radius and angle for all views in <i>constraint_referenced_ids</i>
   *
   **/
  
  public class CircularFlow extends VirtualLayout {
      private static final String TAG = "CircularFlow";
      ConstraintLayout mContainer;
      int mViewCenter;
      private static int sDefaultRadius = 0;
      private static float sDefaultAngle = 0F;
      /**
       *
       */
      private float[] mAngles;
  
      /**
       *
       */
      private int[] mRadius;
  
      /**
       *
       */
      private int mCountRadius;
  
      /**
       *
       */
      private int mCountAngle;
  
      /**
       *
       */
      private String mReferenceAngles;
  
      /**
       *
       */
      private String mReferenceRadius;
  
      /**
       *
       */
      private Float mReferenceDefaultAngle;
  
      /**
       *
       */
      private Integer mReferenceDefaultRadius;
  
  
      public CircularFlow(Context context) {
          super(context);
      }
  
      public CircularFlow(Context context, AttributeSet attrs) {
          super(context, attrs);
      }
  
      public CircularFlow(Context context, AttributeSet attrs, int defStyleAttr) {
          super(context, attrs, defStyleAttr);
      }
  
      public int[] getRadius() {
          return Arrays.copyOf(mRadius, mCountRadius);
      }
  
  
      public float[] getAngles() {
          return Arrays.copyOf(mAngles, mCountAngle);
      }
  
  
      @Override
      protected void init(AttributeSet attrs) {
          super.init(attrs);
          if (attrs != null) {
              TypedArray a = getContext().obtainStyledAttributes(attrs,
                      R.styleable.ConstraintLayout_Layout);
              final int n = a.getIndexCount();
  
              for (int i = 0; i < n; i++) {
                  int attr = a.getIndex(i);
                  if (attr == R.styleable.ConstraintLayout_Layout_circularflow_viewCenter) {
                      mViewCenter = a.getResourceId(attr, 0);
                  } else if (attr == R.styleable.ConstraintLayout_Layout_circularflow_angles) {
                      mReferenceAngles = a.getString(attr);
                      setAngles(mReferenceAngles);
                  } else if (attr == R.styleable.ConstraintLayout_Layout_circularflow_radiusInDP) {
                      mReferenceRadius = a.getString(attr);
                      setRadius(mReferenceRadius);
                  } else if (attr == R.styleable.ConstraintLayout_Layout_circularflow_defaultAngle) {
                      mReferenceDefaultAngle = a.getFloat(attr, sDefaultAngle);
                      setDefaultAngle(mReferenceDefaultAngle);
                  } else if (attr == R.styleable.ConstraintLayout_Layout_circularflow_defaultRadius) {
                      mReferenceDefaultRadius = a.getDimensionPixelSize(attr, sDefaultRadius);
                      setDefaultRadius(mReferenceDefaultRadius);
                  }
              }
              a.recycle();
          }
      }
  
      @Override
      public void onAttachedToWindow() {
          super.onAttachedToWindow();
          if (mReferenceAngles != null) {
              mAngles = new float[1];
              setAngles(mReferenceAngles);
          }
          if (mReferenceRadius != null) {
              mRadius = new int[1];
              setRadius(mReferenceRadius);
          }
          if (mReferenceDefaultAngle != null) {
              setDefaultAngle(mReferenceDefaultAngle);
          }
          if (mReferenceDefaultRadius != null) {
              setDefaultRadius(mReferenceDefaultRadius);
          }
          anchorReferences();
      }
  
      private void anchorReferences() {
          mContainer = (ConstraintLayout) getParent();
          for (int i = 0; i < mCount; i++) {
              View view = mContainer.getViewById(mIds[i]);
              if (view == null) {
                  continue;
              }
              int radius = sDefaultRadius;
              float angle = sDefaultAngle;
  
              if (mRadius != null && i < mRadius.length) {
                  radius = mRadius[i];
              } else if (mReferenceDefaultRadius != null && mReferenceDefaultRadius != -1) {
                  mCountRadius++;
                  if (mRadius == null) {
                      mRadius = new int[1];
                  }
                  mRadius = getRadius();
                  mRadius[mCountRadius - 1] = radius;
              } else {
                  Log.e("CircularFlow", "Added radius to view with id: " + mMap.get(view.getId()));
              }
  
              if (mAngles != null && i < mAngles.length) {
                  angle = mAngles[i];
              } else if (mReferenceDefaultAngle != null && mReferenceDefaultAngle != -1) {
                  mCountAngle++;
                  if (mAngles == null) {
                      mAngles = new float[1];
                  }
                  mAngles = getAngles();
                  mAngles[mCountAngle - 1] = angle;
              } else {
                  Log.e("CircularFlow",
                          "Added angle to view with id: " + mMap.get(view.getId()));
              }
              ConstraintLayout.LayoutParams params =
                      (ConstraintLayout.LayoutParams) view.getLayoutParams();
              params.circleAngle = angle;
              params.circleConstraint = mViewCenter;
              params.circleRadius = radius;
              view.setLayoutParams(params);
          }
          applyLayoutFeatures();
      }
  
      /**
       * Add a view to the CircularFlow.
       * The referenced view need to be a child of the container parent.
       * The view also need to have its id set in order to be added.
       * The views previous need to have its radius and angle set in order
       * to be added correctly a new view.
       * @param view
       * @param radius
       * @param angle
       * @return
       */
      public void addViewToCircularFlow(View view, int radius, float angle) {
          if (containsId(view.getId())) {
              return;
          }
          addView(view);
          mCountAngle++;
          mAngles = getAngles();
          mAngles[mCountAngle - 1] = angle;
          mCountRadius++;
          mRadius = getRadius();
          mRadius[mCountRadius - 1] =
                  (int) (radius * myContext.getResources().getDisplayMetrics().density);
          anchorReferences();
      }
  
      /**
       * Update radius from a view in CircularFlow.
       * The referenced view need to be a child of the container parent.
       * The view also need to have its id set in order to be added.
       * @param view
       * @param radius
       * @return
       */
      public void updateRadius(View view, int radius) {
          if (!isUpdatable(view)) {
              Log.e("CircularFlow",
                      "It was not possible to update radius to view with id: " + view.getId());
              return;
          }
          int indexView = indexFromId(view.getId());
          if (indexView > mRadius.length) {
              return;
          }
          mRadius = getRadius();
          mRadius[indexView] = (int) (radius * myContext.getResources().getDisplayMetrics().density);
          anchorReferences();
      }
  
      /**
       * Update angle from a view in CircularFlow.
       * The referenced view need to be a child of the container parent.
       * The view also need to have its id set in order to be added.
       * @param view
       * @param angle
       * @return
       */
      public void updateAngle(View view, float angle) {
          if (!isUpdatable(view)) {
              Log.e("CircularFlow",
                      "It was not possible to update angle to view with id: " + view.getId());
              return;
          }
          int indexView = indexFromId(view.getId());
          if (indexView > mAngles.length) {
              return;
          }
          mAngles = getAngles();
          mAngles[indexView] = angle;
          anchorReferences();
      }
  
      /**
       * Update angle and radius from a view in CircularFlow.
       * The referenced view need to be a child of the container parent.
       * The view also need to have its id set in order to be added.
       * @param view
       * @param radius
       * @param angle
       * @return
       */
      public void updateReference(View view, int radius, float angle) {
          if (!isUpdatable(view)) {
              Log.e("CircularFlow",
                      "It was not possible to update radius and angle to view with id: "
                              + view.getId());
              return;
          }
          int indexView = indexFromId(view.getId());
          if (getAngles().length  > indexView) {
              mAngles = getAngles();
              mAngles[indexView] = angle;
          }
          if (getRadius().length  > indexView) {
              mRadius = getRadius();
              mRadius[indexView] =
                      (int) (radius * myContext.getResources().getDisplayMetrics().density);
          }
          anchorReferences();
      }
  
      /**
       * Set default Angle for CircularFlow.
       *
       * @param angle
       * @return
       */
      public void setDefaultAngle(float angle) {
          sDefaultAngle = angle;
      }
  
      /**
       * Set default Radius for CircularFlow.
       *
       * @param radius
       * @return
       */
      public void setDefaultRadius(int radius) {
          sDefaultRadius = radius;
      }
  
      @Override
      public int removeView(View view) {
          int index = super.removeView(view);
          if (index == -1) {
              return index;
          }
          ConstraintSet c = new ConstraintSet();
          c.clone(mContainer);
          c.clear(view.getId(), ConstraintSet.CIRCLE_REFERENCE);
          c.applyTo(mContainer);
  
          if (index < mAngles.length) {
              mAngles = removeAngle(mAngles, index);
              mCountAngle--;
          }
          if (index < mRadius.length) {
              mRadius = removeRadius(mRadius, index);
              mCountRadius--;
          }
          anchorReferences();
          return index;
      }
  
      /**
       *
       */
      private float[] removeAngle(float[] angles, int index) {
          if (angles == null
                  || index < 0
                  || index >= mCountAngle) {
              return angles;
          }
  
          return removeElementFromArray(angles, index);
      }
  
      /**
       *
       */
      private int[] removeRadius(int[] radius, int index) {
          if (radius == null
                  || index < 0
                  || index >= mCountRadius) {
              return radius;
          }
  
          return removeElementFromArray(radius, index);
      }
  
      /**
       *
       */
      private void setAngles(String idList) {
          if (idList == null) {
              return;
          }
          int begin = 0;
          mCountAngle = 0;
          while (true) {
              int end = idList.indexOf(',', begin);
              if (end == -1) {
                  addAngle(idList.substring(begin).trim());
                  break;
              }
              addAngle(idList.substring(begin, end).trim());
              begin = end + 1;
          }
      }
  
      /**
       *
       */
      private void setRadius(String idList) {
          if (idList == null) {
              return;
          }
          int begin = 0;
          mCountRadius = 0;
          while (true) {
              int end = idList.indexOf(',', begin);
              if (end == -1) {
                  addRadius(idList.substring(begin).trim());
                  break;
              }
              addRadius(idList.substring(begin, end).trim());
              begin = end + 1;
          }
      }
  
      /**
       *
       */
      private void addAngle(String angleString) {
          if (angleString == null || angleString.length() == 0) {
              return;
          }
          if (myContext == null) {
              return;
          }
          if (mAngles == null) {
              return;
          }
  
          if (mCountAngle + 1 > mAngles.length) {
              mAngles = Arrays.copyOf(mAngles, mAngles.length + 1);
          }
          mAngles[mCountAngle] = Integer.parseInt(angleString);
          mCountAngle++;
      }
  
      /**
       *
       */
      private void addRadius(String radiusString) {
          if (radiusString == null || radiusString.length() == 0) {
              return;
          }
          if (myContext == null) {
              return;
          }
          if (mRadius == null) {
              return;
          }
  
          if (mCountRadius + 1 > mRadius.length) {
              mRadius = Arrays.copyOf(mRadius, mRadius.length + 1);
          }
  
          mRadius[mCountRadius] = (int) (Integer.parseInt(radiusString)
                  * myContext.getResources().getDisplayMetrics().density);
          mCountRadius++;
      }
  
      private static int[] removeElementFromArray(int[] array, int index) {
          int[] newArray = new int[array.length - 1];
  
          for (int i = 0, k = 0; i < array.length; i++) {
              if (i == index) {
                  continue;
              }
              newArray[k++] = array[i];
          }
          return newArray;
      }
  
      private static float[] removeElementFromArray(float[] array, int index) {
          float[] newArray = new float[array.length - 1];
  
          for (int i = 0, k = 0; i < array.length; i++) {
              if (i == index) {
                  continue;
              }
              newArray[k++] = array[i];
          }
          return newArray;
      }
  
      /**
       * if view is part of circular flow
       * @param view
       * @return true if the flow contains the view
       */
      public boolean isUpdatable(View view) {
          if (!containsId(view.getId())) {
              return false;
          }
          int indexView = indexFromId(view.getId());
          return indexView != -1;
      }
  }
  