import { createSignal, children, splitProps, createMemo, createComputed, untrack, batch, createEffect } from 'solid-js';
import { delay } from 'lodash-es';
import { createComponent, mergeProps } from 'solid-js/web';

function nextFrame(fn) {
  requestAnimationFrame(() => {
    requestAnimationFrame(fn);
  });
}

const Transition = props => {
  let el;
  let first = true;
  const [s1, set1] = createSignal();
  const [s2, set2] = createSignal();
  const resolved = children(() => props.children);
  const {
    onBeforeEnter,
    onEnter,
    onAfterEnter,
    onBeforeExit,
    onExit,
    onAfterExit
  } = props;
  const [ClassNames] = splitProps(props, ['enterActiveClass', 'enterClass', 'enterToClass', 'exitActiveClass', 'exitClass', 'exitToClass']);
  const classnames = createMemo(() => {
    const name = props.name || 's';
    return Object.assign({
      enterActiveClass: name + '-enter-active',
      enterClass: name + '-enter',
      enterToClass: name + '-enter-to',
      exitActiveClass: name + '-exit-active',
      exitClass: name + '-exit',
      exitToClass: name + '-exit-to'
    }, ClassNames);
  });

  function enterTransition(el, prev) {
    if (!first || props.appear) {
      const enterClasses = classnames().enterClass.split(' ');
      const enterActiveClasses = classnames().enterActiveClass.split(' ');
      const enterToClasses = classnames().enterToClass.split(' ');
      onBeforeEnter && onBeforeEnter(el);
      el.classList.add(...enterClasses);
      el.classList.add(...enterActiveClasses);
      nextFrame(() => {
        el.classList.remove(...enterClasses);
        el.classList.add(...enterToClasses);
        onEnter && onEnter(el, () => endTransition());

        if (!onEnter || onEnter.length < 2) {
          el.addEventListener('transitionend', endTransition);
          el.addEventListener('animationend', endTransition);
        }
      });

      function endTransition(e) {
        if (el && (!e || e.target === el)) {
          el.removeEventListener('transitionend', endTransition);
          el.removeEventListener('animationend', endTransition);
          el.classList.remove(...enterActiveClasses);
          el.classList.remove(...enterToClasses);
          batch(() => {
            s1() !== el && set1(el);
            s2() === el && set2(undefined);
          });
          onAfterEnter && onAfterEnter(el);
          if (props.mode === 'inout') exitTransition(el, prev);
        }
      }
    }

    prev && !props.mode ? set2(el) : set1(el);
  }

  function exitTransition(el, prev) {
    const exitClasses = classnames().exitClass.split(' ');
    const exitActiveClasses = classnames().exitActiveClass.split(' ');
    const exitToClasses = classnames().exitToClass.split(' ');
    if (!prev.parentNode) return endTransition();
    onBeforeExit && onBeforeExit(prev);
    prev.classList.add(...exitClasses);
    prev.classList.add(...exitActiveClasses);
    nextFrame(() => {
      prev.classList.remove(...exitClasses);
      prev.classList.add(...exitToClasses);
    });
    onExit && onExit(prev, () => endTransition());

    if (!onExit || onExit.length < 2) {
      prev.addEventListener('transitionend', endTransition);
      prev.addEventListener('animationend', endTransition);
    }

    function endTransition(e) {
      if (!e || e.target === prev) {
        prev.removeEventListener('transitionend', endTransition);
        prev.removeEventListener('animationend', endTransition);
        prev.classList.remove(...exitActiveClasses);
        prev.classList.remove(...exitToClasses);
        s1() === prev && set1(undefined);
        onAfterExit && onAfterExit(prev);
        if (props.mode === 'outin') enterTransition(el, prev);
      }
    }
  }

  createComputed(prev => {
    const children = resolved();
    el = children instanceof Array ? children[0] : children;

    while (typeof el === 'function') el = el();

    return untrack(() => {
      if (el && el !== prev) {
        if (props.mode !== 'outin') enterTransition(el, prev);else if (first) set1(el);
      }

      if (prev && prev !== el && props.mode !== 'inout') exitTransition(el, prev);
      first = false;
      return el;
    });
  });
  return [s1, s2];
};

function getRect(element) {
  const {
    top,
    bottom,
    left,
    right,
    width,
    height
  } = element.getBoundingClientRect();
  const parentRect = element.parentNode.getBoundingClientRect();
  return {
    top: top - parentRect.top,
    bottom,
    left: left - parentRect.left,
    right,
    width,
    height
  };
}

const TransitionGroup = props => {
  const resolved = children(() => props.children); // fixed: 修补没有用到的类

  const [ClassNames] = splitProps(props, ['enterActiveClass', 'enterClass', 'enterToClass', 'exitActiveClass', 'exitClass', 'exitToClass', 'moveClass']);
  const classnames = createMemo(() => {
    const name = props.name || 's';
    return Object.assign({
      enterActiveClass: name + '-enter-active',
      enterClass: name + '-enter',
      enterToClass: name + '-enter-to',
      exitActiveClass: name + '-exit-active',
      exitClass: name + '-exit',
      exitToClass: name + '-exit-to',
      moveClass: name + '-move'
    }, ClassNames);
  });
  const {
    onBeforeEnter,
    onEnter,
    onAfterEnter,
    onBeforeExit,
    onExit,
    onAfterExit
  } = props;
  const [combined, setCombined] = createSignal();
  let p = [];
  let first = true;
  createComputed(() => {
    const c = resolved();
    const comb = [...c];
    const next = new Set(c);
    const prev = new Set(p);
    const enterClasses = classnames().enterClass.split(' ');
    const enterActiveClasses = classnames().enterActiveClass.split(' ');
    const enterToClasses = classnames().enterToClass.split(' ');
    const exitClasses = classnames().exitClass.split(' ');
    const exitActiveClasses = classnames().exitActiveClass.split(' ');
    const exitToClasses = classnames().exitToClass.split(' ');

    for (let i = 0; i < c.length; i++) {
      const el = c[i]; // fixed: 添加 appear 第一次动画渲染

      if ((!first || props.appear) && !prev.has(el) && el) {
        // 优先进行初始化类名的注入
        el.classList.add(...enterClasses);
        delay(() => {
          onBeforeEnter && onBeforeEnter(el);
          el.classList.add(...enterActiveClasses);
          nextFrame(() => {
            el.classList.remove(...enterClasses);
            el.classList.add(...enterToClasses);
            onEnter && onEnter(el, () => endTransition());

            if (!onEnter || onEnter.length < 2) {
              el.addEventListener('transitionend', endTransition);
              el.addEventListener('animationend', endTransition);
            }
          });

          function endTransition(e) {
            if (el && (!e || e.target === el)) {
              el.removeEventListener('transitionend', endTransition);
              el.removeEventListener('animationend', endTransition);
              el.classList.remove(...enterActiveClasses);
              el.classList.remove(...enterToClasses);
              onAfterEnter && onAfterEnter(el);
            }
          }
        }, i * props.stagger);
      }
    }

    for (let i = 0; i < p.length; i++) {
      const old = p[i];

      if (old && !next.has(old) && old.parentNode) {
        comb.splice(i, 0, old);
        old.classList.add(...exitClasses);
        delay(() => {
          onBeforeExit && onBeforeExit(old);
          old.classList.add(...exitActiveClasses);
          nextFrame(() => {
            old.classList.remove(...exitClasses);
            old.classList.add(...exitToClasses);
          });
          onExit && onExit(old, () => endTransition());

          if (!onExit || onExit.length < 2) {
            old.addEventListener('transitionend', endTransition);
            old.addEventListener('animationend', endTransition);
          }

          function endTransition(e) {
            if (!e || e.target === old) {
              old.removeEventListener('transitionend', endTransition);
              old.removeEventListener('animationend', endTransition);
              old.classList.remove(...exitActiveClasses);
              old.classList.remove(...exitToClasses);
              onAfterExit && onAfterExit(old);
              p = p.filter(i => i !== old);
              setCombined(p);
            }
          }
        }, i * props.stagger);
      }
    }

    p = comb;
    setCombined(comb);
  });
  createEffect(nodes => {
    const c = combined();
    c.forEach(child => {
      let n = nodes.get(child);

      if (!n) {
        try {
          // 这里查找的时候会发生 BUG
          n = {
            pos: getRect(child),
            new: !first
          };
        } catch (e) {
          return;
        }

        nodes.set(child, n);
      } else if (n.new) {
        n.new = false;
        n.newPos = getRect(child);
      }

      if (n.new) {
        child.addEventListener('transitionend', () => {
          n.new = false;
          child.parentNode && (n.newPos = getRect(child));
        }, {
          once: true
        });
      }

      n.newPos && (n.pos = n.newPos);
      n.newPos = getRect(child);
    });

    if (first) {
      first = false;
      return nodes;
    }

    c.forEach(child => {
      const c = nodes.get(child);
      if (!c) return;
      const oldPos = c.pos;
      const newPos = c.newPos;
      const dx = oldPos.left - newPos.left;
      const dy = oldPos.top - newPos.top;

      if (dx || dy) {
        c.moved = true;
        const s = child.style;
        s.transform = `translate(${dx}px,${dy}px)`;
        s.transitionDuration = '0s';
      }
    });
    document.body.offsetHeight;
    c.forEach(child => {
      const c = nodes.get(child);
      if (!c) return;

      if (c.moved) {
        c.moved = false;
        const s = child.style;
        const moveClasses = classnames().moveClass.split(' ');
        child.classList.add(...moveClasses);
        s.transform = s.transitionDuration = '';

        function endTransition(e) {
          if (e && e.target !== child || !child.parentNode) return;

          if (!e || /transform$/.test(e.propertyName)) {
            child.removeEventListener('transitionend', endTransition);
            child.classList.remove(...moveClasses);
          }
        }

        child.addEventListener('transitionend', endTransition);
      }
    });
    return nodes;
  }, new Map());
  return combined;
};

/**
 * @zh 基于 animate.css 的一个插件, css 文件需要单独导入
 */
const Anime = props => {
  const Comp = props.group ? TransitionGroup : Transition;
  return createComponent(Comp, mergeProps(props, {
    get enterActiveClass() {
      return 'animated ' + props.in;
    },

    get exitActiveClass() {
      return 'animated ' + props.out;
    }

  }));
};

export { Anime, Transition, TransitionGroup };
