// Copyright 2004-present Facebook. All Rights Reserved.

package com.facebook.react.uimanager;

import javax.annotation.Nullable;


import com.facebook.react.bridge.Dynamic;
import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
import com.facebook.react.bridge.ReadableType;

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;
import com.facebook.react.uimanager.annotations.ReactProp;
import com.facebook.react.uimanager.annotations.ReactPropGroup;

/**
 * Supply setters for base view layout properties such as width, height, flex properties,
 * borders, etc.
 *
 * 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 ReactShadowNode {

  /**
   * A Mutable version of com.facebook.yoga.YogaValue
   */
  private static class MutableYogaValue {
    float value;
    YogaUnit 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 = 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();
  }

  @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;
      }
      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;
      }
      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_LEFT,
      ViewProps.MARGIN_RIGHT,
      ViewProps.MARGIN_TOP,
      ViewProps.MARGIN_BOTTOM,
  })
  public void setMargins(int index, Dynamic margin) {
    if (isVirtual()) {
      return;
    }

    mTempYogaValue.setFromDynamic(margin);
    switch (mTempYogaValue.unit) {
      case POINT:
      case UNDEFINED:
        setMargin(ViewProps.PADDING_MARGIN_SPACING_TYPES[index], mTempYogaValue.value);
        break;
      case AUTO:
        setMarginAuto(ViewProps.PADDING_MARGIN_SPACING_TYPES[index]);
        break;
      case PERCENT:
        setMarginPercent(ViewProps.PADDING_MARGIN_SPACING_TYPES[index], mTempYogaValue.value);
        break;
    }

    margin.recycle();
  }

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

    mTempYogaValue.setFromDynamic(padding);
    switch (mTempYogaValue.unit) {
      case POINT:
      case UNDEFINED:
        setPadding(ViewProps.PADDING_MARGIN_SPACING_TYPES[index], mTempYogaValue.value);
        break;
      case PERCENT:
        setPaddingPercent(ViewProps.PADDING_MARGIN_SPACING_TYPES[index], mTempYogaValue.value);
        break;
    }

    padding.recycle();
  }

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

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

    mTempYogaValue.setFromDynamic(position);
    switch (mTempYogaValue.unit) {
      case POINT:
      case UNDEFINED:
        setPosition(ViewProps.POSITION_SPACING_TYPES[index], mTempYogaValue.value);
        break;
      case PERCENT:
        setPositionPercent(ViewProps.POSITION_SPACING_TYPES[index], mTempYogaValue.value);
        break;
    }

    position.recycle();
  }

  @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);
  }
}
