/**
 * Copyright (c) 2014, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 */
package com.facebook.csslayout;

import javax.annotation.Nullable;

import java.util.ArrayList;

import com.facebook.infer.annotation.Assertions;

import static com.facebook.csslayout.CSSLayout.DIMENSION_HEIGHT;
import static com.facebook.csslayout.CSSLayout.DIMENSION_WIDTH;
import static com.facebook.csslayout.CSSLayout.POSITION_BOTTOM;
import static com.facebook.csslayout.CSSLayout.POSITION_LEFT;
import static com.facebook.csslayout.CSSLayout.POSITION_RIGHT;
import static com.facebook.csslayout.CSSLayout.POSITION_TOP;

/**
 * A CSS Node. It has a style object you can manipulate at {@link #style}. After calling
 * {@link #calculateLayout()}, {@link #layout} will be filled with the results of the layout.
 */
public class CSSNode {

  private static enum LayoutState {
    /**
     * Some property of this node or its children has changes and the current values in
     * {@link #layout} are not valid.
     */
    DIRTY,

    /**
     * This node has a new layout relative to the last time {@link #markLayoutSeen()} was called.
     */
    HAS_NEW_LAYOUT,

    /**
     * {@link #layout} is valid for the node's properties and this layout has been marked as
     * having been seen.
     */
    UP_TO_DATE,
  }

  public static interface MeasureFunction {

    /**
     * Should measure the given node and put the result in the given MeasureOutput.
     *
     * NB: measure is NOT guaranteed to be threadsafe/re-entrant safe!
     */
    public void measure(CSSNode node, float width, MeasureOutput measureOutput);
  }

  // VisibleForTesting
  /*package*/ final CSSStyle style = new CSSStyle();
  /*package*/ final CSSLayout layout = new CSSLayout();
  /*package*/ final CachedCSSLayout lastLayout = new CachedCSSLayout();

  public int lineIndex = 0;

  /*package*/ CSSNode nextAbsoluteChild;
  /*package*/ CSSNode nextFlexChild;

  private @Nullable ArrayList<CSSNode> mChildren;
  private @Nullable CSSNode mParent;
  private @Nullable MeasureFunction mMeasureFunction = null;
  private LayoutState mLayoutState = LayoutState.DIRTY;

  public int getChildCount() {
    return mChildren == null ? 0 : mChildren.size();
  }

  public CSSNode getChildAt(int i) {
    Assertions.assertNotNull(mChildren);
    return mChildren.get(i);
  }

  public void addChildAt(CSSNode child, int i) {
    if (child.mParent != null) {
      throw new IllegalStateException("Child already has a parent, it must be removed first.");
    }
    if (mChildren == null) {
      // 4 is kinda arbitrary, but the default of 10 seems really high for an average View.
      mChildren = new ArrayList<>(4);
    }

    mChildren.add(i, child);
    child.mParent = this;
    dirty();
  }

  public CSSNode removeChildAt(int i) {
    Assertions.assertNotNull(mChildren);
    CSSNode removed = mChildren.remove(i);
    removed.mParent = null;
    dirty();
    return removed;
  }

  public @Nullable CSSNode getParent() {
    return mParent;
  }

  /**
   * @return the index of the given child, or -1 if the child doesn't exist in this node.
   */
  public int indexOf(CSSNode child) {
    Assertions.assertNotNull(mChildren);
    return mChildren.indexOf(child);
  }

  public void setMeasureFunction(MeasureFunction measureFunction) {
    if (mMeasureFunction != measureFunction) {
      mMeasureFunction = measureFunction;
      dirty();
    }
  }

  public boolean isMeasureDefined() {
    return mMeasureFunction != null;
  }

  /*package*/ MeasureOutput measure(MeasureOutput measureOutput, float width) {
    if (!isMeasureDefined()) {
      throw new RuntimeException("Measure function isn't defined!");
    }
    measureOutput.height = CSSConstants.UNDEFINED;
    measureOutput.width = CSSConstants.UNDEFINED;
    Assertions.assertNotNull(mMeasureFunction).measure(this, width, measureOutput);
    return measureOutput;
  }

  /**
   * Performs the actual layout and saves the results in {@link #layout}
   */
  public void calculateLayout(CSSLayoutContext layoutContext) {
    layout.resetResult();
    LayoutEngine.layoutNode(layoutContext, this, CSSConstants.UNDEFINED, null);
  }

  /**
   * See {@link LayoutState#DIRTY}.
   */
  protected boolean isDirty() {
    return mLayoutState == LayoutState.DIRTY;
  }

  /**
   * See {@link LayoutState#HAS_NEW_LAYOUT}.
   */
  public boolean hasNewLayout() {
    return mLayoutState == LayoutState.HAS_NEW_LAYOUT;
  }

  protected void dirty() {
    if (mLayoutState == LayoutState.DIRTY) {
      return;
    } else if (mLayoutState == LayoutState.HAS_NEW_LAYOUT) {
      throw new IllegalStateException("Previous layout was ignored! markLayoutSeen() never called");
    }

    mLayoutState = LayoutState.DIRTY;

    if (mParent != null) {
      mParent.dirty();
    }
  }

  /*package*/ void markHasNewLayout() {
    mLayoutState = LayoutState.HAS_NEW_LAYOUT;
  }

  /**
   * Tells the node that the current values in {@link #layout} have been seen. Subsequent calls
   * to {@link #hasNewLayout()} will return false until this node is laid out with new parameters.
   * You must call this each time the layout is generated if the node has a new layout.
   */
  public void markLayoutSeen() {
    if (!hasNewLayout()) {
      throw new IllegalStateException("Expected node to have a new layout to be seen!");
    }

    mLayoutState = LayoutState.UP_TO_DATE;
  }

  private void toStringWithIndentation(StringBuilder result, int level) {
    // Spaces and tabs are dropped by IntelliJ logcat integration, so rely on __ instead.
    StringBuilder indentation = new StringBuilder();
    for (int i = 0; i < level; ++i) {
      indentation.append("__");
    }

    result.append(indentation.toString());
    result.append(layout.toString());

    if (getChildCount() == 0) {
      return;
    }

    result.append(", children: [\n");
    for (int i = 0; i < getChildCount(); i++) {
      getChildAt(i).toStringWithIndentation(result, level + 1);
      result.append("\n");
    }
    result.append(indentation + "]");
  }

  @Override
  public String toString() {
    StringBuilder sb = new StringBuilder();
    this.toStringWithIndentation(sb, 0);
    return sb.toString();
  }

  protected boolean valuesEqual(float f1, float f2) {
    return FloatUtil.floatsEqual(f1, f2);
  }

  /**
   * Get this node's direction, as defined in the style.
   */
  public CSSDirection getStyleDirection() {
    return style.direction;
  }

  public void setDirection(CSSDirection direction) {
    if (style.direction != direction) {
      style.direction = direction;
      dirty();
    }
  }

  /**
   * Get this node's flex direction, as defined by style.
   */
  public CSSFlexDirection getFlexDirection() {
    return style.flexDirection;
  }

  public void setFlexDirection(CSSFlexDirection flexDirection) {
    if (style.flexDirection != flexDirection) {
      style.flexDirection = flexDirection;
      dirty();
    }
  }

  /**
   * Get this node's justify content, as defined by style.
   */
  public CSSJustify getJustifyContent() {
    return style.justifyContent;
  }

  public void setJustifyContent(CSSJustify justifyContent) {
    if (style.justifyContent != justifyContent) {
      style.justifyContent = justifyContent;
      dirty();
    }
  }

  /**
   * Get this node's align items, as defined by style.
   */
  public CSSAlign getAlignItems() {
    return style.alignItems;
  }

  public void setAlignItems(CSSAlign alignItems) {
    if (style.alignItems != alignItems) {
      style.alignItems = alignItems;
      dirty();
    }
  }

  /**
   * Get this node's align items, as defined by style.
   */
  public CSSAlign getAlignSelf() {
    return style.alignSelf;
  }

  public void setAlignSelf(CSSAlign alignSelf) {
    if (style.alignSelf != alignSelf) {
      style.alignSelf = alignSelf;
      dirty();
    }
  }

  /**
   * Get this node's position type, as defined by style.
   */
  public CSSPositionType getPositionType() {
    return style.positionType;
  }

  public void setPositionType(CSSPositionType positionType) {
    if (style.positionType != positionType) {
      style.positionType = positionType;
      dirty();
    }
  }

  public void setWrap(CSSWrap flexWrap) {
    if (style.flexWrap != flexWrap) {
      style.flexWrap = flexWrap;
      dirty();
    }
  }

  /**
   * Get this node's flex, as defined by style.
   */
  public float getFlex() {
    return style.flex;
  }

  public void setFlex(float flex) {
    if (!valuesEqual(style.flex, flex)) {
      style.flex = flex;
      dirty();
    }
  }

  /**
   * Get this node's margin, as defined by style + default margin.
   */
  public Spacing getMargin() {
    return style.margin;
  }

  public void setMargin(int spacingType, float margin) {
    if (style.margin.set(spacingType, margin)) {
      dirty();
    }
  }

  /**
   * Get this node's padding, as defined by style + default padding.
   */
  public Spacing getPadding() {
    return style.padding;
  }

  public void setPadding(int spacingType, float padding) {
    if (style.padding.set(spacingType, padding)) {
      dirty();
    }
  }

  /**
   * Get this node's border, as defined by style.
   */
  public Spacing getBorder() {
    return style.border;
  }

  public void setBorder(int spacingType, float border) {
    if (style.border.set(spacingType, border)) {
      dirty();
    }
  }

  /**
   * Get this node's position top, as defined by style.
   */
  public float getPositionTop() {
    return style.position[POSITION_TOP];
  }

  public void setPositionTop(float positionTop) {
    if (!valuesEqual(style.position[POSITION_TOP], positionTop)) {
      style.position[POSITION_TOP] = positionTop;
      dirty();
    }
  }

  /**
   * Get this node's position bottom, as defined by style.
   */
  public float getPositionBottom() {
    return style.position[POSITION_BOTTOM];
  }

  public void setPositionBottom(float positionBottom) {
    if (!valuesEqual(style.position[POSITION_BOTTOM], positionBottom)) {
      style.position[POSITION_BOTTOM] = positionBottom;
      dirty();
    }
  }

  /**
   * Get this node's position left, as defined by style.
   */
  public float getPositionLeft() {
    return style.position[POSITION_LEFT];
  }

  public void setPositionLeft(float positionLeft) {
    if (!valuesEqual(style.position[POSITION_LEFT], positionLeft)) {
      style.position[POSITION_LEFT] = positionLeft;
      dirty();
    }
  }

  /**
   * Get this node's position right, as defined by style.
   */
  public float getPositionRight() {
    return style.position[POSITION_RIGHT];
  }

  public void setPositionRight(float positionRight) {
    if (!valuesEqual(style.position[POSITION_RIGHT], positionRight)) {
      style.position[POSITION_RIGHT] = positionRight;
      dirty();
    }
  }

  /**
   * Get this node's width, as defined in the style.
   */
  public float getStyleWidth() {
    return style.dimensions[DIMENSION_WIDTH];
  }

  public void setStyleWidth(float width) {
    if (!valuesEqual(style.dimensions[DIMENSION_WIDTH], width)) {
      style.dimensions[DIMENSION_WIDTH] = width;
      dirty();
    }
  }

  /**
   * Get this node's height, as defined in the style.
   */
  public float getStyleHeight() {
    return style.dimensions[DIMENSION_HEIGHT];
  }

  public void setStyleHeight(float height) {
    if (!valuesEqual(style.dimensions[DIMENSION_HEIGHT], height)) {
      style.dimensions[DIMENSION_HEIGHT] = height;
      dirty();
    }
  }

  public float getLayoutX() {
    return layout.position[POSITION_LEFT];
  }

  public float getLayoutY() {
    return layout.position[POSITION_TOP];
  }

  public float getLayoutWidth() {
    return layout.dimensions[DIMENSION_WIDTH];
  }

  public float getLayoutHeight() {
    return layout.dimensions[DIMENSION_HEIGHT];
  }

  public CSSDirection getLayoutDirection() {
    return layout.direction;
  }

  /**
   * Set a default padding (left/top/right/bottom) for this node.
   */
  public void setDefaultPadding(int spacingType, float padding) {
    if (style.padding.setDefault(spacingType, padding)) {
      dirty();
    }
  }

  /**
   * Resets this instance to its default state. This method is meant to be used when
   * recycling {@link CSSNode} instances.
   */
  public void reset() {
    if (mParent != null || (mChildren != null && mChildren.size() > 0)) {
      throw new IllegalStateException("You should not reset an attached CSSNode");
    }

    style.reset();
    layout.resetResult();
    lineIndex = 0;
    mLayoutState = LayoutState.DIRTY;
  }
}
