/**
 * @typedef {Object} ExtensionData
 * @property {string[]} commonWords
 * @property {Fix[]} fixes
 * @property {{ domains: string[], tags: string[] }} skips
 * @property {{ backdrops: string[], classes: string[], containers: string[], selectors: string[] }} tokens
 */

/**
 * @typedef {Object} ContentState
 * @property {boolean} on
 */

/**
 * @typedef {Object} Fix
 * @property {string} action
 * @property {string} domain
 * @property {string} [property]
 * @property {string} selector
 */

/**
 * @typedef {Object} RunParams
 * @property {HTMLElement[]} [containers]
 * @property {HTMLElement[]} [elements]
 * @property {boolean} [skipMatch]
 */

/**
 * @typedef {Object} GetElementsParams
 * @property {boolean} [filterEarly]
 * @property {HTMLElement} [from]
 */

/**
 * @typedef {Object} SetUpParams
 * @property {boolean} [skipRunFn]
 */

if (typeof browser === 'undefined') {
  browser = chrome;
}

/**
 * @description Actions done by the extension
 * @type {Set<string>}
 */
const actions = new Set();

/**
 * @description Data object with all the necessary information
 * @type {ExtensionData}
 */
let { commonWords, fixes, skips, tokens } = {
  commonWords: [],
  fixes: [],
  skips: {
    domains: [],
    tags: [],
  },
  tokens: {
    backdrops: [],
    classes: [],
    selectors: [],
  },
};

/**
 * @description Shortcut to send messages to background script
 */
const dispatch = browser.runtime.sendMessage;

/**
 * @description Current hostname
 * @type {string}
 */
const hostname = getHostname();

/**
 * @description Initial visibility state
 * @type {boolean}
 */
let initiallyVisible = document.visibilityState === 'visible';

/**
 * @description Options provided to observer
 * @type {MutationObserverInit}
 */
const options = { childList: true, subtree: true };

/**
 * @description Elements that were already matched and are removable
 * @type {Set<HTMLElement>}
 */
const seen = new Set();

/**
 * @description Extension state
 * @type {ContentState}
 */
let state = { on: true };

/**
 * @description Clean DOM
 * @param {Element[]} elements
 * @param {boolean} [skipMatch]
 * @returns {void}
 */
function clean(elements, skipMatch) {
  let index = 0;
  const size = 50;

  function chunk() {
    const end = Math.min(index + size, elements.length);

    for (; index < end; index++) {
      const element = elements[index];

      if (match(element, skipMatch)) {
        if (element instanceof HTMLDialogElement) element.close();
        hide(element);

        actions.add(`${Date.now()}`);
        dispatch({ type: 'UPDATE_BADGE', value: actions.size });
      }

      seen.add(element);
    }

    if (index < elements.length) {
      requestAnimationFrame(chunk);
    }
  }

  requestAnimationFrame(chunk);
}

/**
 * @description Check if element contains a common word
 * @param {HTMLElement} element
 */
function containsCommonWord(element) {
  return !!commonWords.length && !!element.outerHTML.match(new RegExp(commonWords.join('|')));
}

/**
 * @description Force a DOM clean in the specific element
 * @param {HTMLElement} from
 * @returns {void}
 */
function forceClean(from) {
  const elements = getElements(tokens.selectors, { filterEarly: true, from });

  if (elements.length) {
    fix();
    clean(elements, true);
  }
}

/**
 * Get all elements that match the selector
 * @param {string | string[]} [selector]
 * @param {GetElementsParams} [params]
 * @returns {HTMLElement[]}
 */
function getElements(selector, params = {}) {
  const { filterEarly, from } = params;
  let result = [];

  if (selector?.length) {
    result = [...(from ?? document).querySelectorAll(selector)];

    if (filterEarly) {
      result = result.flatMap((node) => filterNodeEarly(node));
    }
  }

  return result;
}

/**
 * Get all elements with their children that match the selector
 * @param {string | string[]} selector
 * @param {GetElementsParams} [params]
 * @returns {HTMLElement[]}
 */
function getElementsWithChildren(selector, params) {
  return getElements(selector, params).flatMap((element) => [element, ...element.children]);
}

/**
 * @description Calculate current hostname
 * @returns {string}
 */
function getHostname() {
  let hostname = document.location.hostname;
  const referrer = document.referrer;

  if (referrer && window.self !== window.top) {
    hostname = new URL(referrer).hostname;
  }

  return hostname.split('.').slice(-3).join('.').replace('www.', '');
}

/**
 * @description Check if an element is visible in the viewport
 * @param {HTMLElement} element
 * @returns {boolean}
 */
function isInViewport(element) {
  const styles = window.getComputedStyle(element);
  const height = window.innerHeight || document.documentElement.clientHeight;
  const position = element.getBoundingClientRect();
  const scroll = window.scrollY;

  return (
    position.bottom === position.top ||
    (scroll + position.top <= scroll + height && scroll + position.bottom >= scroll) ||
    styles.animationDuration !== '0s' ||
    styles.transitionDuration !== '0s'
  );
}

/**
 * @description Check if element element is removable
 * @param {Element} element
 * @param {boolean} [skipMatch]
 * @returns {boolean}
 */
function match(element, skipMatch) {
  if (!tokens.selectors.length || !skips.tags.length) {
    return false;
  }

  if (!(element instanceof HTMLElement) || !element.tagName) {
    return false;
  }

  if (seen.has(element)) {
    return false;
  }

  const tagName = element.tagName.toUpperCase();

  if (skips.tags.includes(tagName)) {
    return false;
  }

  const hasAttributes = !!element.getAttributeNames().filter((x) => x !== 'data-nosnippet').length;

  if (!hasAttributes && !tagName.includes('-')) {
    forceClean(element);
  }

  // 2023-06-10: fix #113 temporarily
  if (element.classList.contains('chat-line__message')) {
    return false;
  }

  // 2024-08-03: fix #701 temporarily
  if (element.classList.contains('sellos')) {
    return false;
  }

  const isDialog = tagName === 'DIALOG' && element.getAttribute('open') === 'true';
  const isFakeDialog = tagName === 'DIV' && element.className.includes('cmp');

  return (
    (isDialog || isFakeDialog || isInViewport(element)) &&
    (skipMatch || element.matches(tokens.selectors))
  );
}

/**
 * @description Filter early nodes
 * @param {Node} node
 * @param {boolean} stopRecursion
 * @returns {HTMLElement[]}
 */
function filterNodeEarly(node, stopRecursion) {
  if (node.nodeType !== Node.ELEMENT_NODE || !(node instanceof HTMLElement)) {
    return [];
  }

  if (commonWords && containsCommonWord(node) && !stopRecursion) {
    return [node, ...[...node.children].flatMap((node) => filterNodeEarly(node, true))];
  }

  return [node];
}

/**
 * @description Fix data, middle consent page and scroll issues
 * @returns {void}
 */
function fix() {
  const backdrops = getElements(tokens.backdrops);
  const domains = skips.domains.map((x) => (x.split('.').length < 3 ? `*${x}` : x));

  for (const backdrop of backdrops) {
    if (backdrop.children.length === 0 && !seen.has(backdrop)) {
      actions.add(`${Date.now()}`);
      seen.add(backdrop);
      hide(backdrop);
    }
  }

  if (domains.every((x) => !hostname.match(x.replaceAll(/\*/g, '[^ ]*')))) {
    for (const element of [document.body, document.documentElement]) {
      element?.classList.remove(...(tokens.classes ?? []));
      element?.style.setProperty('position', 'initial', 'important');
      element?.style.setProperty('overflow-y', 'initial', 'important');
    }
  }

  for (const fix of fixes) {
    const { action, domain, property, selector } = fix;

    if (hostname.includes(domain)) {
      switch (action) {
        case 'click': {
          const element = document.querySelector(selector);

          actions.add('click');
          element?.click();
          break;
        }
        case 'remove': {
          const element = document.querySelector(selector);

          actions.add('remove');
          element?.style?.removeProperty(property);
          break;
        }
        case 'reload': {
          window.location.reload();
          break;
        }
        case 'reset': {
          const element = document.querySelector(selector);

          actions.add('reset');
          element?.style?.setProperty(property, 'initial', 'important');
          break;
        }
        case 'resetAll': {
          const elements = getElements(selector);

          actions.add('resetAll');
          elements.forEach((e) => e?.style?.setProperty(property, 'initial', 'important'));
          break;
        }
      }
    }
  }

  const ionRouterOutlet = document.getElementsByTagName('ion-router-outlet')[0];

  if (ionRouterOutlet) {
    actions.add('ion-router-outlet');
    // 2024-08-02: fix #644 temporarily
    ionRouterOutlet.removeAttribute('inert');
  }

  const t4Wrapper = document.getElementsByClassName('t4-wrapper')[0];

  if (t4Wrapper) {
    actions.add('t4-wrapper');
    // 2024-09-12: fix #945 temporarily
    t4Wrapper.removeAttribute('inert');
  }

  dispatch({ type: 'UPDATE_BADGE', value: actions.size });
}

/**
 * @description Hide DOM element
 * @param {HTMLElement} element
 * @returns {void}
 */
function hide(element) {
  element.style.setProperty('clip-path', 'circle(0px)', 'important');
  element.style.setProperty('display', 'none', 'important');
  element.style.setProperty('height', '0px', 'important');
  element.style.setProperty('overflow', 'hidden', 'important');
  element.style.setProperty('transform', 'scale(0)', 'important');
}

/**
 * @description Clean DOM when this function is called
 * @param {RunParams} [params]
 * @returns {void}
 */
function run(params = {}) {
  const { containers, elements, skipMatch } = params;

  if (document.body?.children.length && state.on && tokens.selectors.length) {
    fix();

    if (elements?.length) {
      clean(elements, skipMatch);
    }

    if (elements === undefined && containers?.length) {
      clean(containers.flatMap((x) => getElementsWithChildren(x, { filterEarly: true })));
    }
  }
}

/**
 * @async
 * @description Set up the extension
 * @param {SetUpParams} [params]
 */
async function setUp(params = {}) {
  state = await dispatch({ hostname, type: 'GET_STATE' });
  dispatch({ type: 'ENABLE_POPUP' });

  if (state.on) {
    const data = await dispatch({ hostname, type: 'GET_DATA' });

    commonWords = data?.commonWords ?? commonWords;
    fixes = data?.fixes ?? fixes;
    skips = data?.skips ?? skips;
    tokens = data?.tokens ?? tokens;

    dispatch({ type: 'ENABLE_REPORT' });
    dispatch({ hostname, type: 'ENABLE_ICON' });
    dispatch({ type: 'UPDATE_BADGE', value: actions.size });
    observer.observe(document.body ?? document.documentElement, options);
    if (!params.skipRunFn) run({ containers: tokens.containers });
  } else {
    dispatch({ type: 'DISABLE_REPORT' });
    dispatch({ type: 'DISABLE_ICON' });
    dispatch({ type: 'UPDATE_BADGE', value: actions.size });
    observer.disconnect();
  }
}

/**
 * @description Wait for the body to exist
 * @param {void} callback
 * @returns {void}
 */
async function setUpAfterWaitForBody() {
  if (document.body) {
    await setUp();
  } else {
    setTimeout(setUpAfterWaitForBody, 50);
  }
}

/**
 * @description Mutation Observer instance
 * @type {MutationObserver}
 */
const observer = new MutationObserver((mutations) => {
  if (!state.on || !tokens.selectors.length) {
    return;
  }

  const nodes = mutations.flatMap((mutation) => [...mutation.addedNodes]);
  const elements = nodes.flatMap((node) => filterNodeEarly(node));

  run({ elements });
});

/**
 * @description Listen to messages from any other scripts
 * @listens browser.runtime#onMessage
 */
browser.runtime.onMessage.addListener(async (message) => {
  switch (message.type) {
    case 'INCREASE_ACTIONS_COUNT': {
      actions.add(`${Date.now()}`);
      break;
    }
  }
});

/**
 * @description Fix bfcache issues
 * @listens window#pageshow
 * @returns {void}
 */
window.addEventListener('pageshow', async (event) => {
  if (document.visibilityState === 'visible' && event.persisted) {
    await setUp();
  }
});

/**
 * @async
 * @description Run if the page wasn't visited yet
 * @listens window#visibilitychange
 * @returns {void}
 */
window.addEventListener('visibilitychange', async () => {
  if (document.visibilityState === 'visible') {
    if (!initiallyVisible) {
      initiallyVisible = true;
      await setUp();
    }

    dispatch({ type: state.on ? 'ENABLE_REPORT' : 'DISABLE_REPORT' });
  }
});

/**
 * @description Run as soon as possible, if the user is in front of the page
 */
if (document.visibilityState === 'visible') {
  setUpAfterWaitForBody();
}
