const hasResizeObserver = !!window.ResizeObserver;

const mutationObserverFullOptions = {
  childList: true,
  attributes: true,
  subtree: true,
  characterData: true
};

const mutationObserverOptionsForStyleChanges = {
  attributes: true
};

export function shallowCompare(obj1, obj2) {
  if (obj1 === obj2) {
    return true;
  } else if (!(obj1 instanceof Object) || !(obj2 instanceof Object)) {
    return false;
  }
  let keys1 = Object.keys(obj1).sort();
  let keys2 = Object.keys(obj2).sort();
  if (keys1.length !== keys2.length) {
    return false;
  }
  return keys1.every(
    (key, i) => key === keys2[i] && obj1[key] === obj2[keys2[i]]
  );
}

export function getPropertyValueWithoutUnit(styleObj, property) {
  return parseFloat(styleObj[property], 10) || 0;
}

export function getSizeAndMargin(element, hasMargin) {
  let { clientHeight, clientWidth } = element;
  const { height, width } = element.getBoundingClientRect();
  let size = {
    height: height,
    width: width,
    clientHeight,
    clientWidth,
    marginTop: 0,
    marginBottom: 0,
    marginLeft: 0,
    marginRight: 0
  };
  if (hasMargin) {
    // this line referred from https://stackoverflow.com/questions/14275304/how-to-get-margin-value-of-a-div-in-plain-javascript
    let computedStyle = window.getComputedStyle(element);
    size.marginTop = getPropertyValueWithoutUnit(computedStyle, "marginTop");
    size.marginBottom = getPropertyValueWithoutUnit(
      computedStyle,
      "marginBottom"
    );
    size.marginLeft = getPropertyValueWithoutUnit(computedStyle, "marginLeft");
    size.marginRight = getPropertyValueWithoutUnit(
      computedStyle,
      "marginRight"
    );
  }
  return size;
}

export default class ResizeObserverPolyfill {
  constructor(onResize, hasMargin) {
    // method binds
    this.transitionEndHandler = this.transitionEndHandler.bind(this);
    this.resizeHandler = this._resizeHandler.bind(this);
    // this.resizeHandler = (...args) => {
    //   requestAnimationFrame(() => {
    //     this._resizeHandler(...args);
    //   });
    // };

    this.onResize = onResize;
    this.hasMargin = hasMargin;
    this.targetNode = null;
    this.size = null;
    this.mutationObserverInstance = new window.MutationObserver(
      this.resizeHandler
    );
    if (hasResizeObserver) {
      this.resizeObserverInstance = new window.ResizeObserver(
        this.resizeHandler
      );
    }
  }

  transitionEndHandler(event) {
    // TODO: we need to update this condition mouch better
    //  like ['height', 'width'].indexOf(propertyName) >= 0 the specific property
    const { propertyName } = event;
    const includes = (str) => propertyName.indexOf(str) >= 0;
    if (includes("color")) {
      return;
    }
    // NOTE: border-width and min-width, max-width are includes width
    // NOTE: line-height and min-height, max-height are includes height
    if (["margin", "padding", "width", "height", "flex-basic"].some(includes)) {
      this.resizeHandler();
      return;
    }
  }

  _resizeHandler() {
    if (!this.targetNode) {
      return;
    }
    const currentSize = getSizeAndMargin(this.targetNode, this.hasMargin);
    if (this.size && shallowCompare(currentSize, this.size)) {
      return;
    }
    this.size = currentSize;
    this.onResize(this.size);
  }

  observe(targetNode, hasMargin) {
    this.targetNode = targetNode;
    if (!targetNode) {
      return;
    }
    targetNode.addEventListener("transitionend", this.transitionEndHandler);
    this.hasMargin = hasMargin;
    if (hasResizeObserver) {
      this.resizeObserverInstance.observe(targetNode);
      if (hasMargin) {
        this.mutationObserverInstance.observe(
          targetNode,
          mutationObserverOptionsForStyleChanges
        );
      }
    } else {
      this.mutationObserverInstance.observe(
        targetNode,
        mutationObserverFullOptions
      );
    }
    // NOTE: just for initialize
    // cancelAnimationFrame(this.interval);
    // this.interval = requestAnimationFrame(() => {
    clearTimeout(this.interval);
    this.interval = setTimeout(() => {
      this.resizeHandler();
    });
  }

  replaceObservationElement(targetNode) {
    if (this.targetNode === targetNode) {
      return;
    }
    this.disconnect();
    this.observe(targetNode, this.hasMargin);
  }

  disconnect() {
    this.size = null;
    this.targetNode &&
      this.targetNode.removeEventListener(
        "transitionend",
        this.transitionEndHandler
      );
    hasResizeObserver && this.resizeObserverInstance.disconnect();
    this.mutationObserverInstance.disconnect();
  }
}

//TODO:   HACK:
//    for window.ResizeObserver unsupported browsers we want to calculate dimensions because flex affect element size with siblings
