(function () {
  "use strict";

  if (!window.LiveLangSettings) {
    return;
  }

  class LiveLangFrontend {
    constructor(settings) {
      this.settings = settings;
      this.enabled = false;
      this.undoStack = [];
      this.redoStack = [];
      this.currentElement = null;

      this.bar = document.getElementById("livelang-bar");

      this.init = this.init.bind(this);

      if (document.readyState === "loading") {
        document.addEventListener("DOMContentLoaded", this.init);
      } else {
        this.init();
      }
    }

    init() {
      this.languageSwitcherInit();
      this.dynamicTranslationInit();
      this.setDefaultLanguage();
      this.applyAlignments();
    }

    getAllLanguages() {
      // Return languages from settings (passed from PHP)
      if (
        this.settings.languages &&
        typeof this.settings.languages === "object"
      ) {
        return this.settings.languages;
      }
      // Fallback to defaults if not provided
      return {
        en: "English"
      };
    }

    getCleanPath() {
      const homeUrl = this.settings.homeUrl || window.location.origin;
      let homePath = "";
      try {
        homePath = new URL(homeUrl).pathname.replace(/\/$/, "");
      } catch (e) {}

      let path = window.location.pathname;
      if (homePath && path.startsWith(homePath)) {
        path = path.substring(homePath.length);
      }
      if (!path.startsWith("/")) path = "/" + path;

      // Remove existing language prefix ONLY if it's a valid registered language
      const languages = this.getAllLanguages();
      const match = path.match(/^\/([a-z]{2,5})(\/|$)/);
      if (match && languages[match[1]]) {
        path = path.replace(/^\/[a-z]{2,5}/, "");
        if (!path.startsWith("/")) path = "/" + path;
      }

      return path;
    }

    changeLanguage(langCode, reload = true) {
      if (!langCode || langCode === this.settings.currentLanguage) {
        return;
      }

      // Update current language
      this.settings.currentLanguage = langCode;

      if (reload) {
        const homeUrl = this.settings.homeUrl || window.location.origin;
        const baseUrl = homeUrl.replace(/\/$/, "");

        const path = this.getCleanPath();
        if (
          LiveLangSettings.defaultLanguage &&
          langCode === LiveLangSettings.defaultLanguage
        ) {
          window.location.href = baseUrl + path;
        } else {
          window.location.href = baseUrl + "/" + langCode + path;
        }
      }
    }

    updateLanguageDropdown() {
      if (!this.bar) return;

      const languages = this.getAllLanguages();
      const currentLang = this.settings.currentLanguage;
      const currentLabel = languages[currentLang] || currentLang;

      // Update button text
      const toggleBtn = this.bar.querySelector(".livelang-language-toggle");
      if (toggleBtn) {
        toggleBtn.innerHTML = `${currentLabel} <span class="livelang-toggle-icon">▼</span>`;
      }

      // Update active class in list
      const listItems = this.bar.querySelectorAll(".livelang-language-list a");
      listItems.forEach((a) => {
        if (a.getAttribute("data-lang") === currentLang) {
          a.classList.add("active");
        } else {
          a.classList.remove("active");
        }
      });
    }

    setDefaultLanguage() {
      if (
        this.settings.currentLanguage == undefined ||
        this.settings.currentLanguage == null
      ) {
        this.settings.currentLanguage =
          LiveLangSettings.defaultLanguage || "en";
      }
      this.updateLanguageDropdown();
    }

    languageSwitcherInit() {
      document.addEventListener("click", (e) => {
        // Handle toggle button
        const toggleBtn = e.target.closest(".livelang-language-toggle");
        if (toggleBtn) {
          e.preventDefault();
          const dropdown = toggleBtn.closest(".livelang-language-dropdown");
          dropdown.classList.toggle("open");
          //return;
        }

        // Handle language selection
        const langLink = e.target.closest("a[data-lang]");
        if (langLink) {
          e.preventDefault();
          const langCode = langLink.getAttribute("data-lang");

          if (langLink.closest("#livelang-toggle")) {
            this.changeLanguage(langCode, false); // false = no reload
            this.updateLanguageDropdown();
          }

          const homeUrl = this.settings.homeUrl || window.location.origin;
          const baseUrl = homeUrl.replace(/\/$/, "");
          const path = this.getCleanPath();

          const date = new Date();
          date.setTime(date.getTime() + 30 * 24 * 60 * 60 * 1000); // 30 days
          document.cookie =
            "livelang_lang=" +
            langCode +
            ";expires=" +
            date.toUTCString() +
            ";path=/";

          const query = window.location.search;
          const hash = window.location.hash;

          // Omit language prefix if switching to default language
          if ( langCode === LiveLangSettings.defaultLanguage ) {
            const finalPath = (path === "/" || path === "") ? "/" : path;
            window.location.href = baseUrl + finalPath + query + hash;
            return;
          }

          const langPrefix = "/" + langCode;
          const finalPath = (path === "/" || path === "") ? "/" : path;
          window.location.href = baseUrl + langPrefix + finalPath + query + hash;
          return;
        }

        // Close dropdown when clicking outside
        const dropdown = document.querySelector(
          ".livelang-language-dropdown.open",
        );
        if (dropdown && !e.target.closest(".livelang-language-dropdown")) {
          dropdown.classList.remove("open");
        }
      });
    }

    dynamicTranslationInit() {
      if (!this.shouldTranslateDynamicContent()) {
        return;
      }

      // Version counter bumped whenever the dictionary changes; lets us
      // cheaply skip text nodes we've already translated under the current
      // dictionary without re-running the matcher on them.
      this.matcherVersion = 0;
      this.processedVersion = new WeakMap();
      this.processedAttrVersion = new WeakMap();
      this.pendingRoots = new Set();
      this.rafScheduled = false;

      // IMPORTANT: do NOT run applyTranslations() over the initial
      // document.body here. The server (buffer_callback() /
      // translate_html_string()) has already translated the full page
      // HTML using this exact same dictionary before it ever reached the
      // browser. Running the client-side matcher over that HTML again
      // would re-match the original text as a substring of the already
      // translated text (e.g. original "Shop" is a substring of the
      // translated "Shop Page") and append the replacement a second time,
      // producing "Shop Page Page".
      //
      // Instead we just stamp everything currently on the page as
      // "already handled" for the current dictionary version. Only nodes
      // that are genuinely new (added later via AJAX/React re-renders,
      // caught by the MutationObserver/WooCommerce hooks below) will run
      // through the real translator.
      this.markAsAlreadyTranslated(document.body);

      if (window.jQuery) {
        // WooCommerce fires these on full fragment/cart/checkout refreshes
        // (both the legacy AJAX cart and most Blocks-era re-renders bubble
        // through DOM changes the MutationObserver below also catches, but
        // these events are cheap, infrequent, and give us a reliable
        // "definitely changed, re-check everything relevant" signal).
        //
        // NOTE: WooCommerce fragment HTML is already translated server-side
        // by translate_cart_fragments() (same map/engine as the initial
        // page). By the time this event fires, the fragment has already
        // been injected into the DOM, so re-running the real translator
        // over document.body here would hit the same substring
        // double-translation bug as the initial page load. We only mark
        // it as handled, same as we do on load.
        window.jQuery(document.body).on(
          "updated_checkout wc_fragments_loaded wc_fragments_refreshed added_to_cart removed_from_cart",
          () => {
            this.markAsAlreadyTranslated(document.body);
          },
        );
      }

      if (!window.MutationObserver) {
        return;
      }

      const observer = new MutationObserver((mutations) => {
        mutations.forEach((mutation) => {
          if (mutation.type === "characterData") {
            const parent = mutation.target && mutation.target.parentElement;
            if (parent) {
              this.pendingRoots.add(parent);
            }
            return;
          }

          mutation.addedNodes &&
            mutation.addedNodes.forEach((node) => {
              if (node.nodeType === Node.ELEMENT_NODE) {
                this.pendingRoots.add(node);
              } else if (node.nodeType === Node.TEXT_NODE && node.parentElement) {
                this.pendingRoots.add(node.parentElement);
              }
            });
        });

        if (this.pendingRoots.size) {
          this.scheduleFlush();
        }
      });

      observer.observe(document.body, {
        childList: true,
        characterData: true,
        subtree: true,
      });
    }

    /**
     * Batches translation passes into a single requestAnimationFrame flush
     * so rapid bursts of DOM mutations (React re-renders during WooCommerce
     * Blocks cart/checkout updates, quantity steppers, etc.) only trigger
     * one translation pass per frame instead of one per mutation.
     */
    scheduleFlush() {
      if (this.rafScheduled) {
        return;
      }
      this.rafScheduled = true;

      const flush = () => {
        this.rafScheduled = false;
        const roots = Array.from(this.pendingRoots);
        this.pendingRoots.clear();
        roots.forEach((root) => {
          if (root && root.isConnected) {
            this.applyTranslations(root);
          }
        });
      };

      if (window.requestAnimationFrame) {
        window.requestAnimationFrame(flush);
      } else {
        window.setTimeout(flush, 16);
      }
    }

    shouldTranslateDynamicContent() {
      return (
        this.settings.currentLanguage &&
        Array.isArray(this.settings.dict) &&
        this.settings.dict.length > 0 &&
        document.body
      );
    }

    /**
     * Builds (and caches) a single compiled RegExp alternation covering
     * every literal translation pair, plus a Map for O(1) lookup of the
     * matched text's translation. This replaces running a `split/join` for
     * every stored pair against every text node (O(pairs × nodes)) with a
     * single O(text length) pass per node. Numeric "::NUM::" placeholder
     * pairs can't be folded into a plain alternation (they need capture
     * groups), so they stay in a small separate list applied afterwards.
     */
    getMatcher() {
      if (this.matcher && this.matcher.version === this.matcherVersion) {
        return this.matcher;
      }

      const literalMap = new Map();
      const literalPairs = [];
      const translatedSet = new Set();
      const numPairs = [];
      const htmlPairs = [];

      this.settings.dict.forEach((entry) => {
        if (!entry || !entry.original || !entry.translated) {
          return;
        }
        let original = String(entry.original);
        const translated = String(entry.translated);

        translatedSet.add(translated.trim());

        const isMediaUrl = /\.(png|jpe?g|webp|gif|svg|avif|bmp|ico)(\?.*)?$/i.test(original) || original.includes('/uploads/') || original.startsWith('http://') || original.startsWith('https://');

        if (original.indexOf("<") !== -1 && original.indexOf(">") !== -1) {
          original = original.replace(/<br\s*\/?>/gi, "<br>");
          htmlPairs.push({ original, translated });
        } else if (!isMediaUrl && (original.indexOf("::NUM::") !== -1 || translated.indexOf("{NUM}") !== -1 || /\d+/.test(original))) {
          let normOrig = original.indexOf("::NUM::") !== -1 ? original : original.replace(/\d+/g, "::NUM::");
          let normTrans = translated.indexOf("{NUM}") !== -1 ? translated : translated.replace(/\d+/g, "{NUM}");
          numPairs.push({ original: normOrig, translated: normTrans });
        } else if (!literalMap.has(original)) {
          literalMap.set(original, translated);
          literalPairs.push({ original, translated });
        }
      });

      // Longest-first so e.g. "Add to Cart Now" is matched before "Add to Cart".
      literalPairs.sort((a, b) => b.original.length - a.original.length);

      const sortedKeys = Array.from(literalMap.keys()).sort(
        (a, b) => b.length - a.length,
      );

      const regex = sortedKeys.length
        ? new RegExp(
            sortedKeys.map((key) => this.escapeRegExp(key)).join("|"),
            "g",
          )
        : null;

      this.matcher = {
        version: this.matcherVersion,
        regex,
        map: literalMap,
        literalPairs,
        translatedSet,
        numPairs,
        htmlPairs,
      };

      return this.matcher;
    }

    escapeRegExp(str) {
      return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
    }

    refreshTranslationPairs() {
      this.matcherVersion++;
      this.matcher = null;
    }

    /**
     * Walks the same nodes/attributes applyTranslations() would touch, but
     * only stamps them as processed for the current dictionary version
     * instead of running the matcher. Used once at page load to mark the
     * server-already-translated initial HTML so it's never re-run through
     * the client-side matcher (see comment in dynamicTranslationInit()).
     */
    markAsAlreadyTranslated(root) {
      if (!root) {
        return;
      }

      const walker = document.createTreeWalker(
        root.nodeType === Node.TEXT_NODE ? root.parentElement || root : root,
        NodeFilter.SHOW_TEXT,
        {
          acceptNode: (node) => {
            if (!node.nodeValue || !node.nodeValue.trim()) {
              return NodeFilter.FILTER_REJECT;
            }
            const parent = node.parentElement;
            if (!parent || this.shouldSkipTranslationNode(parent)) {
              return NodeFilter.FILTER_REJECT;
            }
            return NodeFilter.FILTER_ACCEPT;
          },
        },
      );

      while (walker.nextNode()) {
        this.processedVersion.set(walker.currentNode, this.matcherVersion);
      }

      const attrSelector =
        "[placeholder], [title], [alt], [aria-label], [data-title]";
      const rootEl = root.nodeType === Node.ELEMENT_NODE ? root : root.parentElement;

      if (rootEl) {
        const attrCandidates = rootEl.matches && rootEl.matches(attrSelector)
          ? [rootEl, ...rootEl.querySelectorAll(attrSelector)]
          : Array.from(rootEl.querySelectorAll(attrSelector));

        attrCandidates.forEach((el) => {
          this.processedAttrVersion.set(el, this.matcherVersion);
        });

        const valueInputs = rootEl.matches && rootEl.matches('input[type="submit"], input[type="button"]')
          ? [rootEl]
          : Array.from(rootEl.querySelectorAll('input[type="submit"], input[type="button"]'));

        valueInputs.forEach((el) => {
          this.processedAttrVersion.set(el, this.matcherVersion);
        });
      }
      this.applyAlignments(rootEl || document.body);
    }
    
    applyAlignments(root = document.body) {
      if (!this.settings.dict || !Array.isArray(this.settings.dict)) return;
      
      const alignmentItems = this.settings.dict.filter(item => item.alignment && item.alignment !== '');
      if (alignmentItems.length === 0) return;
      
      const walker = document.createTreeWalker(
        root,
        NodeFilter.SHOW_TEXT,
        {
          acceptNode: (node) => {
            if (!node.nodeValue.trim()) return NodeFilter.FILTER_REJECT;
            if (node.parentElement && this.shouldSkipTranslationNode(node.parentElement)) return NodeFilter.FILTER_REJECT;
            return NodeFilter.FILTER_ACCEPT;
          }
        }
      );
      
      while (walker.nextNode()) {
        const node = walker.currentNode;
        const text = node.nodeValue.trim().replace(/\s+/g, ' ');
        if (!text) continue;
        
        const match = alignmentItems.find(item => item.translated === text || item.original === text);
        if (match && match.alignment) {
           const p = node.parentElement;
           if (p) {
             p.classList.add(`ll-align-${match.alignment}`);
             const blockParent = p.closest('p, h1, h2, h3, h4, h5, h6, div, td, th, li, section, article');
             if (blockParent) {
               blockParent.classList.add(`ll-align-${match.alignment}`);
             }
           }
        }
      }
    }

    applyTranslations(root) {
      if (!root || !this.shouldTranslateDynamicContent()) {
        return;
      }

      const matcher = this.getMatcher();
      if (!matcher.regex && !matcher.numPairs.length && !matcher.htmlPairs.length) {
        return;
      }

      if (matcher.htmlPairs.length > 0) {
          const rootEl = root.nodeType === Node.ELEMENT_NODE ? root : root.parentElement;
          if (rootEl) {
              const elWalker = document.createTreeWalker(
                  rootEl,
                  NodeFilter.SHOW_ELEMENT,
                  {
                      acceptNode: (node) => {
                          if (this.shouldSkipTranslationNode(node)) return NodeFilter.FILTER_REJECT;
                          return NodeFilter.FILTER_ACCEPT;
                      }
                  }
              );
              
              const elements = [];
              if (!this.shouldSkipTranslationNode(rootEl)) {
                  elements.push(rootEl);
              }
              while (elWalker.nextNode()) {
                  elements.push(elWalker.currentNode);
              }
              
              elements.forEach(el => {
                  if (this.processedVersion.get(el) === this.matcherVersion) return;
                  if (el.children.length === 0) return;
                  
                  let htmlStr = (el.innerHTML || "").replace(/\s+/g, " ").trim();
                  htmlStr = htmlStr.replace(/<br\s*\/?>/gi, "<br>");
                  if (!htmlStr) return;

                  for (let i = 0; i < matcher.htmlPairs.length; i++) {
                      if (htmlStr === matcher.htmlPairs[i].original) {
                          el.innerHTML = matcher.htmlPairs[i].translated;
                          this.processedVersion.set(el, this.matcherVersion);
                          break;
                      }
                  }
              });
          }
      }

      const walker = document.createTreeWalker(
        root.nodeType === Node.TEXT_NODE ? root.parentElement || root : root,
        NodeFilter.SHOW_TEXT,
        {
          acceptNode: (node) => {
            if (!node.nodeValue || !node.nodeValue.trim()) {
              return NodeFilter.FILTER_REJECT;
            }

            if (this.processedVersion.get(node) === this.matcherVersion) {
              return NodeFilter.FILTER_REJECT;
            }

            const parent = node.parentElement;
            if (!parent || this.shouldSkipTranslationNode(parent)) {
              return NodeFilter.FILTER_REJECT;
            }

            return NodeFilter.FILTER_ACCEPT;
          },
        },
      );

      const textNodes = [];
      while (walker.nextNode()) {
        textNodes.push(walker.currentNode);
      }

      textNodes.forEach((node) => {
        const translated = this.translateString(node.nodeValue, matcher);
        if (translated !== node.nodeValue) {
          node.nodeValue = translated;
        }
        this.processedVersion.set(node, this.matcherVersion);
      });

      const attrSelector =
        "[placeholder], [title], [alt], [aria-label], [data-title]";
      const attrs = ["placeholder", "title", "alt", "aria-label", "data-title"];
      const rootEl = root.nodeType === Node.ELEMENT_NODE ? root : root.parentElement;

      if (rootEl) {
        const attrCandidates = rootEl.matches && rootEl.matches(attrSelector)
          ? [rootEl, ...rootEl.querySelectorAll(attrSelector)]
          : Array.from(rootEl.querySelectorAll(attrSelector));

        attrCandidates.forEach((el) => {
          if (this.shouldSkipTranslationNode(el)) {
            return;
          }

          // Skip elements whose attributes we've already translated under
          // this dictionary version, so an unrelated mutation elsewhere on
          // the page (which pulls this element in as part of a subtree
          // walk) can't cause the same attribute to be translated again on
          // top of itself.
          if (this.processedAttrVersion.get(el) === this.matcherVersion) {
            return;
          }

          attrs.forEach((attr) => {
            const value = el.getAttribute(attr);
            if (!value) {
              return;
            }
            const translated = this.translateString(value, matcher);
            if (translated !== value) {
              el.setAttribute(attr, translated);
            }
          });

          this.processedAttrVersion.set(el, this.matcherVersion);
        });

        // Image & LazyLoad Attribute Handling
        const imgSelector = "img[src], img[data-src], img[data-lazy-src], img[data-orig-src], source[srcset]";
        const imgs = rootEl.matches && rootEl.matches(imgSelector)
          ? [rootEl, ...rootEl.querySelectorAll(imgSelector)]
          : Array.from(rootEl.querySelectorAll(imgSelector));

        imgs.forEach((img) => {
          if (this.shouldSkipTranslationNode(img)) return;
          const currentSrc = img.getAttribute("src") || img.getAttribute("data-src") || img.getAttribute("data-lazy-src") || "";
          if (!currentSrc) return;

          const translated = this.translateString(currentSrc, matcher);
          if (translated && translated !== currentSrc) {
            img.setAttribute("src", translated);
            if (img.hasAttribute("data-src")) img.setAttribute("data-src", translated);
            if (img.hasAttribute("data-lazy-src")) img.setAttribute("data-lazy-src", translated);
            if (img.hasAttribute("data-orig-src")) img.setAttribute("data-orig-src", translated);

            // Strip srcset to prevent LazyLoad / Responsive browser override
            img.removeAttribute("srcset");
            img.removeAttribute("data-srcset");
            img.removeAttribute("data-lazy-srcset");
            img.removeAttribute("sizes");
          }
        });

        // Inline CSS Background Image Handling
        const bgSelector = "[style*='background']";
        const bgEls = rootEl.matches && rootEl.matches(bgSelector)
          ? [rootEl, ...rootEl.querySelectorAll(bgSelector)]
          : Array.from(rootEl.querySelectorAll(bgSelector));

        bgEls.forEach((el) => {
          if (this.shouldSkipTranslationNode(el)) return;
          const styleVal = el.getAttribute("style");
          if (!styleVal || styleVal.indexOf("url(") === -1) return;

          const translatedStyle = this.translateString(styleVal, matcher);
          if (translatedStyle && translatedStyle !== styleVal) {
            el.setAttribute("style", translatedStyle);
          }
        });

        // Legacy `<input type="submit"|"button" value="...">` buttons (some
        // themes/checkout templates still use these instead of <button>).
        const valueInputs = rootEl.matches && rootEl.matches('input[type="submit"], input[type="button"]')
          ? [rootEl]
          : Array.from(rootEl.querySelectorAll('input[type="submit"], input[type="button"]'));

        valueInputs.forEach((el) => {
          if (this.shouldSkipTranslationNode(el)) {
            return;
          }
          if (this.processedAttrVersion.get(el) === this.matcherVersion) {
            return;
          }
          const value = el.getAttribute("value");
          if (!value) {
            return;
          }
          const translated = this.translateString(value, matcher);
          if (translated !== value) {
            el.setAttribute("value", translated);
          }
          this.processedAttrVersion.set(el, this.matcherVersion);
        });
      }
    }

    shouldSkipTranslationNode(el) {
      return !!(
        el.closest("script, style, noscript, code, pre, textarea, #wpadminbar, #livelang-editor-tooltip-container, #livelang-floating-switcher") ||
        el.closest(".livelang-language-dropdown") ||
        el.isContentEditable
      );
    }

    translateString(value, matcher) {
      if (!value || typeof value !== "string") return value;
      let text = value;

      if (matcher.translatedSet && matcher.translatedSet.has(text.trim())) {
        return text;
      }

      if (matcher.literalPairs && matcher.literalPairs.length > 0) {
        matcher.literalPairs.forEach((pair) => {
          if (!pair.original || !pair.translated) return;
          if (text.indexOf(pair.translated) !== -1) {
            return;
          }
          if (text.indexOf(pair.original) !== -1) {
            const escaped = this.escapeRegExp(pair.original);
            text = text.replace(new RegExp(escaped, "g"), pair.translated);
          }
        });
      }

      matcher.numPairs.forEach((pair) => {
        if (text.indexOf(pair.translated) !== -1) {
          return;
        }
        const escapedParts = pair.original
          .split("::NUM::")
          .map((part) => this.escapeRegExp(part));
        const pattern = new RegExp(escapedParts.join("([0-9]+)"), "g");

        text = text.replace(pattern, (...matches) => {
          let index = 1;
          return pair.translated.replace(/\{NUM\}/g, () => matches[index++] || "");
        });
      });

      return text;
    }
  }

  // single instance (for debug: window.LiveLangFrontendInstance)
  window.LiveLangFrontendInstance = new LiveLangFrontend(
    window.LiveLangSettings,
  );
})();
