/*
# 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/.
*/
/*
 original:
   http://github.com/piroor/webextensions-lib-tab-favicon-helper
*/
'use strict';

const TabFavIconHelper = {
  LAST_EFFECTIVE_FAVICON: 'last-effective-favIcon',
  VALID_FAVICON_PATTERN: /^(about|app|chrome|data|file|ftp|https?|moz-extension|resource):/,
  DRAWABLE_FAVICON_PATTERN: /^(https?|moz-extension|resource):/,

  // original: chrome://browser/content/aboutlogins/icons/favicon.svg
  FAVICON_LOCKWISE: `
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32">
  <defs>
    <linearGradient id="b" x1="24.684" y1="5.756" x2="6.966" y2="26.663" gradientUnits="userSpaceOnUse">
      <stop offset="0" stop-color="#ff9640"/>
      <stop offset=".6" stop-color="#fc4055"/>
      <stop offset="1" stop-color="#e31587"/>
    </linearGradient>
    <linearGradient id="c" x1="26.362" y1="4.459" x2="10.705" y2="21.897" gradientUnits="userSpaceOnUse">
      <stop offset="0" stop-color="#fff36e" stop-opacity=".8"/>
      <stop offset=".094" stop-color="#fff36e" stop-opacity=".699"/>
      <stop offset=".752" stop-color="#fff36e" stop-opacity="0"/>
    </linearGradient>
    <linearGradient id="d" x1="7.175" y1="27.275" x2="23.418" y2="11.454" gradientUnits="userSpaceOnUse">
      <stop offset="0" stop-color="#0090ed"/>
      <stop offset=".386" stop-color="#5b6df8"/>
      <stop offset=".629" stop-color="#9059ff"/>
      <stop offset="1" stop-color="#b833e1"/>
    </linearGradient>
    <linearGradient id="a" x1="29.104" y1="15.901" x2="26.135" y2="21.045" gradientUnits="userSpaceOnUse">
      <stop offset="0" stop-color="#722291" stop-opacity=".5"/>
      <stop offset=".5" stop-color="#722291" stop-opacity="0"/>
    </linearGradient>
    <linearGradient id="e" x1="20.659" y1="21.192" x2="13.347" y2="28.399" xlink:href="#a"/>
    <linearGradient id="f" x1="2.97" y1="19.36" x2="10.224" y2="21.105" gradientUnits="userSpaceOnUse">
      <stop offset="0" stop-color="#054096" stop-opacity=".5"/>
      <stop offset=".054" stop-color="#0f3d9c" stop-opacity=".441"/>
      <stop offset=".261" stop-color="#2f35b1" stop-opacity=".249"/>
      <stop offset=".466" stop-color="#462fbf" stop-opacity=".111"/>
      <stop offset=".669" stop-color="#542bc8" stop-opacity=".028"/>
      <stop offset=".864" stop-color="#592acb" stop-opacity="0"/>
    </linearGradient>
  </defs>
  <path d="M15.986 31.076A5.635 5.635 0 0 1 12.3 29.76 103.855 103.855 0 0 1 2.249 19.7a5.841 5.841 0 0 1-.006-7.4A103.792 103.792 0 0 1 12.3 2.247a5.837 5.837 0 0 1 7.4-.006A104.1 104.1 0 0 1 29.751 12.3a5.842 5.842 0 0 1 .006 7.405c-.423.484-.584.661-.917 1.025l-.234.255a1.749 1.749 0 1 1-2.585-2.357l.234-.258c.314-.344.467-.511.86-.961a2.352 2.352 0 0 0-.008-2.814 100.308 100.308 0 0 0-9.7-9.714 2.352 2.352 0 0 0-2.814.007 100.323 100.323 0 0 0-9.7 9.7 2.354 2.354 0 0 0 .006 2.815 100.375 100.375 0 0 0 9.7 9.708 2.352 2.352 0 0 0 2.815-.006c1.311-1.145 2.326-2.031 3.434-3.086l-3.4-3.609a2.834 2.834 0 0 1-.776-2.008 2.675 2.675 0 0 1 .834-1.9l.194-.184a2.493 2.493 0 0 0 1.124-2.333 2.81 2.81 0 0 0-5.6 0 2.559 2.559 0 0 0 1.092 2.279l.127.118a2.735 2.735 0 0 1 .324 3.7 1.846 1.846 0 0 1-.253.262l-1.578 1.326a1.75 1.75 0 0 1-2.252-2.68l.755-.634a5.758 5.758 0 0 1-1.715-4.366 6.305 6.305 0 0 1 12.6 0 5.642 5.642 0 0 1-1.854 4.528l3.84 4.082a2.071 2.071 0 0 1 .59 1.446 2.128 2.128 0 0 1-.628 1.526c-1.6 1.592-2.895 2.72-4.533 4.15a5.745 5.745 0 0 1-3.753 1.354z" fill="url(#b)"/>
  <path d="M15.986 31.076A5.635 5.635 0 0 1 12.3 29.76 103.855 103.855 0 0 1 2.249 19.7a5.841 5.841 0 0 1-.006-7.4A103.792 103.792 0 0 1 12.3 2.247a5.837 5.837 0 0 1 7.4-.006A104.1 104.1 0 0 1 29.751 12.3a5.842 5.842 0 0 1 .006 7.405c-.423.484-.584.661-.917 1.025l-.234.255a1.749 1.749 0 1 1-2.585-2.357l.234-.258c.314-.344.467-.511.86-.961a2.352 2.352 0 0 0-.008-2.814 100.308 100.308 0 0 0-9.7-9.714 2.352 2.352 0 0 0-2.814.007 100.323 100.323 0 0 0-9.7 9.7 2.354 2.354 0 0 0 .006 2.815 100.375 100.375 0 0 0 9.7 9.708 2.352 2.352 0 0 0 2.815-.006c1.311-1.145 2.326-2.031 3.434-3.086l-3.4-3.609a2.834 2.834 0 0 1-.776-2.008 2.675 2.675 0 0 1 .834-1.9l.194-.184a2.493 2.493 0 0 0 1.124-2.333 2.81 2.81 0 0 0-5.6 0 2.559 2.559 0 0 0 1.092 2.279l.127.118a2.735 2.735 0 0 1 .324 3.7 1.846 1.846 0 0 1-.253.262l-1.578 1.326a1.75 1.75 0 0 1-2.252-2.68l.755-.634a5.758 5.758 0 0 1-1.715-4.366 6.305 6.305 0 0 1 12.6 0 5.642 5.642 0 0 1-1.854 4.528l3.84 4.082a2.071 2.071 0 0 1 .59 1.446 2.128 2.128 0 0 1-.628 1.526c-1.6 1.592-2.895 2.72-4.533 4.15a5.745 5.745 0 0 1-3.753 1.354z" fill="url(#c)"/>
  <path d="M29.58 17.75a34.267 34.267 0 0 0-2.473-3.15 2.352 2.352 0 0 1 .008 2.814c-.393.45-.546.617-.86.961l-.234.258a1.749 1.749 0 1 0 2.585 2.357l.234-.255c.231-.253.379-.415.59-.653a2.161 2.161 0 0 0 .15-2.332zm-8.734 6.275c-1.108 1.055-2.123 1.941-3.434 3.086a2.352 2.352 0 0 1-2.815.006 100.375 100.375 0 0 1-9.7-9.708 2.354 2.354 0 0 1-.006-2.815s-2.131 1.984-2.4 3.424a2.956 2.956 0 0 0 .724 2.782 103.772 103.772 0 0 0 9.085 8.96 5.635 5.635 0 0 0 3.683 1.316 5.745 5.745 0 0 0 3.753-1.351c.926-.808 1.741-1.52 2.565-2.273a1.558 1.558 0 0 0 0-1.476 11.55 11.55 0 0 0-1.455-1.951z" fill="url(#d)"/>
  <path d="M29.43 20.079c-.211.238-.359.4-.59.653l-.234.255a1.749 1.749 0 1 1-2.585-2.357l.234-.258c.314-.344.467-.511.86-.961a2.352 2.352 0 0 0-.008-2.814 34.267 34.267 0 0 1 2.473 3.153 2.161 2.161 0 0 1-.15 2.329z" fill="url(#a)"/>
  <path d="M22.3 25.976a11.55 11.55 0 0 0-1.458-1.951c-1.108 1.055-2.123 1.941-3.434 3.086a2.352 2.352 0 0 1-2.815.006c-1.414-1.234-2.768-2.5-4.095-3.8L8.023 25.8c1.384 1.35 2.8 2.668 4.28 3.958a5.635 5.635 0 0 0 3.683 1.316 5.745 5.745 0 0 0 3.753-1.351c.926-.808 1.741-1.52 2.565-2.273a1.558 1.558 0 0 0-.004-1.474z" fill="url(#e)"/>
  <path d="M4.892 17.409a2.354 2.354 0 0 1-.006-2.815s-2.131 1.984-2.4 3.424a2.956 2.956 0 0 0 .729 2.782c1.56 1.739 3.158 3.4 4.808 5.007l2.479-2.48c-1.935-1.891-3.802-3.844-5.61-5.918z" opacity=".9" fill="url(#f)"/>
</svg>
`,
  // original: chrome://browser/content/robot.ico
  FAVICON_ROBOT: `
data:image/x-icon;base64,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
`.trim(),
  // original: chrome://browser/skin/controlcenter/dashboard.svg
  FAVICON_DASHBOARD: `
<svg data-name="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
  <path fill="context-fill" fill-opacity="context-fill-opacity" d="M15 12H4a2 2 0 0 1-2-2V3a1 1 0 0 0-2 0v7a4 4 0 0 0 4 4h11a1 1 0 0 0 0-2z"/>
  <path fill="context-fill" fill-opacity="context-fill-opacity" d="M4 11a1 1 0 0 0 1-1V8a1 1 0 0 0-2 0v2a1 1 0 0 0 1 1zM7 11a1 1 0 0 0 1-1V4a1 1 0 0 0-2 0v6a1 1 0 0 0 1 1zM10 11a1 1 0 0 0 1-1V6a1 1 0 0 0-2 0v4a1 1 0 0 0 1 1zM13 11a1 1 0 0 0 1-1V7a1 1 0 0 0-2 0v3a1 1 0 0 0 1 1z"/>
</svg>
`,
  // original: chrome://browser/skin/developer.svg
  FAVICON_DEVELOPER: `
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
  <path fill="context-fill" fill-opacity="context-fill-opacity" d="M14.555 3.2l-2.434 2.436a1.243 1.243 0 1 1-1.757-1.757L12.8 1.445A3.956 3.956 0 0 0 11 1a3.976 3.976 0 0 0-3.434 6.02l-6.273 6.273a1 1 0 1 0 1.414 1.414L8.98 8.434A3.96 3.96 0 0 0 11 9a4 4 0 0 0 4-4 3.956 3.956 0 0 0-.445-1.8z"/>
</svg>
`,
  // original: chrome://browser/skin/privatebrowsing/favicon.svg
  FAVICON_PRIVATE_BROWSING: `
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
  <circle cx="8" cy="8" r="8" fill="#8d20ae"/>
  <circle cx="8" cy="8" r="7.5" stroke="#7b149a" stroke-width="1" fill="none"/>
  <path d="M11.309,10.995C10.061,10.995,9.2,9.5,8,9.5s-2.135,1.5-3.309,1.5c-1.541,0-2.678-1.455-2.7-3.948C1.983,5.5,2.446,5.005,4.446,5.005S7.031,5.822,8,5.822s1.555-.817,3.555-0.817S14.017,5.5,14.006,7.047C13.987,9.54,12.85,10.995,11.309,10.995ZM5.426,6.911a1.739,1.739,0,0,0-1.716.953A2.049,2.049,0,0,0,5.3,8.544c0.788,0,1.716-.288,1.716-0.544A1.428,1.428,0,0,0,5.426,6.911Zm5.148,0A1.429,1.429,0,0,0,8.981,8c0,0.257.928,0.544,1.716,0.544a2.049,2.049,0,0,0,1.593-.681A1.739,1.739,0,0,0,10.574,6.911Z" stroke="#670c83" stroke-width="2" fill="none"/>
  <path d="M11.309,10.995C10.061,10.995,9.2,9.5,8,9.5s-2.135,1.5-3.309,1.5c-1.541,0-2.678-1.455-2.7-3.948C1.983,5.5,2.446,5.005,4.446,5.005S7.031,5.822,8,5.822s1.555-.817,3.555-0.817S14.017,5.5,14.006,7.047C13.987,9.54,12.85,10.995,11.309,10.995ZM5.426,6.911a1.739,1.739,0,0,0-1.716.953A2.049,2.049,0,0,0,5.3,8.544c0.788,0,1.716-.288,1.716-0.544A1.428,1.428,0,0,0,5.426,6.911Zm5.148,0A1.429,1.429,0,0,0,8.981,8c0,0.257.928,0.544,1.716,0.544a2.049,2.049,0,0,0,1.593-.681A1.739,1.739,0,0,0,10.574,6.911Z" fill="#fff"/>
</svg>
`,
  // original: chrome://browser/skin/settings.svg
  FAVICON_SETTINGS: `
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
  <path fill="context-fill" fill-opacity="context-fill-opacity" d="M15 7h-2.1a4.967 4.967 0 0 0-.732-1.753l1.49-1.49a1 1 0 0 0-1.414-1.414l-1.49 1.49A4.968 4.968 0 0 0 9 3.1V1a1 1 0 0 0-2 0v2.1a4.968 4.968 0 0 0-1.753.732l-1.49-1.49a1 1 0 0 0-1.414 1.415l1.49 1.49A4.967 4.967 0 0 0 3.1 7H1a1 1 0 0 0 0 2h2.1a4.968 4.968 0 0 0 .737 1.763c-.014.013-.032.017-.045.03l-1.45 1.45a1 1 0 1 0 1.414 1.414l1.45-1.45c.013-.013.018-.031.03-.045A4.968 4.968 0 0 0 7 12.9V15a1 1 0 0 0 2 0v-2.1a4.968 4.968 0 0 0 1.753-.732l1.49 1.49a1 1 0 0 0 1.414-1.414l-1.49-1.49A4.967 4.967 0 0 0 12.9 9H15a1 1 0 0 0 0-2zM5 8a3 3 0 1 1 3 3 3 3 0 0 1-3-3z"/>
</svg>
`,
  // original: chrome://browser/skin/window.svg
  FAVICON_WINDOW: `
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
  <path fill="context-fill" fill-opacity="context-fill-opacity" d="M13 1H3a3 3 0 0 0-3 3v8a3 3 0 0 0 3 3h11a2 2 0 0 0 2-2V4a3 3 0 0 0-3-3zm1 11a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V6h12zm0-7H2V4a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1z"/>
</svg>
`,
  // original: chrome://devtools/skin/images/profiler-stopwatch.svg
  FAVICON_PROFILER: `
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
  <path fill="context-fill" fill-opacity="context-fill-opacity" d="M10.85 6.85a.5.5 0 0 0-.7-.7l-2.5 2.5.7.7 2.5-2.5zM8 10a1 1 0 1 0 0-2 1 1 0 0 0 0 2zM5 1a1 1 0 0 1 1-1h4a1 1 0 1 1 0 2H6a1 1 0 0 1-1-1zM8 4a5 5 0 1 0 0 10A5 5 0 0 0 8 4zM1 9a7 7 0 1 1 14 0A7 7 0 0 1 1 9z"/>
</svg>
`,
  // original: chrome://global/skin/icons/performance.svg
  FAVICON_PERFORMANCE: `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
  <path fill="context-fill" d="M8 1a8 8 0 0 0-8 8 7.89 7.89 0 0 0 .78 3.43 1 1 0 0 0 .9.57.94.94 0 0 0 .43-.1 1 1 0 0 0 .47-1.33A6.07 6.07 0 0 1 2 9a6 6 0 0 1 12 0 5.93 5.93 0 0 1-.59 2.57 1 1 0 0 0 1.81.86A7.89 7.89 0 0 0 16 9a8 8 0 0 0-8-8z"/>
  <path fill="context-fill" d="M11.77 7.08a.5.5 0 0 0-.69.15L8.62 11.1A2.12 2.12 0 0 0 8 11a2 2 0 0 0 0 4 2.05 2.05 0 0 0 1.12-.34 2.31 2.31 0 0 0 .54-.54 2 2 0 0 0 0-2.24 2.31 2.31 0 0 0-.2-.24l2.46-3.87a.5.5 0 0 0-.15-.69z"/>
</svg>
`,
  // original: chrome://global/skin/icons/warning.svg
  FAVICON_WARNING: `
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
  <path fill="context-fill" fill-opacity="context-fill-opacity" d="M14.742 12.106L9.789 2.2a2 2 0 0 0-3.578 0l-4.953 9.91A2 2 0 0 0 3.047 15h9.905a2 2 0 0 0 1.79-2.894zM7 5a1 1 0 0 1 2 0v4a1 1 0 0 1-2 0zm1 8.25A1.25 1.25 0 1 1 9.25 12 1.25 1.25 0 0 1 8 13.25z"/>
</svg>
`,
  // original: chrome://mozapps/skin/extensions/extensionGeneric-16.svg
  FAVICON_EXTENSION: `
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="context-fill" fill-opacity="context-fill-opacity">
  <path d="M14.5 8c-.971 0-1 1-1.75 1a.765.765 0 0 1-.75-.75V5a1 1 0 0 0-1-1H7.75A.765.765 0 0 1 7 3.25c0-.75 1-.779 1-1.75C8 .635 7.1 0 6 0S4 .635 4 1.5c0 .971 1 1 1 1.75a.765.765 0 0 1-.75.75H1a1 1 0 0 0-1 1v2.25A.765.765 0 0 0 .75 8c.75 0 .779-1 1.75-1C3.365 7 4 7.9 4 9s-.635 2-1.5 2c-.971 0-1-1-1.75-1a.765.765 0 0 0-.75.75V15a1 1 0 0 0 1 1h3.25a.765.765 0 0 0 .75-.75c0-.75-1-.779-1-1.75 0-.865.9-1.5 2-1.5s2 .635 2 1.5c0 .971-1 1-1 1.75a.765.765 0 0 0 .75.75H11a1 1 0 0 0 1-1v-3.25a.765.765 0 0 1 .75-.75c.75 0 .779 1 1.75 1 .865 0 1.5-.9 1.5-2s-.635-2-1.5-2z"/>
</svg>
`,
  // original: globe-16.svg
  FAVICON_GLOBE: `
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
  <path fill="context-fill" d="M8 0a8 8 0 1 0 8 8 8.009 8.009 0 0 0-8-8zm5.163 4.958h-1.552a7.7 7.7 0 0 0-1.051-2.376 6.03 6.03 0 0 1 2.603 2.376zM14 8a5.963 5.963 0 0 1-.335 1.958h-1.821A12.327 12.327 0 0 0 12 8a12.327 12.327 0 0 0-.156-1.958h1.821A5.963 5.963 0 0 1 14 8zm-6 6c-1.075 0-2.037-1.2-2.567-2.958h5.135C10.037 12.8 9.075 14 8 14zM5.174 9.958a11.084 11.084 0 0 1 0-3.916h5.651A11.114 11.114 0 0 1 11 8a11.114 11.114 0 0 1-.174 1.958zM2 8a5.963 5.963 0 0 1 .335-1.958h1.821a12.361 12.361 0 0 0 0 3.916H2.335A5.963 5.963 0 0 1 2 8zm6-6c1.075 0 2.037 1.2 2.567 2.958H5.433C5.963 3.2 6.925 2 8 2zm-2.56.582a7.7 7.7 0 0 0-1.051 2.376H2.837A6.03 6.03 0 0 1 5.44 2.582zm-2.6 8.46h1.549a7.7 7.7 0 0 0 1.051 2.376 6.03 6.03 0 0 1-2.603-2.376zm7.723 2.376a7.7 7.7 0 0 0 1.051-2.376h1.552a6.03 6.03 0 0 1-2.606 2.376z"/>
</svg>
`,

  async _urlToKey(url) { // sha1 hash
    const encoder = new TextEncoder();
    const data = encoder.encode(url);

    const hashBuffer = await crypto.subtle.digest('SHA-1', data);
    const hashArray = Array.from(new Uint8Array(hashBuffer));
    const hashHex = hashArray.map((byte) => byte.toString(16).padStart(2, '0')).join('');

    return hashHex;
  },

  DB_NAME: 'TabFavIconHelper',
  DB_VERSION: 2,
  STORE_FAVICONS: 'favIcons',
  STORE_EFFECTIVE_FAVICONS: 'effectiveFavIcons',
  STORE_UNEFFECTIVE_FAVICONS: 'uneffectiveFavIcons',
  EXPIRATION_TIME_IN_MSEC: 7 * 24 * 60 * 60 * 1000, // 7 days

  async _openDB() {
    if (this._openedDB)
      return this._openedDB;
    return new Promise((resolve, _reject) => {
      const request = indexedDB.open(this.DB_NAME, this.DB_VERSION);

      request.onerror = () => {
        // This can fail if this is in a private window.
        // See: https://github.com/piroor/treestyletab/issues/3387
        //reject(new Error('Failed to open database'));
        resolve(null);
      };

      request.onsuccess = () => {
        const db = request.result;
        this._openedDB = db;
        resolve(db);
      };

      request.onupgradeneeded = (event) => {
        const db = event.target.result;
        const objectStores = db.objectStoreNames;

        const needToUpgrade = event.oldVersion < this.DB_VERSION;
        if (needToUpgrade) {
          if (objectStores.contains(this.STORE_FAVICONS))
            db.deleteObjectStore(this.STORE_FAVICONS);
          if (objectStores.contains(this.STORE_EFFECTIVE_FAVICONS))
            db.deleteObjectStore(this.STORE_EFFECTIVE_FAVICONS);
          if (objectStores.contains(this.STORE_UNEFFECTIVE_FAVICONS))
            db.deleteObjectStore(this.STORE_UNEFFECTIVE_FAVICONS);
        }

        if (needToUpgrade ||
            !objectStores.contains(this.STORE_FAVICONS)) {
          const favIconsStore = db.createObjectStore(this.STORE_FAVICONS, { keyPath: 'key', unique: true });
          favIconsStore.createIndex('urlKey', 'urlKey', { unique: false });
          favIconsStore.createIndex('timestamp', 'timestamp');
        }

        if (needToUpgrade ||
            !objectStores.contains(this.STORE_EFFECTIVE_FAVICONS)) {
          const effectiveFavIconsStore = db.createObjectStore(this.STORE_EFFECTIVE_FAVICONS, { keyPath: 'urlKey', unique: true });
          effectiveFavIconsStore.createIndex('timestamp', 'timestamp');
          effectiveFavIconsStore.createIndex('favIconKey', 'favIconKey', { unique: false });
        }

        if (needToUpgrade ||
            !objectStores.contains(this.STORE_UNEFFECTIVE_FAVICONS)) {
          const uneffectiveFavIconsStore = db.createObjectStore(this.STORE_UNEFFECTIVE_FAVICONS, { keyPath: 'urlKey', unique: true });
          uneffectiveFavIconsStore.createIndex('timestamp', 'timestamp');
          uneffectiveFavIconsStore.createIndex('favIconKey', 'favIconKey', { unique: false });
        }
      };
    });
  },

  async _associateFavIconUrlToTabUrl({ favIconUrl, tabUrl, store } = {}) {
    const [db, tabUrlKey, favIconKey] = await Promise.all([
      this._openDB(),
      this._urlToKey(tabUrl),
      this._urlToKey(favIconUrl),
    ]);
    if (!db)
      return;

    try {
      const transaction = db.transaction([store, this.STORE_FAVICONS], 'readwrite');
      const associationStore = transaction.objectStore(store);
      const favIconStore = transaction.objectStore(this.STORE_FAVICONS);
      const timestamp = Date.now();

      const associationRequest = associationStore.put({ urlKey: tabUrlKey, favIconKey, timestamp });
      const favIconRequest = favIconStore.put({ key: favIconKey, url: favIconUrl, timestamp });

      transaction.oncomplete = () => {
        //db.close();
        this._reserveToExpireOldEntries();
        favIconUrl = undefined;
        tabUrl = undefined;
        store = undefined;
      };

      associationRequest.onerror = event => {
        console.error(`Failed to associate favIconUrl ${favIconUrl} to tabUrl ${tabUrl} in the store ${store}`, event);
      };

      favIconRequest.onerror = event => {
        console.error(`Failed to store favIconUrl ${favIconUrl} to tabUrl ${tabUrl} in the store ${store}`, event);
      };
    }
    catch(error) {
      console.error(`Failed to associate favIconUrl ${favIconUrl} to tabUrl ${tabUrl} in the store ${store}`, error);
    }
  },

  async _unassociateFavIconUrlFromTabUrl({ tabUrl, store } = {}) {
    const [db, tabUrlKey] = await Promise.all([
      this._openDB(),
      this._urlToKey(tabUrl),
    ]);
    if (!db)
      return;

    try {
      const transaction = db.transaction([store], 'readwrite');
      const associationStore = transaction.objectStore(store);
      const unassociationRequest = associationStore.delete(tabUrlKey);

      transaction.oncomplete = () => {
        //db.close();
        this._reserveToExpireOldEntries();
        tabUrl = undefined;
        store = undefined;
      };

      unassociationRequest.onerror = event => {
        console.error(`Failed to unassociate favIconUrl from tabUrl ${tabUrl} in the store ${store}`, event);
      };
    }
    catch(error) {
      console.error(`Failed to unassociate favIconUrl from tabUrl ${tabUrl} in the store ${store}`, error);
    }
  },

  async _getAssociatedFavIconUrlFromTabUrl({ tabUrl, store } = {}) {
    return new Promise(async (resolve, _reject) => {
      const [db, tabUrlKey] = await Promise.all([
        this._openDB(),
        this._urlToKey(tabUrl),
      ]);
      if (!db) {
        resolve(null);
        return;
      }

      try {
        const transaction = db.transaction([store, this.STORE_FAVICONS], 'readonly');
        const associationStore = transaction.objectStore(store);
        const favIconStore = transaction.objectStore(this.STORE_FAVICONS);

        const associationRequest = associationStore.get(tabUrlKey);

        associationRequest.onsuccess = () => {
          const association = associationRequest.result;
          if (!association) {
            //console.log(`No associated favIconUrl for the tabUrl ${tabUrl} in the store ${store}`);
            resolve(null);
            return;
          }

          const favIconRequest = favIconStore.get(association.favIconKey);

          favIconRequest.onsuccess = () => {
            let favIcon = favIconRequest.result;
            if (!favIcon) {
              //console.log(`FavIcon data not found for the tabUrl ${tabUrl} in the store ${store}`);
              resolve(null);
              return;
            }
            if (favIcon.timestamp < Date.now() - this.EXPIRATION_TIME_IN_MSEC) {
              //console.log(`FavIcon data is expired for the tabUrl ${tabUrl} in the store ${store}`);
              this._reserveToExpireOldEntries();
              resolve(null);
              return;
            }
            resolve(favIcon.url);
            favIcon.url = undefined;
            favIcon = undefined;
          };

          favIconRequest.onerror = event => {
            console.error(`Failed to get favIconUrl from tabUrl ${tabUrl}`, event);
            resolve(null);
          };
        };

        associationRequest.onerror = event => {
          console.error(`Failed to get favIcon association from tabUrl ${tabUrl}`, event);
          resolve(null);
        };

        transaction.oncomplete = () => {
          //db.close();
          tabUrl = undefined;
          store = undefined;
        };
      }
      catch(error) {
        console.error('Failed to get from cache:', error);
        resolve(null);
      }
    });
  },

  async _reserveToExpireOldEntries() {
    if (this._reservedExpiration)
      clearTimeout(this._reservedExpiration);
    this._reservedExpiration = setTimeout(() => {
      this._reservedExpiration = null;
      this._expireOldEntries();
    }, 500);
  },
  async _expireOldEntries() {
    return new Promise(async (resolve, reject) => {
      const db = await this._openDB();
      if (!db) {
        resolve();
        return;
      }

      try {
        const transaction = db.transaction([this.STORE_FAVICONS, this.STORE_EFFECTIVE_FAVICONS, this.STORE_UNEFFECTIVE_FAVICONS], 'readwrite');
        const favIconsStore = transaction.objectStore(this.STORE_FAVICONS);
        const effectiveFavIconsStore = transaction.objectStore(this.STORE_EFFECTIVE_FAVICONS);
        const uneffectiveFavIconsStore = transaction.objectStore(this.STORE_UNEFFECTIVE_FAVICONS);

        const favIconIndex = favIconsStore.index('timestamp');
        const effectiveFavIconIndex = effectiveFavIconsStore.index('timestamp');
        const uneffectiveFavIconIndex = uneffectiveFavIconsStore.index('timestamp');

        const expirationTimestamp = Date.now() - this.EXPIRATION_TIME_IN_MSEC;

        const favIconRequest = favIconIndex.openCursor(IDBKeyRange.upperBound(expirationTimestamp));
        favIconRequest.onsuccess = (event) => {
          const cursor = event.target.result;
          if (!cursor)
            return;
          const key = cursor.primaryKey;
          cursor.continue();
          const deleteRequest = favIconsStore.delete(key);
          deleteRequest.onerror = event => {
            console.error(`Failed to clear favicon index`, event);
          };
        };
        favIconRequest.onerror = event => {
          console.error(`Failed to retrieve favicon index`, event);
        };

        const effectiveFavIconRequest = effectiveFavIconIndex.openCursor(IDBKeyRange.upperBound(expirationTimestamp));
        effectiveFavIconRequest.onsuccess = (event) => {
          const cursor = event.target.result;
          if (!cursor)
            return;
          const url = cursor.primaryKey;
          cursor.continue();
          const deleteRequest = effectiveFavIconsStore.delete(url);
          deleteRequest.onerror = event => {
            console.error(`Failed to clear effective favicon index`, event);
          };
        };
        effectiveFavIconRequest.onerror = event => {
          console.error(`Failed to retrieve effective favicon index`, event);
        };

        const uneffectiveFavIconRequest = uneffectiveFavIconIndex.openCursor(IDBKeyRange.upperBound(expirationTimestamp));
        uneffectiveFavIconRequest.onsuccess = (event) => {
          const cursor = event.target.result;
          if (!cursor)
            return;
          const url = cursor.primaryKey;
          cursor.continue();
          const deleteRequest = uneffectiveFavIconsStore.delete(url);
          deleteRequest.onerror = event => {
            console.error(`Failed to clear uneffective favicon index`, event);
          };
        };
        uneffectiveFavIconRequest.onerror = event => {
          console.error(`Failed to retrieve uneffective favicon index`, event);
        };

        transaction.oncomplete = () => {
          //db.close();
          resolve();
        };
      }
      catch(error) {
        console.error('Failed to expire old entries:', error);
        reject(error);
      }
    });
  },

  _tasks: [],
  _processStep: 5,
  FAVICON_SIZE: 16,

  _init() {
    this._onTabUpdated = this._onTabUpdated.bind(this);
    browser.tabs.onUpdated.addListener(this._onTabUpdated);

    this.canvas = document.createElement('canvas');
    this.canvas.width = this.canvas.height = this.FAVICON_SIZE;
    this.canvas.setAttribute('style', `
      visibility: hidden;
      pointer-events: none;
      position: fixed
    `);
    document.body.appendChild(this.canvas);

    window.addEventListener('unload', () => {
      browser.tabs.onUpdated.removeListener(this._onTabUpdated);
    }, { once: true });
  },

  _sessionAPIAvailable: (
    browser.sessions &&
    browser.sessions.getTabValue &&
    browser.sessions.setTabValue &&
    browser.sessions.removeTabValue
  ),

  _addTask(task) {
    this._tasks.push(task);
    this._run();
  },

  _run() {
    if (this._running)
      return;
    this._running = true;
    const processOneTask = () => {
      if (this._tasks.length == 0) {
        this._running = false;
      }
      else {
        const tasks = this._tasks.splice(0, this._processStep);
        while (tasks.length > 0) {
          tasks.shift()();
        }
        window.requestAnimationFrame(processOneTask);
      }
    };
    processOneTask();
  },

  // public
  loadToImage(params = {}) {
    this._addTask(() => {
      this._getEffectiveFavIconURL(params.tab, params.url)
        .then(url => {
          params.image.src = url;
          params.image.classList.remove('error');
          url = undefined;
        },
              _error => {
                params.image.src = '';
                params.image.classList.add('error');
              });
    });
  },

  // public
  maybeImageTab(_tab) { // for backward compatibility
    return false;
  },

  _getSafeFaviconUrl(url) {
    switch (url) {
      case 'chrome://browser/content/aboutlogins/icons/favicon.svg':
        return this._getSVGDataURI(this.FAVICON_LOCKWISE);
      case 'chrome://browser/content/robot.ico':
        return this.FAVICON_ROBOT;
      case 'chrome://browser/skin/controlcenter/dashboard.svg':
        return this._getSVGDataURI(this.FAVICON_DASHBOARD);
      case 'chrome://browser/skin/developer.svg':
        return this._getSVGDataURI(this.FAVICON_DEVELOPER);
      case 'chrome://browser/skin/privatebrowsing/favicon.svg':
        return this._getSVGDataURI(this.FAVICON_PRIVATE_BROWSING);
      case 'chrome://browser/skin/settings.svg':
        return this._getSVGDataURI(this.FAVICON_SETTINGS);
      case 'chrome://browser/skin/window.svg':
        return this._getSVGDataURI(this.FAVICON_WINDOW);
      case 'chrome://devtools/skin/images/profiler-stopwatch.svg':
        return this._getSVGDataURI(this.FAVICON_PROFILER);
      case 'chrome://global/skin/icons/performance.svg':
        return this._getSVGDataURI(this.FAVICON_PERFORMANCE);
      case 'chrome://global/skin/icons/warning.svg':
        return this._getSVGDataURI(this.FAVICON_WARNING);
      case 'chrome://mozapps/skin/extensions/extensionGeneric-16.svg':
        return this._getSVGDataURI(this.FAVICON_EXTENSION);
      default:
        if (/^chrome:\/\//.test(url) &&
            !/^chrome:\/\/branding\//.test(url))
          return this._getSVGDataURI(this.FAVICON_GLOBE);
        break;
    }
    return url;
  },
  _getSVGDataURI(svg) {
    return `data:image/svg+xml,${encodeURIComponent(svg.trim())}`;
  },

  // public
  async getLastEffectiveFavIconURL(tab) {
    if (tab.favIconUrl?.startsWith('data:'))
      return tab.favIconUrl;

    const uneffectiveFavIconUrl = await this._getAssociatedFavIconUrlFromTabUrl({ tabUrl: tab.url, store: this.STORE_UNEFFECTIVE_FAVICONS });
    if (uneffectiveFavIconUrl)
      return null;

    const favIconUrl = await this._getAssociatedFavIconUrlFromTabUrl({ tabUrl: tab.url, store: this.STORE_EFFECTIVE_FAVICONS });
    if (favIconUrl)
      return favIconUrl;

    if (!this._sessionAPIAvailable)
      return null;

    const lastData = await browser.sessions.getTabValue(tab.id, this.LAST_EFFECTIVE_FAVICON);
    return lastData && lastData.url == tab.url && lastData.favIconUrl;
  },

  async _getEffectiveFavIconURL(tab, favIconUrl = null) {
    if (tab.favIconUrl?.startsWith('data:')) {
      browser.sessions.removeTabValue(tab.id, this.LAST_EFFECTIVE_FAVICON);
      this._unassociateFavIconUrlFromTabUrl({ tabUrl: tab.url, store: this.STORE_UNEFFECTIVE_FAVICONS });
      return tab.favIconUrl;
    }

    return new Promise(async (resolve, reject) => {
      favIconUrl = this._getSafeFaviconUrl(favIconUrl || tab.favIconUrl);
      let storedFavIconUrl;
      if (!favIconUrl && tab.discarded) {
        // discarded tab doesn't have favIconUrl, so we should use cached data.
        storedFavIconUrl = favIconUrl = await this.getLastEffectiveFavIconURL(tab);
      }

      let loader, onLoad, onError;
      const clear = (() => {
        if (loader) {
          loader.removeEventListener('load', onLoad, { once: true });
          loader.removeEventListener('error', onError, { once: true });
        }
        loader = onLoad = onError = favIconUrl = storedFavIconUrl = undefined;
      });

      onLoad = async foundFavIconUrl => {
        let dataURL = null;
        if (this.DRAWABLE_FAVICON_PATTERN.test(favIconUrl)) {
          const context = this.canvas.getContext('2d');
          context.clearRect(0, 0, this.FAVICON_SIZE, this.FAVICON_SIZE);
          context.drawImage(loader, 0, 0, this.FAVICON_SIZE, this.FAVICON_SIZE);
          try {
            dataURL = this.canvas.toDataURL('image/png');
          }
          catch(_error) {
            // it can fail due to security reasons
          }
        }
        const oldFavIconUrl = foundFavIconUrl || await this._getAssociatedFavIconUrlFromTabUrl({ tabUrl: tab.url, store: this.STORE_EFFECTIVE_FAVICONS });
        if (!oldFavIconUrl ||
            oldFavIconUrl != favIconUrl) {
          if (this._sessionAPIAvailable)
            browser.sessions.setTabValue(tab.id, this.LAST_EFFECTIVE_FAVICON, {
              url: tab.url,
              favIconUrl,
            });
        }
        this._associateFavIconUrlToTabUrl({ tabUrl: tab.url, favIconUrl, store: this.STORE_EFFECTIVE_FAVICONS });
        this._unassociateFavIconUrlFromTabUrl({ tabUrl: tab.url, store: this.STORE_UNEFFECTIVE_FAVICONS });
        resolve(dataURL || favIconUrl);
        clear();
      };
      onError = async error => {
        this._unassociateFavIconUrlFromTabUrl({ tabUrl: tab.url, store: this.STORE_EFFECTIVE_FAVICONS });
        this._associateFavIconUrlToTabUrl({ tabUrl: tab.url, favIconUrl, store: this.STORE_UNEFFECTIVE_FAVICONS });
        if (this._sessionAPIAvailable)
          browser.sessions.removeTabValue(tab.id, this.LAST_EFFECTIVE_FAVICON);
        clear();
        reject(error || new Error('No effective icon'));
      };
      storedFavIconUrl = storedFavIconUrl || await this._getAssociatedFavIconUrlFromTabUrl({ tabUrl: tab.url, store: this.STORE_EFFECTIVE_FAVICONS });
      if (storedFavIconUrl)
        return onLoad(storedFavIconUrl);
      if (!favIconUrl ||
          !this.VALID_FAVICON_PATTERN.test(favIconUrl)) {
        onError();
        return;
      }
      loader = new Image();
      if (/^https?:/.test(favIconUrl))
        loader.crossOrigin = 'anonymous';
      loader.addEventListener('load', () => onLoad(), { once: true });
      loader.addEventListener('error', onError, { once: true });
      try {
        loader.src = favIconUrl;
      }
      catch(error) {
        this._unassociateFavIconUrlFromTabUrl({ tabUrl: tab.url, store: this.STORE_EFFECTIVE_FAVICONS });
        this._associateFavIconUrlToTabUrl({ tabUrl: tab.url, favIconUrl, store: this.STORE_UNEFFECTIVE_FAVICONS });
        onError(error);
      }
    });
  },

  _onTabUpdated(tabId, changeInfo, _tab) {
    if (!this._hasFavIconInfo(changeInfo))
      return;
    let timer = this._updatingTabs.get(tabId);
    if (timer)
      clearTimeout(timer);
    // Updating of last effective favicon must be done after the loading
    // of the tab itself is correctly done, to avoid cookie problems on
    // some websites.
    // See also: https://github.com/piroor/treestyletab/issues/2064
    timer = setTimeout(async () => {
      this._updatingTabs.delete(tabId);
      const tab = await browser.tabs.get(tabId).catch(_error => null);
      if (!tab ||
          (changeInfo.favIconUrl &&
           tab.favIconUrl != changeInfo.favIconUrl) ||
          (changeInfo.url &&
           tab.url != changeInfo.url) ||
          !this._hasFavIconInfo(tab))
        return; // expired

      await this._getEffectiveFavIconURL(
        tab,
        changeInfo.favIconUrl
      ).catch(_error => {});
    }, 5000);
    this._updatingTabs.set(tabId, timer);
  },
  _hasFavIconInfo(tabOrChangeInfo) {
    return 'favIconUrl' in tabOrChangeInfo;
  },
  _updatingTabs: new Map(),
};
TabFavIconHelper._init(); // eslint-disable-line no-underscore-dangle
export default TabFavIconHelper;
