// Copyright (C) 2023-2025 IITC-CE - GPL-3.0 with Store Exception - see LICENSE and COPYING.STORE

const CACHE = {};
const RE_URL = /(.*?):\/\/([^/]*)\/(.*)/;

/**
 * Checks the URL for match/include plugin.
 *
 * @param {plugin} meta - Object with data from ==UserScript== header.
 * @param {string} url - Page URL.
 * @return {boolean}
 */
export function check_matching(meta, url) {
  const match = meta.match || [];
  const include = meta.include || [];
  const match_exclude = meta['exclude-match'] || [];
  const exclude = meta.exclude || [];

  // match all if no @match or @include rule and set url === '<all_ingress>'
  let ok = !match.length && !include.length && url === '<all_ingress>';
  // @match
  ok = ok || testMatch(url, match);
  // @include
  ok = ok || testInclude(url, include);
  // @exclude-match
  ok = ok && !testMatch(url, match_exclude);
  // @exclude
  ok = ok && !testInclude(url, exclude);
  return ok;
}

function str2RE(str) {
  const re = str.replace(/([.?/])/g, '\\$1').replace(/\*/g, '.*?');
  return RegExp(`^${re}$`);
}

/**
 * Test glob rules like `@include` and `@exclude`.
 */
export function testInclude(url, rules) {
  return rules.some(rule => {
    const key = `re:${rule}`;
    let re = CACHE[key];
    if (!re) {
      re = makeIncludeRegExp(rule);
      CACHE[key] = re;
    }
    return re.test(url);
  });
}

function makeIncludeRegExp(str) {
  if (str.length > 1 && str[0] === '/' && str[str.length - 1] === '/') {
    return RegExp(str.slice(1, -1)); // Regular-expression
  }
  return str2RE(str); // Wildcard
}

/**
 * Test match rules like `@match` and `@exclude_match`.
 */
export function testMatch(url, rules) {
  return rules.some(rule => {
    const key = `match:${rule}`;
    let matcher = CACHE[key];
    if (!matcher) {
      matcher = makeMatchRegExp(rule);
      CACHE[key] = matcher;
    }
    return matcher.test(url);
  });
}

function makeMatchRegExp(rule) {
  let test;
  if (rule === '<all_urls>') test = () => true;
  else {
    const ruleParts = rule.match(RE_URL);
    test = url => {
      const parts = url.match(RE_URL);
      return (
        !!ruleParts &&
        !!parts &&
        matchScheme(ruleParts[1], parts[1]) &&
        matchHost(ruleParts[2], parts[2]) &&
        matchPath(ruleParts[3], parts[3])
      );
    };
  }
  return { test };
}

function matchScheme(rule, data) {
  // exact match
  if (rule === data) return 1;
  // * = http | https
  if (rule === '*' && /^https?$/i.test(data)) return 1;
  return 0;
}

function matchHost(rule, data) {
  // * matches all
  if (rule === '*') return 1;
  // exact match
  if (rule === data) return 1;
  // *.example.com
  if (/^\*\.[^*]*$/.test(rule)) {
    // matches the specified domain
    if (rule.slice(2) === data) return 1;
    // matches subdomains
    if (str2RE(rule).test(data)) return 1;
  }
  return 0;
}

function matchPath(rule, data) {
  return str2RE(rule).test(data);
}

/**
 * Returns information about the domains for which the script will be enabled.
 * Returns null if @match and @include are not specified
 * Returns <all_urls> if the script will be run for all domains. Additional URL Scheme and Path filters are not taken into account.
 * Otherwise, it returns a list of strings with domains.
 *
 * @param {plugin} meta - Object with data from ==UserScript== header.
 * @return {null|string|Array.<string>}
 */
export function humanize_match(meta) {
  const match = meta.match || [];
  const include = meta.include || [];
  const matches = match.concat(include);

  if (!matches.length) return null;
  if (matches.includes('<all_urls>')) return '<all_urls>';

  const domains = [];
  for (const item of matches) {
    const parts = item.match(RE_URL);
    if (!parts) continue;

    const [, , domain] = parts;
    if (domain === '*') return '<all_urls>';
    if (!domains.includes(domain)) domains.push(domain);
  }
  return domains;
}