// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
{
  const { Downloads } = ChromeUtils.importESModule('resource://gre/modules/Downloads.sys.mjs');

  const CONFIG = Object.freeze({
    ANIMATION: {
      ARC_STEPS: 60,
      MAX_ARC_HEIGHT: 1200,
      ARC_HEIGHT_RATIO: 0.8, // Arc height = distance * ratio (capped at MAX_ARC_HEIGHT)
      SCALE_END: 0.45, // Final scale at destination
    },
  });

  class nsZenDownloadAnimation extends nsZenDOMOperatedFeature {
    async init() {
      await this.#setupDownloadListeners();
    }

    async #setupDownloadListeners() {
      try {
        const list = await Downloads.getList(Downloads.ALL);
        list.addView({
          onDownloadAdded: this.#handleNewDownload.bind(this),
        });
      } catch (error) {
        console.error(
          `[${nsZenDownloadAnimation.name}] Failed to set up download animation listeners: ${error}`
        );
      }
    }

    #handleNewDownload() {
      if (
        !Services.prefs.getBoolPref('zen.downloads.download-animation') ||
        !nsZenMultiWindowFeature.isActiveWindow
      ) {
        return;
      }

      if (!gZenUIManager._lastClickPosition) {
        console.warn(
          `[${nsZenDownloadAnimation.name}] No recent click position available for animation`
        );
        return;
      }

      this.#animateDownload(gZenUIManager._lastClickPosition);
    }

    #animateDownload(startPosition) {
      let animationElement = document.querySelector('zen-download-animation');

      if (!animationElement) {
        animationElement = document.createElement('zen-download-animation');
        document.body.appendChild(animationElement);
      }

      animationElement.initializeAnimation(startPosition);
    }
  }

  class nsZenDownloadAnimationElement extends HTMLElement {
    #boxAnimationElement = null;
    #boxAnimationTimeoutId = null;
    #isBoxAnimationRunning = false;

    constructor() {
      super();
      this.attachShadow({ mode: 'open' });
      this.#loadArcStyles();
    }

    #loadArcStyles() {
      try {
        const link = document.createElement('link');
        link.setAttribute('rel', 'stylesheet');
        link.setAttribute(
          'href',
          'chrome://browser/content/zen-styles/zen-download-arc-animation.css'
        );
        this.shadowRoot.appendChild(link);
      } catch (error) {
        console.error(`[${nsZenDownloadAnimationElement.name}] Error loading arc styles: ${error}`);
      }
    }

    async initializeAnimation(startPosition) {
      if (!startPosition) {
        console.warn(
          `[${nsZenDownloadAnimationElement.name}] No start position provided, skipping animation`
        );
        return;
      }

      // Determine animation target position
      const { endPosition, isDownloadButtonVisible } = this.#determineEndPosition();
      const areTabsPositionedRight = this.#areTabsOnRightSide();

      // Create and prepare the arc animation element
      const arcAnimationElement = this.#createArcAnimationElement(startPosition);

      // Calculate optimal arc parameters based on available space
      const distance = this.#calculateDistance(startPosition, endPosition);
      const { arcHeight, shouldArcDownward } = this.#calculateOptimalArc(
        startPosition,
        endPosition,
        distance
      );
      const distanceX = endPosition.clientX - startPosition.clientX;
      const distanceY = endPosition.clientY - startPosition.clientY;
      const arcSequence = this.#createArcAnimationSequence(
        distanceX,
        distanceY,
        arcHeight,
        shouldArcDownward
      );

      // Start the download animation
      await this.#startDownloadAnimation(
        areTabsPositionedRight,
        isDownloadButtonVisible,
        arcAnimationElement,
        arcSequence
      );
    }

    #areTabsOnRightSide() {
      return Services.prefs.getBoolPref('zen.tabs.vertical.right-side');
    }

    #determineEndPosition() {
      const downloadsButton = document.getElementById('downloads-button');
      const isDownloadButtonVisible = downloadsButton && this.#isElementVisible(downloadsButton);

      let endPosition = { clientX: 0, clientY: 0 };

      if (isDownloadButtonVisible) {
        // Use download button as target
        const buttonRect = downloadsButton.getBoundingClientRect();
        endPosition = {
          clientX: buttonRect.left + buttonRect.width / 2,
          clientY: buttonRect.top + buttonRect.height / 2,
        };
      } else {
        // Use alternative position at bottom of wrapper
        const areTabsPositionedRight = this.#areTabsOnRightSide();
        const wrapper = document.getElementById('zen-main-app-wrapper');
        const wrapperRect = wrapper.getBoundingClientRect();

        endPosition = {
          clientX: areTabsPositionedRight ? wrapperRect.right - 42 : wrapperRect.left + 42,
          clientY: wrapperRect.bottom - 40,
        };
      }

      return { endPosition, isDownloadButtonVisible };
    }

    #createArcAnimationElement(startPosition) {
      const arcAnimationHTML = `
            <box class="zen-download-arc-animation">
              <box class="zen-download-arc-animation-inner-circle">
                <html:div class="zen-download-arc-animation-icon"></html:div>
              </box>
            </box>
          `;

      const fragment = window.MozXULElement.parseXULToFragment(arcAnimationHTML);
      const animationElement = fragment.querySelector('.zen-download-arc-animation');

      Object.assign(animationElement.style, {
        left: `${startPosition.clientX}px`,
        top: `${startPosition.clientY}px`,
        transform: 'translate(-50%, -50%)',
      });

      this.shadowRoot.appendChild(animationElement);

      return animationElement;
    }

    #calculateOptimalArc(startPosition, endPosition, distance) {
      // Calculate available space for the arc
      const availableTopSpace = Math.min(startPosition.clientY, endPosition.clientY);
      const viewportHeight = window.innerHeight;
      const availableBottomSpace =
        viewportHeight - Math.max(startPosition.clientY, endPosition.clientY);

      // Determine if we should arc downward or upward based on available space
      const shouldArcDownward = availableBottomSpace > availableTopSpace;

      // Use the space in the direction we're arcing
      const availableSpace = shouldArcDownward ? availableBottomSpace : availableTopSpace;

      // Limit arc height to a percentage of the available space
      const arcHeight = Math.min(
        distance * CONFIG.ANIMATION.ARC_HEIGHT_RATIO,
        CONFIG.ANIMATION.MAX_ARC_HEIGHT,
        availableSpace * 0.8
      );

      return { arcHeight, shouldArcDownward };
    }

    #calculateDistance(start, end) {
      const distanceX = end.clientX - start.clientX;
      const distanceY = end.clientY - start.clientY;
      return Math.sqrt(distanceX * distanceX + distanceY * distanceY);
    }

    async #startDownloadAnimation(
      areTabsPositionedRight,
      isDownloadButtonVisible,
      arcAnimationElement,
      sequence
    ) {
      try {
        if (!isDownloadButtonVisible) {
          this.#startBoxAnimation(areTabsPositionedRight);
        }

        await gZenUIManager.motion.animate(arcAnimationElement, sequence, {
          duration: Services.prefs.getIntPref('zen.downloads.download-animation-duration') / 1000,
          easing: 'cubic-bezier(0.37, 0, 0.63, 1)',
          fill: 'forwards',
        });

        this.#cleanArcAnimation(arcAnimationElement);
      } catch (error) {
        console.error('[nsZenDownloadAnimationElement] Error in animation sequence:', error);
        this.#cleanArcAnimation(arcAnimationElement);
      }
    }

    #createArcAnimationSequence(distanceX, distanceY, arcHeight, shouldArcDownward) {
      const sequence = { offset: [], opacity: [], transform: [] };

      const arcDirection = shouldArcDownward ? 1 : -1;
      const steps = CONFIG.ANIMATION.ARC_STEPS;
      const endScale = CONFIG.ANIMATION.SCALE_END;

      function easeInOutQuad(t) {
        return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
      }

      let previousRotation = 0;
      for (let i = 0; i <= steps; i++) {
        const progress = i / steps;
        const eased = easeInOutQuad(progress);

        // Calculate opacity changes
        let opacity;
        if (progress < 0.3) {
          // Fade in during first 30%
          opacity = 0.3 + (progress / 0.3) * 0.6;
        } else if (progress < 0.98) {
          // Slight increase to full opacity
          opacity = 0.9 + ((progress - 0.3) / 0.6) * 0.1;
        } else {
          // Decrease opacity in the final steps
          opacity = 1 - ((progress - 0.9) / 0.1) * 1;
        }

        // Calculate scaling changes
        let scale;
        if (progress < 0.5) {
          scale = 0.5 + (progress / 0.5) * 1.3;
        } else {
          scale = 1.8 - ((progress - 0.5) / 0.5) * (1.8 - endScale);
        }

        // Position on arc
        const x = distanceX * eased;
        const y = distanceY * eased + arcDirection * arcHeight * (1 - (2 * eased - 1) ** 2);

        // Calculate rotation to point in the direction of movement
        let rotation = previousRotation;
        if (i > 0) {
          const prevEased = easeInOutQuad((i - 1) / steps);

          const prevX = distanceX * prevEased;
          const prevAdjustedProgress = prevEased * 2 - 1;
          const prevVerticalOffset = arcDirection * arcHeight * (1 - prevAdjustedProgress * 2);
          const prevY = distanceY * prevEased + prevVerticalOffset;

          const targetRotation = Math.atan2(y - prevY, x - prevX) * (180 / Math.PI);

          rotation += (targetRotation - previousRotation) * 0.01;
          previousRotation = rotation;
        }

        sequence.offset.push(progress);
        sequence.opacity.push(opacity);
        sequence.transform.push(
          `translate(calc(${x}px - 50%), calc(${y}px - 50%)) rotate(${rotation}deg) scale(${scale})`
        );
      }

      return sequence;
    }

    #cleanArcAnimation(element) {
      element.remove();
    }

    async #startBoxAnimation(areTabsPositionedRight) {
      // If animation is already in progress, don't start a new one
      if (this.#isBoxAnimationRunning) {
        console.warn(
          `[${nsZenDownloadAnimationElement.name}] Box animation already running, skipping new request.`
        );
        return;
      }

      if (this.#boxAnimationElement) {
        clearTimeout(this.#boxAnimationTimeoutId);
        this.#boxAnimationTimeoutId = setTimeout(
          () => this.#finishBoxAnimation(areTabsPositionedRight),
          this.#getBoxAnimationDurationMs()
        );
        return;
      }

      const wrapper = document.getElementById('zen-main-app-wrapper');
      if (!wrapper) {
        console.warn(
          `[${nsZenDownloadAnimationElement.name}] Cannot start box animation, Wrapper element not found`
        );
        return;
      }

      this.#isBoxAnimationRunning = true;

      try {
        const boxAnimationHTML = `
            <box class="zen-download-box-animation">
              <html:div class="zen-download-box-animation-icon"></html:div>
            </box>
          `;

        const sideProp = areTabsPositionedRight ? 'right' : 'left';

        const fragment = window.MozXULElement.parseXULToFragment(boxAnimationHTML);
        this.#boxAnimationElement = fragment.querySelector('.zen-download-box-animation');

        Object.assign(this.#boxAnimationElement.style, {
          bottom: '24px',
          transform: 'scale(0.8)',
          [sideProp]: '-50px',
        });

        wrapper.appendChild(this.#boxAnimationElement);

        await gZenUIManager.motion.animate(
          this.#boxAnimationElement,
          {
            [sideProp]: '34px',
            opacity: 1,
            transform: 'scale(1.1)',
          },
          {
            duration: 0.35,
            easing: 'ease-out',
          }
        ).finished;

        await gZenUIManager.motion.animate(
          this.#boxAnimationElement,
          {
            [sideProp]: '24px',
            transform: 'scale(1)',
          },
          {
            duration: 0.2,
            easing: 'ease-in-out',
          }
        ).finished;

        clearTimeout(this.#boxAnimationTimeoutId);
        this.#boxAnimationTimeoutId = setTimeout(
          () => this.#finishBoxAnimation(areTabsPositionedRight),
          this.#getBoxAnimationDurationMs()
        );
      } catch (error) {
        console.error(
          `[${nsZenDownloadAnimationElement.name}] Error during box entry animation: ${error}`
        );
        this.#cleanBoxAnimation();
      } finally {
        this.#isBoxAnimationRunning = false;
      }
    }

    #getBoxAnimationDurationMs() {
      return Services.prefs.getIntPref('zen.downloads.download-animation-duration') + 200;
    }

    async #finishBoxAnimation(areTabsPositionedRight) {
      clearTimeout(this.#boxAnimationTimeoutId);
      this.#boxAnimationTimeoutId = null;

      if (!this.#boxAnimationElement || this.#isBoxAnimationRunning) {
        if (!this.#boxAnimationElement) this.#cleanBoxAnimationState();
        return;
      }

      this.#isBoxAnimationRunning = true;

      try {
        const sideProp = areTabsPositionedRight ? 'right' : 'left';

        await gZenUIManager.motion.animate(
          this.#boxAnimationElement,
          {
            transform: 'scale(0.9)',
          },
          {
            duration: 0.15,
            easing: 'ease-in',
          }
        ).finished;

        await gZenUIManager.motion.animate(
          this.#boxAnimationElement,
          {
            [sideProp]: '-50px',
            opacity: 0,
            transform: 'scale(0.8)',
          },
          {
            duration: 0.3,
            easing: 'cubic-bezier(0.5, 0, 0.75, 0)',
          }
        ).finished;
      } catch (error) {
        console.warn(
          `[${nsZenDownloadAnimationElement.name}] Error during box exit animation: ${error}`
        );
      } finally {
        this.#cleanBoxAnimation();
      }
    }

    #cleanBoxAnimationState() {
      this.#boxAnimationElement = null;
      if (this.#boxAnimationTimeoutId) {
        clearTimeout(this.#boxAnimationTimeoutId);
        this.#boxAnimationTimeoutId = null;
      }
      this.#isBoxAnimationRunning = false;
    }

    #cleanBoxAnimation() {
      if (this.#boxAnimationElement && this.#boxAnimationElement.isConnected) {
        try {
          this.#boxAnimationElement.remove();
        } catch (error) {
          console.error(
            `[${nsZenDownloadAnimationElement.name}] Error removing box animation element: ${error}`,
            error
          );
        }
      }
      this.#cleanBoxAnimationState();
    }

    #isElementVisible(element) {
      if (!element) return false;

      const rect = element.getBoundingClientRect();

      // Element must be in the viewport
      // Is 1 and no 0 because if you pin the download button in the overflow menu
      // the download button is in the viewport but in the position 0,0 so this
      // avoid this case
      if (
        rect.bottom < 1 ||
        rect.right < 1 ||
        rect.top > window.innerHeight ||
        rect.left > window.innerWidth
      ) {
        return false;
      }

      return true;
    }
  }

  customElements.define('zen-download-animation', nsZenDownloadAnimationElement);

  new nsZenDownloadAnimation();
}
