/*
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.react.uimanager;

import androidx.annotation.Nullable;
import com.facebook.react.bridge.Dynamic;
import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
import com.facebook.react.bridge.ReadableType;
import com.facebook.react.modules.i18nmanager.I18nUtil;
import com.facebook.react.uimanager.annotations.ReactProp;
import com.facebook.react.uimanager.annotations.ReactPropGroup;
import com.facebook.yoga.YogaAlign;
import com.facebook.yoga.YogaConstants;
import com.facebook.yoga.YogaDisplay;
import com.facebook.yoga.YogaFlexDirection;
import com.facebook.yoga.YogaJustify;
import com.facebook.yoga.YogaOverflow;
import com.facebook.yoga.YogaPositionType;
import com.facebook.yoga.YogaUnit;
import com.facebook.yoga.YogaWrap;

/**
 * Supply setters for base view layout properties such as width, height, flex properties, borders,
 * etc.
 *
 * <p>Checking for isVirtual everywhere is a hack to get around the fact that some virtual nodes
 * still have layout properties set on them in JS: for example, a component that returns a <Text>
 * may or may not be embedded in a parent text. There are better solutions that should probably be
 * explored, namely using the VirtualText class in JS and setting the correct set of validAttributes
 */
public class LayoutShadowNode extends ReactShadowNodeImpl {

  /** A Mutable version of com.facebook.yoga.YogaValue */
  private static class MutableYogaValue {
    float value;
    YogaUnit unit;

    private MutableYogaValue() {}

    private MutableYogaValue(MutableYogaValue mutableYogaValue) {
      this.value = mutableYogaValue.value;
      this.unit = mutableYogaValue.unit;
    }

    void setFromDynamic(Dynamic dynamic) {
      if (dynamic.isNull()) {
        unit = YogaUnit.UNDEFINED;
        value = YogaConstants.UNDEFINED;
      } else if (dynamic.getType() == ReadableType.String) {
        final String s = dynamic.asString();
        if (s.equals("auto")) {
          unit = YogaUnit.AUTO;
          value = YogaConstants.UNDEFINED;
        } else if (s.endsWith("%")) {
          unit = YogaUnit.PERCENT;
          value = Float.parseFloat(s.substring(0, s.length() - 1));
        } else {
          throw new IllegalArgumentException("Unknown value: " + s);
        }
      } else {
        unit = YogaUnit.POINT;
        value = PixelUtil.toPixelFromDIP(dynamic.asDouble());
      }
    }
  }

  private final MutableYogaValue mTempYogaValue;

  public LayoutShadowNode() {
    mTempYogaValue = new MutableYogaValue();
  }

  @ReactProp(name = ViewProps.WIDTH)
  public void setWidth(Dynamic width) {
    if (isVirtual()) {
      return;
    }

    mTempYogaValue.setFromDynamic(width);
    switch (mTempYogaValue.unit) {
      case POINT:
      case UNDEFINED:
        setStyleWidth(mTempYogaValue.value);
        break;
      case AUTO:
        setStyleWidthAuto();
        break;
      case PERCENT:
        setStyleWidthPercent(mTempYogaValue.value);
        break;
    }

    width.recycle();
  }

  @ReactProp(name = ViewProps.MIN_WIDTH)
  public void setMinWidth(Dynamic minWidth) {
    if (isVirtual()) {
      return;
    }

    mTempYogaValue.setFromDynamic(minWidth);
    switch (mTempYogaValue.unit) {
      case POINT:
      case UNDEFINED:
        setStyleMinWidth(mTempYogaValue.value);
        break;
      case PERCENT:
        setStyleMinWidthPercent(mTempYogaValue.value);
        break;
    }

    minWidth.recycle();
  }

  boolean mCollapsable;

  @ReactProp(name = "collapsable")
  public void setCollapsable(boolean collapsable) {
    mCollapsable = collapsable;
  }

  @ReactProp(name = ViewProps.MAX_WIDTH)
  public void setMaxWidth(Dynamic maxWidth) {
    if (isVirtual()) {
      return;
    }

    mTempYogaValue.setFromDynamic(maxWidth);
    switch (mTempYogaValue.unit) {
      case POINT:
      case UNDEFINED:
        setStyleMaxWidth(mTempYogaValue.value);
        break;
      case PERCENT:
        setStyleMaxWidthPercent(mTempYogaValue.value);
        break;
    }

    maxWidth.recycle();
  }

  @ReactProp(name = ViewProps.HEIGHT)
  public void setHeight(Dynamic height) {
    if (isVirtual()) {
      return;
    }

    mTempYogaValue.setFromDynamic(height);
    switch (mTempYogaValue.unit) {
      case POINT:
      case UNDEFINED:
        setStyleHeight(mTempYogaValue.value);
        break;
      case AUTO:
        setStyleHeightAuto();
        break;
      case PERCENT:
        setStyleHeightPercent(mTempYogaValue.value);
        break;
    }

    height.recycle();
  }

  @ReactProp(name = ViewProps.MIN_HEIGHT)
  public void setMinHeight(Dynamic minHeight) {
    if (isVirtual()) {
      return;
    }

    mTempYogaValue.setFromDynamic(minHeight);
    switch (mTempYogaValue.unit) {
      case POINT:
      case UNDEFINED:
        setStyleMinHeight(mTempYogaValue.value);
        break;
      case PERCENT:
        setStyleMinHeightPercent(mTempYogaValue.value);
        break;
    }

    minHeight.recycle();
  }

  @ReactProp(name = ViewProps.MAX_HEIGHT)
  public void setMaxHeight(Dynamic maxHeight) {
    if (isVirtual()) {
      return;
    }

    mTempYogaValue.setFromDynamic(maxHeight);
    switch (mTempYogaValue.unit) {
      case POINT:
      case UNDEFINED:
        setStyleMaxHeight(mTempYogaValue.value);
        break;
      case PERCENT:
        setStyleMaxHeightPercent(mTempYogaValue.value);
        break;
    }

    maxHeight.recycle();
  }

  @ReactProp(name = ViewProps.FLEX, defaultFloat = 0f)
  public void setFlex(float flex) {
    if (isVirtual()) {
      return;
    }
    super.setFlex(flex);
  }

  @ReactProp(name = ViewProps.FLEX_GROW, defaultFloat = 0f)
  public void setFlexGrow(float flexGrow) {
    if (isVirtual()) {
      return;
    }
    super.setFlexGrow(flexGrow);
  }

  @ReactProp(name = ViewProps.FLEX_SHRINK, defaultFloat = 0f)
  public void setFlexShrink(float flexShrink) {
    if (isVirtual()) {
      return;
    }
    super.setFlexShrink(flexShrink);
  }

  @ReactProp(name = ViewProps.FLEX_BASIS)
  public void setFlexBasis(Dynamic flexBasis) {
    if (isVirtual()) {
      return;
    }

    mTempYogaValue.setFromDynamic(flexBasis);
    switch (mTempYogaValue.unit) {
      case POINT:
      case UNDEFINED:
        setFlexBasis(mTempYogaValue.value);
        break;
      case AUTO:
        setFlexBasisAuto();
        break;
      case PERCENT:
        setFlexBasisPercent(mTempYogaValue.value);
        break;
    }

    flexBasis.recycle();
  }

  @ReactProp(name = ViewProps.ASPECT_RATIO, defaultFloat = YogaConstants.UNDEFINED)
  public void setAspectRatio(float aspectRatio) {
    setStyleAspectRatio(aspectRatio);
  }

  @ReactProp(name = ViewProps.FLEX_DIRECTION)
  public void setFlexDirection(@Nullable String flexDirection) {
    if (isVirtual()) {
      return;
    }

    if (flexDirection == null) {
      setFlexDirection(YogaFlexDirection.COLUMN);
      return;
    }

    switch (flexDirection) {
      case "column":
        {
          setFlexDirection(YogaFlexDirection.COLUMN);
          break;
        }
      case "column-reverse":
        {
          setFlexDirection(YogaFlexDirection.COLUMN_REVERSE);
          break;
        }
      case "row":
        {
          setFlexDirection(YogaFlexDirection.ROW);
          break;
        }
      case "row-reverse":
        {
          setFlexDirection(YogaFlexDirection.ROW_REVERSE);
          break;
        }
      default:
        {
          throw new JSApplicationIllegalArgumentException(
              "invalid value for flexDirection: " + flexDirection);
        }
    }
  }

  @ReactProp(name = ViewProps.FLEX_WRAP)
  public void setFlexWrap(@Nullable String flexWrap) {
    if (isVirtual()) {
      return;
    }

    if (flexWrap == null) {
      setFlexWrap(YogaWrap.NO_WRAP);
      return;
    }

    switch (flexWrap) {
      case "nowrap":
        {
          setFlexWrap(YogaWrap.NO_WRAP);
          break;
        }
      case "wrap":
        {
          setFlexWrap(YogaWrap.WRAP);
          break;
        }
      case "wrap-reverse":
        {
          setFlexWrap(YogaWrap.WRAP_REVERSE);
          break;
        }
      default:
        {
          throw new JSApplicationIllegalArgumentException(
              "invalid value for flexWrap: " + flexWrap);
        }
    }
  }

  @ReactProp(name = ViewProps.ALIGN_SELF)
  public void setAlignSelf(@Nullable String alignSelf) {
    if (isVirtual()) {
      return;
    }

    if (alignSelf == null) {
      setAlignSelf(YogaAlign.AUTO);
      return;
    }

    switch (alignSelf) {
      case "auto":
        {
          setAlignSelf(YogaAlign.AUTO);
          return;
        }
      case "flex-start":
        {
          setAlignSelf(YogaAlign.FLEX_START);
          return;
        }
      case "center":
        {
          setAlignSelf(YogaAlign.CENTER);
          return;
        }
      case "flex-end":
        {
          setAlignSelf(YogaAlign.FLEX_END);
          return;
        }
      case "stretch":
        {
          setAlignSelf(YogaAlign.STRETCH);
          return;
        }
      case "baseline":
        {
          setAlignSelf(YogaAlign.BASELINE);
          return;
        }
      case "space-between":
        {
          setAlignSelf(YogaAlign.SPACE_BETWEEN);
          return;
        }
      case "space-around":
        {
          setAlignSelf(YogaAlign.SPACE_AROUND);
          return;
        }
      default:
        {
          throw new JSApplicationIllegalArgumentException(
              "invalid value for alignSelf: " + alignSelf);
        }
    }
  }

  @ReactProp(name = ViewProps.ALIGN_ITEMS)
  public void setAlignItems(@Nullable String alignItems) {
    if (isVirtual()) {
      return;
    }

    if (alignItems == null) {
      setAlignItems(YogaAlign.STRETCH);
      return;
    }

    switch (alignItems) {
      case "auto":
        {
          setAlignItems(YogaAlign.AUTO);
          return;
        }
      case "flex-start":
        {
          setAlignItems(YogaAlign.FLEX_START);
          return;
        }
      case "center":
        {
          setAlignItems(YogaAlign.CENTER);
          return;
        }
      case "flex-end":
        {
          setAlignItems(YogaAlign.FLEX_END);
          return;
        }
      case "stretch":
        {
          setAlignItems(YogaAlign.STRETCH);
          return;
        }
      case "baseline":
        {
          setAlignItems(YogaAlign.BASELINE);
          return;
        }
      case "space-between":
        {
          setAlignItems(YogaAlign.SPACE_BETWEEN);
          return;
        }
      case "space-around":
        {
          setAlignItems(YogaAlign.SPACE_AROUND);
          return;
        }
      default:
        {
          throw new JSApplicationIllegalArgumentException(
              "invalid value for alignItems: " + alignItems);
        }
    }
  }

  @ReactProp(name = ViewProps.ALIGN_CONTENT)
  public void setAlignContent(@Nullable String alignContent) {
    if (isVirtual()) {
      return;
    }

    if (alignContent == null) {
      setAlignContent(YogaAlign.FLEX_START);
      return;
    }

    switch (alignContent) {
      case "auto":
        {
          setAlignContent(YogaAlign.AUTO);
          return;
        }
      case "flex-start":
        {
          setAlignContent(YogaAlign.FLEX_START);
          return;
        }
      case "center":
        {
          setAlignContent(YogaAlign.CENTER);
          return;
        }
      case "flex-end":
        {
          setAlignContent(YogaAlign.FLEX_END);
          return;
        }
      case "stretch":
        {
          setAlignContent(YogaAlign.STRETCH);
          return;
        }
      case "baseline":
        {
          setAlignContent(YogaAlign.BASELINE);
          return;
        }
      case "space-between":
        {
          setAlignContent(YogaAlign.SPACE_BETWEEN);
          return;
        }
      case "space-around":
        {
          setAlignContent(YogaAlign.SPACE_AROUND);
          return;
        }
      default:
        {
          throw new JSApplicationIllegalArgumentException(
              "invalid value for alignContent: " + alignContent);
        }
    }
  }

  @ReactProp(name = ViewProps.JUSTIFY_CONTENT)
  public void setJustifyContent(@Nullable String justifyContent) {
    if (isVirtual()) {
      return;
    }

    if (justifyContent == null) {
      setJustifyContent(YogaJustify.FLEX_START);
      return;
    }

    switch (justifyContent) {
      case "flex-start":
        {
          setJustifyContent(YogaJustify.FLEX_START);
          break;
        }
      case "center":
        {
          setJustifyContent(YogaJustify.CENTER);
          break;
        }
      case "flex-end":
        {
          setJustifyContent(YogaJustify.FLEX_END);
          break;
        }
      case "space-between":
        {
          setJustifyContent(YogaJustify.SPACE_BETWEEN);
          break;
        }
      case "space-around":
        {
          setJustifyContent(YogaJustify.SPACE_AROUND);
          break;
        }
      case "space-evenly":
        {
          setJustifyContent(YogaJustify.SPACE_EVENLY);
          break;
        }
      default:
        {
          throw new JSApplicationIllegalArgumentException(
              "invalid value for justifyContent: " + justifyContent);
        }
    }
  }

  @ReactProp(name = ViewProps.OVERFLOW)
  public void setOverflow(@Nullable String overflow) {
    if (isVirtual()) {
      return;
    }
    if (overflow == null) {
      setOverflow(YogaOverflow.VISIBLE);
      return;
    }

    switch (overflow) {
      case "visible":
        {
          setOverflow(YogaOverflow.VISIBLE);
          break;
        }
      case "hidden":
        {
          setOverflow(YogaOverflow.HIDDEN);
          break;
        }
      case "scroll":
        {
          setOverflow(YogaOverflow.SCROLL);
          break;
        }
      default:
        {
          throw new JSApplicationIllegalArgumentException(
              "invalid value for overflow: " + overflow);
        }
    }
  }

  @ReactProp(name = ViewProps.DISPLAY)
  public void setDisplay(@Nullable String display) {
    if (isVirtual()) {
      return;
    }

    if (display == null) {
      setDisplay(YogaDisplay.FLEX);
      return;
    }

    switch (display) {
      case "flex":
        {
          setDisplay(YogaDisplay.FLEX);
          break;
        }
      case "none":
        {
          setDisplay(YogaDisplay.NONE);
          break;
        }
      default:
        {
          throw new JSApplicationIllegalArgumentException("invalid value for display: " + display);
        }
    }
  }

  @ReactPropGroup(
      names = {
        ViewProps.MARGIN,
        ViewProps.MARGIN_VERTICAL,
        ViewProps.MARGIN_HORIZONTAL,
        ViewProps.MARGIN_START,
        ViewProps.MARGIN_END,
        ViewProps.MARGIN_TOP,
        ViewProps.MARGIN_BOTTOM,
        ViewProps.MARGIN_LEFT,
        ViewProps.MARGIN_RIGHT,
      })
  public void setMargins(int index, Dynamic margin) {
    if (isVirtual()) {
      return;
    }

    int spacingType =
        maybeTransformLeftRightToStartEnd(ViewProps.PADDING_MARGIN_SPACING_TYPES[index]);

    mTempYogaValue.setFromDynamic(margin);
    switch (mTempYogaValue.unit) {
      case POINT:
      case UNDEFINED:
        setMargin(spacingType, mTempYogaValue.value);
        break;
      case AUTO:
        setMarginAuto(spacingType);
        break;
      case PERCENT:
        setMarginPercent(spacingType, mTempYogaValue.value);
        break;
    }

    margin.recycle();
  }

  @ReactPropGroup(
      names = {
        ViewProps.PADDING,
        ViewProps.PADDING_VERTICAL,
        ViewProps.PADDING_HORIZONTAL,
        ViewProps.PADDING_START,
        ViewProps.PADDING_END,
        ViewProps.PADDING_TOP,
        ViewProps.PADDING_BOTTOM,
        ViewProps.PADDING_LEFT,
        ViewProps.PADDING_RIGHT,
      })
  public void setPaddings(int index, Dynamic padding) {
    if (isVirtual()) {
      return;
    }

    int spacingType =
        maybeTransformLeftRightToStartEnd(ViewProps.PADDING_MARGIN_SPACING_TYPES[index]);

    mTempYogaValue.setFromDynamic(padding);
    switch (mTempYogaValue.unit) {
      case POINT:
      case UNDEFINED:
        setPadding(spacingType, mTempYogaValue.value);
        break;
      case PERCENT:
        setPaddingPercent(spacingType, mTempYogaValue.value);
        break;
    }

    padding.recycle();
  }

  @ReactPropGroup(
      names = {
        ViewProps.BORDER_WIDTH,
        ViewProps.BORDER_START_WIDTH,
        ViewProps.BORDER_END_WIDTH,
        ViewProps.BORDER_TOP_WIDTH,
        ViewProps.BORDER_BOTTOM_WIDTH,
        ViewProps.BORDER_LEFT_WIDTH,
        ViewProps.BORDER_RIGHT_WIDTH,
      },
      defaultFloat = YogaConstants.UNDEFINED)
  public void setBorderWidths(int index, float borderWidth) {
    if (isVirtual()) {
      return;
    }
    int spacingType = maybeTransformLeftRightToStartEnd(ViewProps.BORDER_SPACING_TYPES[index]);
    setBorder(spacingType, PixelUtil.toPixelFromDIP(borderWidth));
  }

  @ReactPropGroup(
      names = {
        ViewProps.START,
        ViewProps.END,
        ViewProps.LEFT,
        ViewProps.RIGHT,
        ViewProps.TOP,
        ViewProps.BOTTOM,
      })
  public void setPositionValues(int index, Dynamic position) {
    if (isVirtual()) {
      return;
    }

    final int[] POSITION_SPACING_TYPES = {
      Spacing.START, Spacing.END, Spacing.LEFT, Spacing.RIGHT, Spacing.TOP, Spacing.BOTTOM
    };

    int spacingType = maybeTransformLeftRightToStartEnd(POSITION_SPACING_TYPES[index]);

    mTempYogaValue.setFromDynamic(position);
    switch (mTempYogaValue.unit) {
      case POINT:
      case UNDEFINED:
        setPosition(spacingType, mTempYogaValue.value);
        break;
      case PERCENT:
        setPositionPercent(spacingType, mTempYogaValue.value);
        break;
    }

    position.recycle();
  }

  private int maybeTransformLeftRightToStartEnd(int spacingType) {
    if (!I18nUtil.getInstance().doLeftAndRightSwapInRTL(getThemedContext())) {
      return spacingType;
    }

    switch (spacingType) {
      case Spacing.LEFT:
        return Spacing.START;
      case Spacing.RIGHT:
        return Spacing.END;
      default:
        return spacingType;
    }
  }

  @ReactProp(name = ViewProps.POSITION)
  public void setPosition(@Nullable String position) {
    if (isVirtual()) {
      return;
    }

    if (position == null) {
      setPositionType(YogaPositionType.RELATIVE);
      return;
    }

    switch (position) {
      case "relative":
        {
          setPositionType(YogaPositionType.RELATIVE);
          break;
        }
      case "absolute":
        {
          setPositionType(YogaPositionType.ABSOLUTE);
          break;
        }
      default:
        {
          throw new JSApplicationIllegalArgumentException(
              "invalid value for position: " + position);
        }
    }
  }

  @Override
  @ReactProp(name = "onLayout")
  public void setShouldNotifyOnLayout(boolean shouldNotifyOnLayout) {
    super.setShouldNotifyOnLayout(shouldNotifyOnLayout);
  }
}
