/* global gsap, ScrollTrigger, imagesLoaded */
(function () {
  const {
    mergeObjects,
    onResize,
    matchMedia,
    prefersReducedMotion,
    isBuilder
  } = BreakdanceFrontend.utils;

  class BreakdanceEntrance {
    enabledClass = 'breakdance-animation-enabled';
    beforeClass = 'is-before';
    animatingClass = 'is-animating';
    completedClass = 'is-animated';

    defaultOptions = {
      animation_type: null,
      duration: { number: 500, unit: 'ms', style: '500ms' },
      delay: { number: 0, unit: 'ms', style: '0ms' },
      advanced: {
        distance: { number: 100, unit: "px", style: "100px" },
        offset: { number: 0, unit: 'px', style: '0px' },
        ease: 'power1.out',
        anchorPlacement: 'top bottom',
        once: false,
        disable_at: null
      }
    };

    delay = 0;
    initialized = false;

    constructor(selector, options) {
      gsap.registerPlugin(ScrollTrigger);

      this.cleanup = this.cleanup.bind(this);
      this.reset = this.reset.bind(this);

      this.selector = selector;
      this.options = mergeObjects(this.defaultOptions, options);
      this.rootEl = document.documentElement;

      this.init();
    }

    getAnimations() {
      const distance = this.options.advanced.distance.style;
      const unit = this.options.advanced.distance.unit;

      return {
        fade: [
          { autoAlpha: 0 },
          { autoAlpha: 1 }
        ],

        slideUp: [
          { y: distance },
          { y: `0${unit}` }
        ],
        slideDown: [
          { y: `-=${distance}` },
          { y: `0${unit}` }
        ],
        slideLeft: [
          { x: `-=${distance}` },
          { x: `0${unit}` }
        ],
        slideRight: [
          { x: distance },
          { x: `0${unit}` }
        ],

        flipUp: [
          { perspective: 2500, rotateX: `-=${distance}` },
          { rotateX: `0${unit}` }
        ],

        flipDown: [
          { perspective: 2500, rotateX: distance },
          { rotateX: `0${unit}` }
        ],
        flipLeft: [
          { perspective: 2500, rotateY: `-=${distance}` },
          { rotateY: `0${unit}` }
        ],
        flipRight: [
          { perspective: 2500, rotateY: distance },
          { rotateY: `0${unit}` }
        ],

        zoomIn: [
          { scale: 0.6 },
          { scale: 1 }
        ],
        zoomOut: [
          { scale: 1.2 },
          { scale: 1 }
        ],
      };
    }

    canAnimate() {
      const breakpoint = this.options.advanced.disable_at;
      if (!breakpoint) return true;
      return !matchMedia(breakpoint);
    }

    getDuration(value) {
      if (!value) return value;
      if (value.unit === 's') return value.number;
      return value.number / 1000; // Convert MS to S
    }

    cleanup() {
      // Clear all inline styles, otherwise they will throw off animations for images.
      gsap.set(this.element, { clearProps: 'all' });
    }

    reset(element) {
      const el = element || this.element;
      el.classList.add(this.beforeClass);
      el.classList.remove(this.completedClass);
      el.classList.remove(this.animatingClass);
    }

    getOffset(offset, anchor) {
      const defaultOffset = 120;

      // Top-bottom placement looks terrible if the element is animated with offset zero.
      if (anchor === 'top bottom' && offset.number === 0) {
        return defaultOffset;
      }

      return offset.number;
    }

    createTween() {
      const type = this.options.animation_type;
      const animations = this.getAnimations();

      if (!animations[type]) {
        console.error(`[ENTRANCE] The selected ${type} animation is invalid.`);
        return;
      }

      const [from, to] = animations[type];

      const { ease, once, anchorPlacement } = this.options.advanced;
      const duration = this.getDuration(this.options.duration);
      const delay = this.getDuration(this.options.delay) + this.delay;
      const offset = this.getOffset(this.options.advanced.offset, anchorPlacement);
      const [elementStart, viewportStart] = anchorPlacement.split(' ');

      const anim = gsap.timeline({
        delay,
        paused: true
      });

      this.element.classList.add(this.beforeClass);

      if (isBuilder()) {
        this.hideEl();
      }

      this.startTrigger = ScrollTrigger.create({
        trigger: this.element,
        start: `${elementStart}+=${offset} ${viewportStart}`,
        toggleActions: "play none none none",
        once,
        onEnter: () => anim.play()
      });

      anim.fromTo(this.element, {
        ...from,
        autoAlpha: 0,
      },
      {
        ...to,
        duration,
        delay,
        ease,
        autoAlpha: 1,
        clearProps: 'all',
        immediateRender: false,
        onStart: () => {
          this.element.classList.add(this.animatingClass);
          this.element.classList.remove(this.beforeClass);
        },
        onComplete: () => {
          this.element.classList.remove(this.animatingClass);
          this.element.classList.add(this.completedClass);
        },
        onReverseComplete: () => {
          this.element.classList.add(this.beforeClass);
          this.element.classList.remove(this.animatingClass);
        },
      });

      if (!once) {
        this.goToBeginningOnReverse(anim);
      }

      return anim;
    }

    goToBeginningOnReverse(animation) {
      // Reset animation to the beginning once its goes offscreen at the bottom.
      this.endTrigger = ScrollTrigger.create({
        trigger: this.element,
        start: `top-=10 bottom`,
        onLeaveBack: () => {
          // Reset the animation to the beginning.
          animation.pause(0);

          // Clear all inline styles.
          this.cleanup();

          // Trick the builder into making the element selectable.
          // This is needed because activating the element sets its visibility to hidden.
          if (isBuilder()) {
            this.hideEl();
          }

          this.reset();
        }
      });
    }

    hideEl() {
      gsap.set(this.element, { autoAlpha: 0 });
    }

    update(options) {
      this.options = mergeObjects(this.defaultOptions, options);
      this.destroy();
      this.init();
    }

    replay(delay = 0) {
      this.destroy();
      this.reset();
      this.delay = delay;
      this.init();
    }

    destroy() {
      this.initialized = false;

      if (!this.element) return;

      this.element.classList.add(this.completedClass);

      if (!this.tween) return;

      this.tween.kill();
      this.tween = null;
      this.startTrigger?.kill();
      this.endTrigger?.kill();

      // Remove all inline styles
      gsap.set(this.element, { clearProps: 'all' });
    }

    refresh() {
      ScrollTrigger.refresh();
    }

    initTween() {
      if (this.initialized) return;
      this.initialized = true;

      this.element.classList.remove(this.completedClass);
      this.tween = this.createTween();
    }

    initOrDestroy() {
      if (this.canAnimate()) {
        this.initTween();
      } else {
        this.destroy();
      }
    }

    init() {
      if (!this.options.animation_type) return;

      this.element = document.querySelector(this.selector);
      this.element.bdAnim = this;

      if (prefersReducedMotion()) {
        this.element.classList.add(this.completedClass);
        return;
      }

      onResize(() => this.initOrDestroy());
      this.rootEl.classList.add(this.enabledClass);
    }

    static autoload() {
      const loaded = imagesLoaded(document.body);

      // Listen for images to be loaded.
      loaded.on("always", () => {
        // Refresh all instances.
        const event = new Event("breakdance_refresh_animations", { bubbles: true });
        document.dispatchEvent(event);

        // Refresh ScrollTrigger only once.
        ScrollTrigger.refresh(true);
      });

      // Play event
      addEventListener("breakdance_play_animations", (event) => {
        const target = event.target === window ? document.body : event.target;

        const nodes = [
          target,
          ...target.querySelectorAll("[data-entrance]")
        ];

        nodes.forEach((el) => el.bdAnim?.replay(event?.detail?.delay));
      });

      // Cleanup event
      addEventListener("breakdance_refresh_animations", (event) => {
        const target = event.target === window ? document.body : event.target;

        const nodes = [
          target,
          ...target.querySelectorAll("[data-entrance]")
        ];

        nodes.forEach((el) => el.bdAnim?.cleanup());
      });

      addEventListener("breakdance_reset_animations", (event) => {
        const target = event.target === window ? document.body : event.target;

        const nodes = [
          target,
          ...target.querySelectorAll("[data-entrance]")
        ];

        nodes.forEach((el) => el.bdAnim?.reset(el));
      });
    }

    static dontLetScrollTriggerMutateScrollPosition() {
      window.addEventListener("load", () => {
        if (!location.hash) return;
        if (window.bdeAnimationScrolled) return;
        const scrollElem = document.querySelector(location.hash);
        if (!scrollElem) return;

        // Safari fix: Scroll to the element after the next repaint.
        requestAnimationFrame(() => {
          scrollElem.scrollIntoView({
            behavior: "smooth",
          });
        });

        // Prevent scrolling to the same element twice.
        window.bdeAnimationScrolled = true;
      });
    }
  }

  window.BreakdanceEntrance = BreakdanceEntrance;

  BreakdanceEntrance.autoload();
  BreakdanceEntrance.dontLetScrollTriggerMutateScrollPosition();
}());
