/*
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * 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.facebook.litho;

import static androidx.annotation.Dimension.DP;

import android.graphics.ComposePathEffect;
import android.graphics.DashPathEffect;
import android.graphics.DiscretePathEffect;
import android.graphics.Path;
import android.graphics.PathDashPathEffect;
import android.graphics.PathEffect;
import androidx.annotation.AttrRes;
import androidx.annotation.ColorInt;
import androidx.annotation.ColorRes;
import androidx.annotation.DimenRes;
import androidx.annotation.Dimension;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.annotation.Px;
import com.facebook.yoga.YogaEdge;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/**
 * Represents a collection of attributes that describe how a border should be applied to a layout
 */
public class Border {
  static final int EDGE_LEFT = 0;
  static final int EDGE_TOP = 1;
  static final int EDGE_RIGHT = 2;
  static final int EDGE_BOTTOM = 3;
  static final int EDGE_COUNT = 4;

  @Retention(RetentionPolicy.SOURCE)
  @IntDef(
      flag = true,
      value = {Corner.TOP_LEFT, Corner.TOP_RIGHT, Corner.BOTTOM_RIGHT, Corner.BOTTOM_LEFT})
  public @interface Corner {
    int TOP_LEFT = 0;
    int TOP_RIGHT = 1;
    int BOTTOM_RIGHT = 2;
    int BOTTOM_LEFT = 3;
  }

  static final int RADIUS_COUNT = 4;

  final float[] mRadius = new float[RADIUS_COUNT];
  final int[] mEdgeWidths = new int[EDGE_COUNT];
  final int[] mEdgeColors = new int[EDGE_COUNT];

  PathEffect mPathEffect;

  public static Builder create(ComponentContext context) {
    return new Builder(context);
  }

  private Border() {}

  void setEdgeWidth(YogaEdge edge, int width) {
    if (width < 0) {
      throw new IllegalArgumentException(
          "Given negative border width value: " + width + " for edge " + edge.name());
    }
    setEdgeValue(mEdgeWidths, edge, width);
  }

  void setEdgeColor(YogaEdge edge, @ColorInt int color) {
    setEdgeValue(mEdgeColors, edge, color);
  }

  static int getEdgeColor(int[] colorArray, YogaEdge edge) {
    if (colorArray.length != EDGE_COUNT) {
      throw new IllegalArgumentException("Given wrongly sized array");
    }
    return colorArray[edgeIndex(edge)];
  }

  static YogaEdge edgeFromIndex(int i) {
    if (i < 0 || i >= EDGE_COUNT) {
      throw new IllegalArgumentException("Given index out of range of acceptable edges: " + i);
    }
    switch (i) {
      case EDGE_LEFT:
        return YogaEdge.LEFT;
      case EDGE_TOP:
        return YogaEdge.TOP;
      case EDGE_RIGHT:
        return YogaEdge.RIGHT;
      case EDGE_BOTTOM:
        return YogaEdge.BOTTOM;
    }
    throw new IllegalArgumentException("Given unknown edge index: " + i);
  }

  /**
   * @param values values pertaining to {@link YogaEdge}s
   * @return whether the values are equal for each edge
   */
  static boolean equalValues(int[] values) {
    if (values.length != EDGE_COUNT) {
      throw new IllegalArgumentException("Given wrongly sized array");
    }
    int lastValue = values[0];
    for (int i = 1, length = values.length; i < length; ++i) {
      if (lastValue != values[i]) {
        return false;
      }
    }
    return true;
  }

  private static void setEdgeValue(int[] edges, YogaEdge edge, int value) {
    switch (edge) {
      case ALL:
        for (int i = 0; i < EDGE_COUNT; ++i) {
          edges[i] = value;
        }
        break;
      case VERTICAL:
        edges[EDGE_TOP] = value;
        edges[EDGE_BOTTOM] = value;
        break;
      case HORIZONTAL:
        edges[EDGE_LEFT] = value;
        edges[EDGE_RIGHT] = value;
        break;
      case LEFT:
      case TOP:
      case RIGHT:
      case BOTTOM:
      case START:
      case END:
        edges[edgeIndex(edge)] = value;
        break;
    }
  }

  private static int edgeIndex(YogaEdge edge) {
    switch (edge) {
      case START:
      case LEFT:
        return EDGE_LEFT;
      case TOP:
        return EDGE_TOP;
      case END:
      case RIGHT:
        return EDGE_RIGHT;
      case BOTTOM:
        return EDGE_BOTTOM;
      case HORIZONTAL:
      case VERTICAL:
      case ALL:
        // Fall-through
    }
    throw new IllegalArgumentException("Given unsupported edge " + edge.name());
  }

  public static class Builder {
    private static final int MAX_PATH_EFFECTS = 2;
    private final Border mBorder;
    private @Nullable ResourceResolver mResourceResolver;
    private PathEffect[] mPathEffects = new PathEffect[MAX_PATH_EFFECTS];
    private int mNumPathEffects;

    Builder(ComponentContext context) {
      mResourceResolver = context.getResourceResolver();
      mBorder = new Border();
    }

    /**
     * Specifies a width for a specific edge
     *
     * <p>Note: Having a border effect with varying widths per edge is currently not supported
     *
     * @param edge The {@link YogaEdge} that will have its width modified
     * @param width The desired width in raw pixels
     */
    public Builder widthPx(YogaEdge edge, @Px int width) {
      checkNotBuilt();
      mBorder.setEdgeWidth(edge, width);
      return this;
    }

    /**
     * Specifies a width for a specific edge
     *
     * <p>Note: Having a border effect with varying widths per edge is currently not supported
     *
     * @param edge The {@link YogaEdge} that will have its width modified
     * @param width The desired width in density independent pixels
     */
    public Builder widthDip(YogaEdge edge, @Dimension(unit = DP) float width) {
      checkNotBuilt();
      return widthPx(edge, mResourceResolver.dipsToPixels(width));
    }

    /**
     * Specifies a width for a specific edge
     *
     * <p>Note: Having a border effect with varying widths per edge is currently not supported
     *
     * @param edge The {@link YogaEdge} that will have its width modified
     * @param widthRes The desired width resource to resolve
     */
    public Builder widthRes(YogaEdge edge, @DimenRes int widthRes) {
      checkNotBuilt();
      return widthPx(edge, mResourceResolver.resolveDimenSizeRes(widthRes));
    }

    /**
     * Specifies a width for a specific edge
     *
     * <p>Note: Having a border effect with varying widths per edge is currently not supported
     *
     * @param edge The {@link YogaEdge} that will have its width modified
     * @param attrId The attribute to resolve a width value from
     */
    public Builder widthAttr(YogaEdge edge, @AttrRes int attrId) {
      checkNotBuilt();
      return widthAttr(edge, attrId, 0);
    }

    /**
     * Specifies a width for a specific edge
     *
     * <p>Note: Having a border effect with varying widths per edge is currently not supported
     *
     * @param edge The {@link YogaEdge} that will have its width modified
     * @param attrId The attribute to resolve a width value from
     * @param defaultResId Default resource value to utilize if the attribute is not set
     */
    public Builder widthAttr(YogaEdge edge, @AttrRes int attrId, @DimenRes int defaultResId) {
      checkNotBuilt();
      return widthPx(edge, mResourceResolver.resolveDimenSizeAttr(attrId, defaultResId));
    }

    /**
     * Specifies the border radius for all corners
     *
     * @param radius The desired border radius for all corners
     */
    public Builder radiusPx(@Px int radius) {
      checkNotBuilt();
      for (int i = 0; i < RADIUS_COUNT; ++i) {
        mBorder.mRadius[i] = radius;
      }
      return this;
    }

    /**
     * Specifies the border radius for all corners
     *
     * @param radius The desired border radius for all corners
     */
    public Builder radiusDip(@Dimension(unit = DP) float radius) {
      checkNotBuilt();
      return radiusPx(mResourceResolver.dipsToPixels(radius));
    }

    /**
     * Specifies the border radius for all corners
     *
     * @param radiusRes The resource id to retrieve the border radius value from
     */
    public Builder radiusRes(@DimenRes int radiusRes) {
      checkNotBuilt();
      return radiusPx(mResourceResolver.resolveDimenSizeRes(radiusRes));
    }

    /**
     * Specifies the border radius for all corners
     *
     * @param attrId The attribute id to retrieve the border radius value from
     */
    public Builder radiusAttr(@AttrRes int attrId) {
      return radiusAttr(attrId, 0);
    }

    /**
     * Specifies the border radius for all corners
     *
     * @param attrId The attribute id to retrieve the border radius value from
     * @param defaultResId Default resource to utilize if the attribute is not set
     */
    public Builder radiusAttr(@AttrRes int attrId, @DimenRes int defaultResId) {
      checkNotBuilt();
      return radiusPx(mResourceResolver.resolveDimenSizeAttr(attrId, defaultResId));
    }

    /**
     * Specifies the border radius for the given corner
     *
     * @param corner The {@link Corner} to specify the radius of
     * @param radius The desired radius
     */
    public Builder radiusPx(@Corner int corner, @Px int radius) {
      checkNotBuilt();
      if (corner < 0 || corner >= RADIUS_COUNT) {
        throw new IllegalArgumentException("Given invalid corner: " + corner);
      }
      mBorder.mRadius[corner] = radius;
      return this;
    }

    /**
     * Specifies the border radius for the given corner
     *
     * @param corner The {@link Corner} to specify the radius of
     * @param radius The desired radius
     */
    public Builder radiusDip(@Corner int corner, @Dimension(unit = DP) float radius) {
      checkNotBuilt();
      return radiusPx(corner, mResourceResolver.dipsToPixels(radius));
    }

    /**
     * Specifies the border radius for the given corner
     *
     * @param corner The {@link Corner} to specify the radius of
     * @param res The desired dimension resource to use for the radius
     */
    public Builder radiusRes(@Corner int corner, @DimenRes int res) {
      checkNotBuilt();
      return radiusPx(corner, mResourceResolver.resolveDimenSizeRes(res));
    }

    /**
     * Specifies the border radius for the given corner
     *
     * @param corner The {@link Corner} to specify the radius of
     * @param attrId The attribute ID to retrieve the radius from
     * @param defaultResId Default resource ID to use if the attribute is not set
     */
    public Builder radiusAttr(@Corner int corner, @AttrRes int attrId, @DimenRes int defaultResId) {
      checkNotBuilt();
      return radiusPx(corner, mResourceResolver.resolveDimenSizeAttr(attrId, defaultResId));
    }

    /**
     * Specifies a color for a specific edge
     *
     * @param edge The {@link YogaEdge} that will have its color modified
     * @param color The raw color value to use
     */
    public Builder color(YogaEdge edge, @ColorInt int color) {
      checkNotBuilt();
      mBorder.setEdgeColor(edge, color);
      return this;
    }

    /**
     * Specifies a color for a specific edge
     *
     * @param edge The {@link YogaEdge} that will have its color modified
     * @param colorRes The color resource to use
     */
    public Builder colorRes(YogaEdge edge, @ColorRes int colorRes) {
      checkNotBuilt();
      return color(edge, mResourceResolver.resolveColorRes(colorRes));
    }

    /**
     * Applies a dash effect to the border
     *
     * <p>Specifying two effects will compose them where the first specified effect acts as the
     * outer effect and the second acts as the inner path effect, e.g. outer(inner(path))
     *
     * @param intervals Must be even-sized >= 2. Even indices specify "on" intervals and odd indices
     *     specify "off" intervals
     * @param phase Offset into the given intervals
     */
    public Builder dashEffect(float[] intervals, float phase) {
      checkNotBuilt();
      checkEffectCount();
      mPathEffects[mNumPathEffects++] = new DashPathEffect(intervals, phase);
      return this;
    }

    /**
     * Applies a corner effect to the border
     *
     * @deprecated Please use {@link #radiusPx(int)} instead
     * @param radius The amount to round sharp angles when drawing the border
     */
    @Deprecated
    public Builder cornerEffect(float radius) {
      checkNotBuilt();
      if (radius < 0f) {
        throw new IllegalArgumentException("Can't have a negative radius value");
      }
      radiusPx(Math.round(radius));
      return this;
    }

    /**
     * Applies a discrete effect to the border
     *
     * <p>Specifying two effects will compose them where the first specified effect acts as the
     * outer effect and the second acts as the inner path effect, e.g. outer(inner(path))
     *
     * @param segmentLength Length of line segments
     * @param deviation Maximum amount of deviation. Utilized value is random in the range
     *     [-deviation, deviation]
     */
    public Builder discreteEffect(float segmentLength, float deviation) {
      checkNotBuilt();
      checkEffectCount();
      mPathEffects[mNumPathEffects++] = new DiscretePathEffect(segmentLength, deviation);
      return this;
    }

    /**
     * Applies a path dash effect to the border
     *
     * <p>Specifying two effects will compose them where the first specified effect acts as the
     * outer effect and the second acts as the inner path effect, e.g. outer(inner(path))
     *
     * @param shape The path to stamp along
     * @param advance The spacing between each stamp
     * @param phase Amount to offset before the first stamp
     * @param style How to transform the shape at each position
     */
    public Builder pathDashEffect(
        Path shape, float advance, float phase, PathDashPathEffect.Style style) {
      checkNotBuilt();
      checkEffectCount();
      mPathEffects[mNumPathEffects++] = new PathDashPathEffect(shape, advance, phase, style);
      return this;
    }

    public Border build() {
      checkNotBuilt();
      mResourceResolver = null;

      if (mNumPathEffects == MAX_PATH_EFFECTS) {
        mBorder.mPathEffect = new ComposePathEffect(mPathEffects[0], mPathEffects[1]);
      } else if (mNumPathEffects > 0) {
        mBorder.mPathEffect = mPathEffects[0];
      }

      if (mBorder.mPathEffect != null && !Border.equalValues(mBorder.mEdgeWidths)) {
        throw new IllegalArgumentException(
            "Borders do not currently support different widths with a path effect");
      }
      return mBorder;
    }

    private void checkNotBuilt() {
      if (mResourceResolver == null) {
        throw new IllegalStateException("This builder has already been disposed / built!");
      }
    }

    private void checkEffectCount() {
      if (mNumPathEffects >= MAX_PATH_EFFECTS) {
        throw new IllegalArgumentException("You cannot specify more than 2 effects to compose");
      }
    }
  }
}