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

/* eslint-env browser */
/* global CPPEmitter:readable, JavaEmitter:readable, JavascriptEmitter:readable */

const INVISIBLE_BORDER_STYLES = ['none', 'initial'];

window.onload = function () {
  checkDefaultValues();

  printTest(
    new CPPEmitter(),
    'cpp',
    document.body.children[0],
    document.body.children[1],
    document.body.children[2],
  );

  printTest(
    new JavaEmitter(),
    'java',
    document.body.children[0],
    document.body.children[1],
    document.body.children[2],
  );

  printTest(
    new JavascriptEmitter(),
    'js',
    document.body.children[0],
    document.body.children[1],
    document.body.children[2],
  );
};

function assert(condition, message) {
  if (!condition) {
    throw new Error(message);
  }
}

function printTest(e, ext, LTRContainer, RTLContainer, genericContainer) {
  e.push([
    ext === 'js' ? '/**' : '/*',
    ' * Copyright (c) Meta Platforms, Inc. and affiliates.',
    ' *',
    ' * This source code is licensed under the MIT license found in the',
    ' * LICENSE file in the root directory of this source tree.',
    ext === 'cpp' ? ' *\n * clang-format off' : ' *',
    ` * MAGIC_PLACEHOLDER`,
    ' * generated by gentest/gentest-driver.ts from gentest/fixtures/' +
      document.title +
      '.html',
    ' */',
    '',
  ]);
  e.emitPrologue();

  const LTRLayoutTree = calculateTree(LTRContainer, 0, 0);
  const RTLLayoutTree = calculateTree(RTLContainer, 0, 0);
  const genericLayoutTree = calculateTree(genericContainer, 0, 0);

  for (let i = 0; i < genericLayoutTree.length; i++) {
    e.emitTestPrologue(
      genericLayoutTree[i].name,
      genericLayoutTree[i].experiments,
      genericLayoutTree[i].disabled,
    );

    if (genericLayoutTree[i].name == 'wrap_column') {
      // Modify width and left values due to both safari and chrome not abiding by the
      // specification. The undefined dimension of a parent should be defined by the total size
      // of their children in that dimension.
      // See diagram under flex-wrap header https://www.w3.org/TR/css-flexbox-1/
      assert(
        LTRLayoutTree[0].width == 30,
        'wrap_column LTR root.width should be 30',
      );
      LTRLayoutTree[0].width = 60;
      assert(
        RTLLayoutTree[0].width == 30,
        'wrap_column RTL root.width should be 30',
      );
      RTLLayoutTree[0].width = 60;
      const children = RTLLayoutTree[0].children;
      assert(
        children[0].left == 0,
        'wrap_column RTL root_child0.left should be 0',
      );
      children[0].left = 30;
      assert(
        children[1].left == 0,
        'wrap_column RTL root_child0.left should be 0',
      );
      children[1].left = 30;
      assert(
        children[2].left == 0,
        'wrap_column RTL root_child2.left should be 0',
      );
      children[2].left = 30;
      assert(
        children[3].left == -30,
        'wrap_column RTL root_child3.left should be -30',
      );
      children[3].left = 0;
    }

    setupTestTree(
      e,
      undefined,
      LTRLayoutTree[i],
      genericLayoutTree[i],
      'root',
      null,
    );

    e.YGNodeCalculateLayout(
      'root',
      e.YGDirectionLTR,
      genericLayoutTree[i].experiments,
    );
    e.push('');

    assertTestTree(e, LTRLayoutTree[i], 'root', null);
    e.push('');

    e.YGNodeCalculateLayout(
      'root',
      e.YGDirectionRTL,
      genericLayoutTree[i].experiments,
    );
    e.push('');

    assertTestTree(e, RTLLayoutTree[i], 'root', null);

    e.emitTestEpilogue(genericLayoutTree[i].experiments);
  }
  e.emitEpilogue();

  e.print();
}

function assertTestTree(e, node, nodeName, _parentName) {
  e.AssertEQ(node.left, e.YGNodeLayoutGetLeft(nodeName));
  e.AssertEQ(node.top, e.YGNodeLayoutGetTop(nodeName));
  e.AssertEQ(node.width, e.YGNodeLayoutGetWidth(nodeName));
  e.AssertEQ(node.height, e.YGNodeLayoutGetHeight(nodeName));

  for (let i = 0; i < node.children.length; i++) {
    e.push('');
    const childName = nodeName + '_child' + i;
    assertTestTree(e, node.children[i], childName, nodeName);
  }
}

function checkDefaultValues() {
  // Sanity check of the Yoga default values by test-template.html
  [
    {style: 'flex-direction', value: 'column'},
    {style: 'justify-content', value: 'flex-start'},
    {style: 'align-content', value: 'flex-start'},
    {style: 'align-items', value: 'stretch'},
    {style: 'position', value: 'relative'},
    {style: 'flex-wrap', value: 'nowrap'},
    {style: 'overflow', value: 'visible'},
    {style: 'flex-grow', value: '0'},
    {style: 'flex-shrink', value: '0'},
    {style: 'left', value: 'undefined'},
    {style: 'top', value: 'undefined'},
    {style: 'right', value: 'undefined'},
    {style: 'bottom', value: 'undefined'},
    {style: 'display', value: 'flex'},
    {style: 'box-sizing', value: 'border-box'},
  ].forEach(item => {
    assert(
      isDefaultStyleValue(item.style, item.value),
      item.style + ' should be ' + item.value,
    );
  });
}

function setupTestTree(
  e,
  parent,
  node,
  genericNode,
  nodeName,
  parentName,
  index,
) {
  e.emitTestTreePrologue(nodeName);
  for (const style in node.style) {
    // Skip position info for root as it messes up tests
    if (
      node.declaredStyle[style] === '' &&
      (style == 'left' ||
        style == 'top' ||
        style == 'right' ||
        style == 'bottom' ||
        style == 'width' ||
        style == 'height')
    ) {
      continue;
    }
    if (!isDefaultStyleValue(style, node.style[style])) {
      switch (style) {
        case 'aspect-ratio':
          e.YGNodeStyleSetAspectRatio(
            nodeName,
            pointValue(e, node.style[style]),
          );
          break;
        case 'gap':
          e.YGNodeStyleSetGap(
            nodeName,
            e.YGGutterAll,
            pointValue(e, node.style[style]),
          );
          break;
        case 'column-gap':
          e.YGNodeStyleSetGap(
            nodeName,
            e.YGGutterColumn,
            pointValue(e, node.style[style]),
          );
          break;
        case 'row-gap':
          e.YGNodeStyleSetGap(
            nodeName,
            e.YGGutterRow,
            pointValue(e, node.style[style]),
          );
          break;
        case 'direction':
          e.YGNodeStyleSetDirection(
            nodeName,
            directionValue(e, node.style[style]),
          );
          break;
        case 'flex-direction':
          e.YGNodeStyleSetFlexDirection(
            nodeName,
            flexDirectionValue(e, node.style[style]),
          );
          break;
        case 'justify-content':
          e.YGNodeStyleSetJustifyContent(
            nodeName,
            justifyValue(e, node.style[style]),
          );
          break;
        case 'align-content':
          e.YGNodeStyleSetAlignContent(
            nodeName,
            alignValue(e, node.style[style]),
          );
          break;
        case 'align-items':
          e.YGNodeStyleSetAlignItems(
            nodeName,
            alignValue(e, node.style[style]),
          );
          break;
        case 'align-self':
          if (!parent || node.style[style] !== parent.style['align-items']) {
            e.YGNodeStyleSetAlignSelf(
              nodeName,
              alignValue(e, node.style[style]),
            );
          }
          break;
        case 'position':
          e.YGNodeStyleSetPositionType(
            nodeName,
            positionValue(e, node.style[style]),
          );
          break;
        case 'flex-wrap':
          e.YGNodeStyleSetFlexWrap(nodeName, wrapValue(e, node.style[style]));
          break;
        case 'overflow':
          e.YGNodeStyleSetOverflow(
            nodeName,
            overflowValue(e, node.style[style]),
          );
          break;
        case 'flex-grow':
          e.YGNodeStyleSetFlexGrow(nodeName, node.style[style]);
          break;
        case 'flex-shrink':
          e.YGNodeStyleSetFlexShrink(nodeName, node.style[style]);
          break;
        case 'flex-basis':
          e.YGNodeStyleSetFlexBasis(nodeName, pointValue(e, node.style[style]));
          break;
        case 'left':
          if (genericNode.rawStyle.indexOf('start:') >= 0) {
            e.YGNodeStyleSetPosition(
              nodeName,
              e.YGEdgeStart,
              pointValue(e, node.style[style]),
            );
          } else {
            e.YGNodeStyleSetPosition(
              nodeName,
              e.YGEdgeLeft,
              pointValue(e, node.style[style]),
            );
          }
          break;
        case 'top':
          e.YGNodeStyleSetPosition(
            nodeName,
            e.YGEdgeTop,
            pointValue(e, node.style[style]),
          );
          break;
        case 'right':
          if (genericNode.rawStyle.indexOf('end:') >= 0) {
            e.YGNodeStyleSetPosition(
              nodeName,
              e.YGEdgeEnd,
              pointValue(e, node.style[style]),
            );
          } else {
            e.YGNodeStyleSetPosition(
              nodeName,
              e.YGEdgeRight,
              pointValue(e, node.style[style]),
            );
          }
          break;
        case 'bottom':
          e.YGNodeStyleSetPosition(
            nodeName,
            e.YGEdgeBottom,
            pointValue(e, node.style[style]),
          );
          break;
        case 'margin-left':
          if (genericNode.rawStyle.indexOf('margin-start:') >= 0) {
            e.YGNodeStyleSetMargin(
              nodeName,
              e.YGEdgeStart,
              pointValue(e, node.style[style]),
            );
          } else {
            e.YGNodeStyleSetMargin(
              nodeName,
              e.YGEdgeLeft,
              pointValue(e, node.style[style]),
            );
          }
          break;
        case 'margin-top':
          e.YGNodeStyleSetMargin(
            nodeName,
            e.YGEdgeTop,
            pointValue(e, node.style[style]),
          );
          break;
        case 'margin-right':
          if (genericNode.rawStyle.indexOf('margin-end:') >= 0) {
            e.YGNodeStyleSetMargin(
              nodeName,
              e.YGEdgeEnd,
              pointValue(e, node.style[style]),
            );
          } else {
            e.YGNodeStyleSetMargin(
              nodeName,
              e.YGEdgeRight,
              pointValue(e, node.style[style]),
            );
          }
          break;
        case 'margin-bottom':
          e.YGNodeStyleSetMargin(
            nodeName,
            e.YGEdgeBottom,
            pointValue(e, node.style[style]),
          );
          break;
        case 'padding-left':
          if (genericNode.rawStyle.indexOf('padding-start:') >= 0) {
            e.YGNodeStyleSetPadding(
              nodeName,
              e.YGEdgeStart,
              pointValue(e, node.style[style]),
            );
          } else {
            e.YGNodeStyleSetPadding(
              nodeName,
              e.YGEdgeLeft,
              pointValue(e, node.style[style]),
            );
          }
          break;
        case 'padding-top':
          e.YGNodeStyleSetPadding(
            nodeName,
            e.YGEdgeTop,
            pointValue(e, node.style[style]),
          );
          break;
        case 'padding-right':
          if (genericNode.rawStyle.indexOf('padding-end:') >= 0) {
            e.YGNodeStyleSetPadding(
              nodeName,
              e.YGEdgeEnd,
              pointValue(e, node.style[style]),
            );
          } else {
            e.YGNodeStyleSetPadding(
              nodeName,
              e.YGEdgeRight,
              pointValue(e, node.style[style]),
            );
          }
          break;
        case 'padding-bottom':
          e.YGNodeStyleSetPadding(
            nodeName,
            e.YGEdgeBottom,
            pointValue(e, node.style[style]),
          );
          break;
        case 'border-left-width':
          if (
            !INVISIBLE_BORDER_STYLES.includes(
              node.declaredStyle['border-left-style'],
            )
          ) {
            if (genericNode.rawStyle.indexOf('border-start-width:') >= 0) {
              e.YGNodeStyleSetBorder(
                nodeName,
                e.YGEdgeStart,
                pointValue(e, node.style[style]),
              );
            } else {
              e.YGNodeStyleSetBorder(
                nodeName,
                e.YGEdgeLeft,
                pointValue(e, node.style[style]),
              );
            }
          }
          break;
        case 'border-top-width':
          if (
            !INVISIBLE_BORDER_STYLES.includes(
              node.declaredStyle['border-top-style'],
            )
          ) {
            e.YGNodeStyleSetBorder(
              nodeName,
              e.YGEdgeTop,
              pointValue(e, node.style[style]),
            );
          }
          break;
        case 'border-right-width':
          if (
            !INVISIBLE_BORDER_STYLES.includes(
              node.declaredStyle['border-right-style'],
            )
          ) {
            if (genericNode.rawStyle.indexOf('border-end-width:') >= 0) {
              e.YGNodeStyleSetBorder(
                nodeName,
                e.YGEdgeEnd,
                pointValue(e, node.style[style]),
              );
            } else {
              e.YGNodeStyleSetBorder(
                nodeName,
                e.YGEdgeRight,
                pointValue(e, node.style[style]),
              );
            }
          }
          break;
        case 'border-bottom-width':
          if (
            !INVISIBLE_BORDER_STYLES.includes(
              node.declaredStyle['border-bottom-style'],
            )
          ) {
            e.YGNodeStyleSetBorder(
              nodeName,
              e.YGEdgeBottom,
              pointValue(e, node.style[style]),
            );
          }
          break;
        case 'width':
          e.YGNodeStyleSetWidth(nodeName, pointValue(e, node.style[style]));
          break;
        case 'min-width':
          e.YGNodeStyleSetMinWidth(nodeName, pointValue(e, node.style[style]));
          break;
        case 'max-width':
          e.YGNodeStyleSetMaxWidth(nodeName, pointValue(e, node.style[style]));
          break;
        case 'height':
          e.YGNodeStyleSetHeight(nodeName, pointValue(e, node.style[style]));
          break;
        case 'min-height':
          e.YGNodeStyleSetMinHeight(nodeName, pointValue(e, node.style[style]));
          break;
        case 'max-height':
          e.YGNodeStyleSetMaxHeight(nodeName, pointValue(e, node.style[style]));
          break;
        case 'display':
          e.YGNodeStyleSetDisplay(nodeName, displayValue(e, node.style[style]));
          break;
        case 'box-sizing':
          e.YGNodeStyleSetBoxSizing(
            nodeName,
            boxSizingValue(e, node.style[style]),
          );
      }
    }
  }

  if (parentName) {
    e.YGNodeInsertChild(parentName, nodeName, index);
  }

  if (node.innerText && node.children.length === 0) {
    e.YGNodeSetMeasureFunc(nodeName, node.innerText);
  }

  for (let i = 0; i < node.children.length; i++) {
    e.push('');
    const childName = nodeName + '_child' + i;
    setupTestTree(
      e,
      node,
      node.children[i],
      genericNode.children[i],
      childName,
      nodeName,
      i,
    );
  }
}

function overflowValue(e, value) {
  switch (value) {
    case 'visible':
      return e.YGOverflowVisible;
    case 'hidden':
      return e.YGOverflowHidden;
    case 'scroll':
      return e.YGOverflowScroll;
  }
}

function wrapValue(e, value) {
  switch (value) {
    case 'wrap':
      return e.YGWrapWrap;
    case 'wrap-reverse':
      return e.YGWrapWrapReverse;
    case 'nowrap':
      return e.YGWrapNoWrap;
  }
}

function flexDirectionValue(e, value) {
  switch (value) {
    case 'row':
      return e.YGFlexDirectionRow;
    case 'row-reverse':
      return e.YGFlexDirectionRowReverse;
    case 'column':
      return e.YGFlexDirectionColumn;
    case 'column-reverse':
      return e.YGFlexDirectionColumnReverse;
  }
}

function justifyValue(e, value) {
  switch (value) {
    case 'center':
      return e.YGJustifyCenter;
    case 'space-around':
      return e.YGJustifySpaceAround;
    case 'space-between':
      return e.YGJustifySpaceBetween;
    case 'space-evenly':
      return e.YGJustifySpaceEvenly;
    case 'flex-start':
      return e.YGJustifyFlexStart;
    case 'flex-end':
      return e.YGJustifyFlexEnd;
  }
}

function positionValue(e, value) {
  switch (value) {
    case 'absolute':
      return e.YGPositionTypeAbsolute;
    case 'static':
      return e.YGPositionTypeStatic;
    default:
      return e.YGPositionTypeRelative;
  }
}

function directionValue(e, value) {
  switch (value) {
    case 'ltr':
      return e.YGDirectionLTR;
    case 'rtl':
      return e.YGDirectionRTL;
    case 'inherit':
      return e.YGDirectionInherit;
  }
}

function alignValue(e, value) {
  switch (value) {
    case 'auto':
      return e.YGAlignAuto;
    case 'center':
      return e.YGAlignCenter;
    case 'stretch':
      return e.YGAlignStretch;
    case 'flex-start':
      return e.YGAlignFlexStart;
    case 'flex-end':
      return e.YGAlignFlexEnd;
    case 'space-between':
      return e.YGAlignSpaceBetween;
    case 'space-around':
      return e.YGAlignSpaceAround;
    case 'space-evenly':
      return e.YGAlignSpaceEvenly;
    case 'baseline':
      return e.YGAlignBaseline;
  }
}

function pointValue(e, value) {
  switch (value) {
    case 'auto':
      return e.YGAuto;
    case 'undefined':
      return e.YGUndefined;
    default:
      return value;
  }
}

function displayValue(e, value) {
  switch (value) {
    case 'flex':
      return e.YGDisplayFlex;
    case 'none':
      return e.YGDisplayNone;
    case 'contents':
      return e.YGDisplayContents;
  }
}

function boxSizingValue(e, value) {
  switch (value) {
    case 'border-box':
      return e.YGBoxSizingBorderBox;
    case 'content-box':
      return e.YGBoxSizingContentBox;
  }
}

const DEFAULT_STYLES = new Map();

function isDefaultStyleValue(style, value) {
  let defaultStyle = DEFAULT_STYLES.get(style);
  if (defaultStyle == null) {
    switch (style) {
      case 'position':
        defaultStyle = new Set(['relative']);
        break;

      case 'left':
      case 'top':
      case 'right':
      case 'bottom':
      case 'start':
      case 'end':
        defaultStyle = new Set(['undefined']);
        break;

      case 'min-height':
      case 'min-width':
        defaultStyle = new Set(['0', '0px', 'auto']);
        break;

      default: {
        const node = document.getElementById('default');
        defaultStyle = new Set([getComputedStyle(node, null)[style]]);
        break;
      }
    }
    DEFAULT_STYLES.set(style, defaultStyle);
  }
  return DEFAULT_STYLES.get(style).has(value);
}

function getRoundedSize(node) {
  const boundingRect = node.getBoundingClientRect();
  return {
    width: Math.round(boundingRect.right) - Math.round(boundingRect.left),
    height: Math.round(boundingRect.bottom) - Math.round(boundingRect.top),
  };
}

function calculateTree(root, parentOffsetLeft, parentOffsetTop) {
  const rootLayout = [];

  for (let i = 0; i < root.children.length; i++) {
    const child = root.children[i];
    const boundingRect = child.getBoundingClientRect();
    const layout = {
      name: child.id !== '' ? child.id : 'INSERT_NAME_HERE',
      left: Math.round(boundingRect.left - parentOffsetLeft),
      top: Math.round(boundingRect.top - parentOffsetTop),
      width: child.offsetWidth,
      height: child.offsetHeight,
      children: calculateTree(child, boundingRect.left, boundingRect.top),
      style: getYogaStyle(child),
      declaredStyle: child.style,
      rawStyle: child.getAttribute('style'),
      experiments: child.dataset.experiments
        ? child.dataset.experiments.split(' ')
        : [],
      disabled: child.dataset.disabled === 'true',
      innerText: child.innerText,
    };

    const size = getRoundedSize(child);
    layout.width = size.width;
    layout.height = size.height;

    rootLayout.push(layout);
  }

  return rootLayout;
}

function getYogaStyle(node) {
  // TODO: Relying on computed style means we cannot test shorthand props like
  // "padding", "margin", "gap", or negative values.
  return [
    'direction',
    'flex-direction',
    'justify-content',
    'align-content',
    'align-items',
    'align-self',
    'position',
    'flex-wrap',
    'overflow',
    'flex-grow',
    'flex-shrink',
    'flex-basis',
    'left',
    'top',
    'right',
    'bottom',
    'margin-left',
    'margin-top',
    'margin-right',
    'margin-bottom',
    'padding-left',
    'padding-top',
    'padding-right',
    'padding-bottom',
    'border-left-width',
    'border-top-width',
    'border-right-width',
    'border-bottom-width',
    'width',
    'min-width',
    'max-width',
    'height',
    'min-height',
    'max-height',
    'column-gap',
    'row-gap',
    'display',
    'aspect-ratio',
    'box-sizing',
  ].reduce((map, key) => {
    map[key] =
      node.style[key] || getComputedStyle(node, null).getPropertyValue(key);
    return map;
  }, {});
}

const Emitter = function (lang, indent) {
  this.lang = lang;
  this.indent = indent;
  this.indents = [];
  this.lines = [];
};

Emitter.prototype = Object.create(Object.prototype, {
  constructor: {value: Emitter},

  pushIndent: {
    value: function () {
      this.indents.push(this.indent);
    },
  },

  popIndent: {
    value: function () {
      this.indents.pop();
    },
  },

  push: {
    value: function (line) {
      if (line instanceof Array) {
        line.forEach(function (element) {
          this.push(element);
        }, this);
        return;
      } else if (line.length > 0) {
        line = this.indents.join('') + line;
      }
      this.lines.push(line);
    },
  },

  print: {
    value: function () {
      console.log(this.lines.join('\n'));
    },
  },
});
