import {
  assertDoesNotContainDisplay,
  computedStyle,
  setStyles,
} from '#core/dom/style';

import {Services} from '#service';

import {dev, devAssert, userAssert} from '#utils/log';

import {FxType} from '../fx-type';

/**
 * These fully qualified names, my goodness.
 * @typedef {!../../../../src/service/position-observer/position-observer-worker.PositionInViewportEntryDef}
 */
let PositionObserverEntryDef;

/** @typedef {function(this:./fx-provider.FxElement, ?PositionObserverEntryDef)} */
let FxUpdateDef;

/** @typedef {{userAsserts: function(!Element):*, update: !FxUpdateDef}} */
let FxPresetDef;

/**
 * @param {!./fx-provider.FxElement} fxElement
 * @param {string} axis 'X' or 'Y' (uppercase)
 * @param {number} coeff 1 or -1
 */
function flyIn(fxElement, axis, coeff) {
  devAssert(axis == 'X' || axis == 'Y');
  devAssert(Math.abs(coeff) == 1);

  const element = dev().assertElement(fxElement.element);

  const axisIsX = axis == 'X';

  // Not using interpolation in the following assignment since closure compiles
  // to a leading, useless empty string.
  const flyInDistanceAsLength =
    coeff * fxElement.flyInDistance + (axisIsX ? 'vw' : 'vh');

  // only do this on the first element
  if (!fxElement.initialTrigger) {
    Services.mutatorForDoc(element).mutateElement(element, () => {
      const style = computedStyle(fxElement.win, element);
      const prop = axisIsX ? 'left' : 'top';
      const propAsLength = style[prop] === 'auto' ? '0px' : style[prop];
      const position =
        style.position === 'static' ? 'relative' : style.position;
      const styles = {
        position,
        visibility: 'visible',
      };
      styles[prop] = `calc(${propAsLength} - ${flyInDistanceAsLength})`;
      setStyles(element, assertDoesNotContainDisplay(styles));
    });
    fxElement.initialTrigger = true;
  }

  // If above the threshold of trigger-position, translate the element by
  // distance as [vw|vh].
  setStyles(element, {
    'transition-duration': fxElement.duration,
    'transition-timing-function': fxElement.easing,
    'transform': `translate${axis}(${flyInDistanceAsLength})`,
  });
}

/**
 * @param {!Element} element
 * @return {number} [data-margin-start] value
 */
function marginStartAsserts(element) {
  const marginStart = parseFloat(element.getAttribute('data-margin-start'));
  if (marginStart) {
    userAssert(
      marginStart >= 0 && marginStart <= 100,
      'data-margin-start must be a percentage value ' +
        'and be between 0% and 100% for: %s',
      element
    );
  }
  return marginStart;
}

/**
 * @param {?PositionObserverEntryDef} entry
 * @return {?number}
 */
function topFromPosObsEntryOrNull(entry) {
  return entry && entry.positionRect ? entry.positionRect.top : null;
}

/**
 * @param {?PositionObserverEntryDef} entry
 * @param {!./fx-provider.FxElement} fxElement
 * @param {number} coeff
 * @return {boolean}
 */
function isInViewportForTopAxis(entry, fxElement, coeff) {
  const top = topFromPosObsEntryOrNull(entry);
  devAssert(Math.abs(coeff) == 1);
  return (
    !!top &&
    top + (coeff * fxElement.viewportHeight * fxElement.flyInDistance) / 100 <=
      (1 - fxElement.marginStart) * fxElement.viewportHeight
  );
}

/**
 * @param {?PositionObserverEntryDef} entry
 * @param {!./fx-provider.FxElement} fxElement
 * @param {number=} opt_vh optional adjusted viewport height
 * @return {boolean}
 */
function isInViewportConsideringMargins(entry, fxElement, opt_vh) {
  const top = topFromPosObsEntryOrNull(entry);
  const vh = opt_vh !== undefined ? opt_vh : fxElement.viewportHeight;
  return !!top && top <= (1 - fxElement.marginStart) * vh;
}

/** @const {!{[key: !FxType]: !FxPresetDef}} */
export const Presets = {
  [FxType.PARALLAX]: {
    userAsserts(element) {
      const factorValue = userAssert(
        element.getAttribute('data-parallax-factor'),
        'data-parallax-factor=<number> attribute must be provided for: %s',
        element
      );
      userAssert(
        parseFloat(factorValue) > 0,
        'data-parallax-factor must be a number and greater than 0 for: %s',
        element
      );
    },
    update(entry) {
      const fxElement = this;
      const top = topFromPosObsEntryOrNull(entry);
      devAssert(fxElement.adjustedViewportHeight);
      if (!top || top > fxElement.adjustedViewportHeight) {
        return;
      }

      // User provided factor is 1-based for easier understanding.
      // Also negating number since we are using tranformY so negative = upward,
      // positive = downward.
      const adjustedFactor = -(parseFloat(fxElement.factor) - 1);
      // Offset is how much extra to move the element which is position within
      // viewport times adjusted factor.
      const offset = (fxElement.adjustedViewportHeight - top) * adjustedFactor;
      fxElement.offset = offset;

      // If above the threshold of trigger-position
      // Translate the element offset pixels.
      setStyles(fxElement.element, {
        transform: `translateY(${fxElement.offset.toFixed(0)}px)`,
      });
    },
  },
  [FxType.FLY_IN_BOTTOM]: {
    userAsserts: marginStartAsserts,
    update(entry) {
      const fxElement = this;
      devAssert(fxElement.viewportHeight);
      if (!isInViewportForTopAxis(entry, fxElement, /* coeff */ -1)) {
        return;
      }
      flyIn(fxElement, 'Y', /* coeff */ -1);
    },
  },
  [FxType.FLY_IN_LEFT]: {
    userAsserts: marginStartAsserts,
    update(entry) {
      const fxElement = this;
      devAssert(fxElement.viewportHeight);
      if (!isInViewportConsideringMargins(entry, fxElement)) {
        return;
      }
      flyIn(fxElement, 'X', /* coeff */ 1);
    },
  },
  [FxType.FLY_IN_RIGHT]: {
    userAsserts: marginStartAsserts,
    update(entry) {
      const fxElement = this;
      devAssert(fxElement.viewportHeight);
      if (!isInViewportConsideringMargins(entry, fxElement)) {
        return;
      }
      flyIn(fxElement, 'X', /* coeff */ -1);
    },
  },
  [FxType.FLY_IN_TOP]: {
    userAsserts: marginStartAsserts,
    update(entry) {
      const fxElement = this;
      devAssert(fxElement.viewportHeight);
      if (!isInViewportForTopAxis(entry, fxElement, /* coeff */ 1)) {
        return;
      }
      flyIn(fxElement, 'Y', /* coeff */ 1);
    },
  },
  [FxType.FADE_IN]: {
    userAsserts: marginStartAsserts,
    update(entry) {
      const fxElement = this;
      devAssert(fxElement.viewportHeight);
      if (!isInViewportConsideringMargins(entry, fxElement)) {
        return;
      }

      // If above the threshold of trigger-position
      // Translate the element offset pixels.
      setStyles(fxElement.element, {
        'transition-duration': fxElement.duration,
        'transition-timing-function': fxElement.easing,
        'opacity': 1,
      });
    },
  },
  [FxType.FADE_IN_SCROLL]: {
    userAsserts(element) {
      const marginStart = marginStartAsserts(element);
      const marginEnd = parseFloat(element.getAttribute('data-margin-end'));

      if (!marginEnd) {
        return;
      }

      userAssert(
        marginEnd >= 0 && marginEnd <= 100,
        'data-margin-end must be a percentage value ' +
          'and be between 0% and 100% for: %s',
        element
      );

      userAssert(
        marginEnd > marginStart,
        'data-margin-end must be greater than data-margin-start for: %s',
        element
      );
    },
    update(entry) {
      const fxElement = this;
      const {marginStart, viewportHeight} = fxElement;
      devAssert(fxElement.adjustedViewportHeight);

      if (
        !isInViewportConsideringMargins(
          entry,
          fxElement,
          fxElement.adjustedViewportHeight
        )
      ) {
        return;
      }

      // Early exit if the animation doesn't need to repeat and it is fully
      // opaque.
      if (!fxElement.hasRepeat && fxElement.offset >= 1) {
        return;
      }
      const top = topFromPosObsEntryOrNull(entry);
      // Translate the element offset pixels.
      const marginDelta = fxElement.marginEnd - marginStart;
      // Offset is how much extra to move the element which is position within
      // viewport times adjusted factor.
      const offset =
        (1 * (viewportHeight - top - marginStart * viewportHeight)) /
        (marginDelta * viewportHeight);
      fxElement.offset = offset;

      // If above the threshold of trigger-position
      // Translate the element offset pixels.
      setStyles(fxElement.element, {opacity: fxElement.offset});
    },
  },
};
