<!doctype html>
<html>
  <head>
    <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
    <title>{{title}}</title>

    {%- if google_font %}
    <link rel="preconnect" href="https://fonts.googleapis.com" />
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
    <link
      href="https://fonts.googleapis.com/css2?family={{google_font}}&display=swap"
      rel="stylesheet"
    />
    {{google_font_data}}
    {%- if google_tooltip_font %}
    <link
      href="https://fonts.googleapis.com/css2?family={{google_tooltip_font}}&display=swap"
      rel="stylesheet"
    />
    {%- endif -%} 
    {%- endif %}
    <link
      href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap-theme.min.css"
      rel="stylesheet"
    />
    <link
      href="https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css"
      rel="stylesheet"
    />

    {% for url in js_dependency_urls -%}
    <script src="{{ url }}"></script>
    {% endfor -%}

    {% for _, css_src in css_dependency_srcs.items() -%}
    <style>
      {{ css_src | safe }}
    </style>
    {%- endfor %}
    <style>
      body {
        margin: 0;
        padding: 0;
        overflow: hidden;
        background: {{page_background_color}};
        font-family: {{title_font_family}};
        color: {{title_font_color}};
      }
      .container-box {
        margin: 16px;
        padding: 12px;
        border-radius: 16px;
        line-height: 0.95;
        z-index: 2;
        background: {{title_background}};
        box-shadow: 2px 3px 10px {{shadow_color}};
      }
      .more-opaque {
        background-color: {{title_background[:-2] + "ee"}};
      }
      #deck-container {
        width: 100vw;
        height: 100vh;
      }
      #deck-container canvas {
        z-index: 1;
        background: {{page_background_color}};
      }
      .deck-tooltip {
          {{tooltip_css}}
      }
      input {
        margin: 2px;
        padding: 4px;
        border-radius: 8px;
        color: {{title_font_color}};
        background: {{input_background}};
        border: 1px solid {{input_border}};
        transition: 0.5s;
        outline: none;
      }
      input:focus {
        border: 2px solid #555;
      }
      #loading {
        width: 100%;
        height: 100%;
        top: 0px;
        left: 0px;
        position: absolute;
        display: block;
        z-index: 99
      }
      #loading-image {
        position: absolute;
        top: 45%;
        left: 47.5%;
        z-index: 100
      }
      #title-container {
        position: absolute;
        top: 0;
        left: 0;
      }

      {%- if logo %}
      #logo-container {
        position: absolute;
        bottom: 0;
        right: 0;
      }
      img {
        display: block;
        margin-left: auto;
        margin-right: auto;
      }
      {%- endif %}

      {%- if search %}
      #search-container{
        position: absolute;
        left: -16px;
        margin: 16px;
        width: fit-content;
      }
      {%- endif %}
      {%- if custom_css %}
      {{custom_css}}
      {%- endif %}
    </style>
  </head>
  <body>
    <div id="loading">
      <img
        id="loading-image"
        src="https://i.gifer.com/ZKZg.gif"
        alt="Loading..."
        width="5%"
      />
    </div>

    {% if use_title %}
    <div id="title-container" class="container-box">
      <span
        style="font-family:{{title_font_family}};font-size:{{title_font_size}}pt;color:{{title_font_color}}"
      >
        {{title}}
      </span>
      <br />
      <span
        style="font-family:{{title_font_family}};font-size:{{sub_title_font_size}}pt;color:{{sub_title_font_color}}"
      >
        {{sub_title}}
      </span>

      {% if search %}
      <div id="search-container" class="container-box">
        <input autocomplete="off" type="search" id="text-search" placeholder="🔍" />
      </div>
      {% endif %}
    </div>
    {% elif search %}
    <div id="search-container" class="container-box" style="left: 0 !important">
      <input autocomplete="off" type="search" id="text-search" placeholder="🔍" />
    </div>
    {% endif %} {% if logo %}
    <div id="logo-container" class="container-box">
      <img src="{{logo}}" style="width:{{logo_width}}px" />
    </div>
    {% endif %}

    <div id="deck-container"></div>

    {%- if enable_histogram %}
    <div id="d3histogram-container" class="container-box"></div>
    {%- endif -%}
    {%- if show_loading_progress %}
    <div id="progress-container" class="datamapplot-progress-container container-box">
      <div id="point-data-progress" class="datamapplot-progress-bar">
        <span class="datamapplot-progress-bar-fill" style="width: 0%;">
          <span class="datamapplot-progress-bar-text">Point Data: 0%</span>
        </span>
      </div>
      <div id="label-data-progress" class="datamapplot-progress-bar">
        <span class="datamapplot-progress-bar-fill" style="width: 0%;">
          <span class="datamapplot-progress-bar-text">Label Data: 0%</span>
        </span>
      </div>
      <div id="meta-data-progress" class="datamapplot-progress-bar">
        <span class="datamapplot-progress-bar-fill" style="width: 0%;">
          <span class="datamapplot-progress-bar-text">Meta Data: 0%</span>
        </span>
      </div>
      {%- if enable_histogram %}
      <div id="histogram-bin-data-progress" class="datamapplot-progress-bar">
        <span class="datamapplot-progress-bar-fill" style="width: 0%;">
          <span class="datamapplot-progress-bar-text">Histogram Bin Data: 0%</span>
        </span>
      </div>
      <div id="histogram-index-data-progress" class="datamapplot-progress-bar">
        <span class="datamapplot-progress-bar-fill" style="width: 0%;">
          <span class="datamapplot-progress-bar-text">Histogram Index Data: 0%</span>
        </span>
      </div>
      {%- endif -%}
    </div>
    {%- endif -%}

    {% if custom_html %} {{custom_html}} {%- endif -%}
  </body>
  {%- for _, js_src in js_dependency_srcs.items() %}
  <script>
    {{ js_src | safe }}
  </script>
  {%- endfor %}
  <script type="module">
    async function simpleArrowParser(arrow_bytes) {
      const table = await Arrow.tableFromIPC(arrow_bytes);
      const result = {};
      table.schema.fields.forEach((field) => {
        result[field.name] = table.getChild(field.name).toArray();
      });
      return result;
    }

    if (!("CompressionStream" in window)) {
      throw new Error(
        "Your browser doesn't support the Compression Streams API " +
          "https://developer.mozilla.org/docs/Web/API/Compression_Streams_API#browser_compatibility",
      );
    }

    {% if inline_data %}
    const pointDataEncoded = "{{base64_point_data}}";
    const hoverDataEncoded = "{{base64_hover_data}}";
    const labelDataEncoded = "{{base64_label_data}}";
    const histogramBinDataEncoded = "{{base64_histogram_bin_data}}";
    const histogramIndexDataEncoded = "{{base64_histogram_index_data}}";

    // Blob for the parsing worker
    const parsingWorkerBlob = new Blob([`
      self.onmessage = async function(event) {
      const { encodedData, JSONParse } = event.data;
        // Function to parse base64 to Uint8Array
        async function DecompressBytes(bytes) {
          const blob = new Blob([bytes]);
          const decompressedStream = blob.stream().pipeThrough(
            new DecompressionStream("gzip")
          );
          const arr = await new Response(decompressedStream).arrayBuffer()
          return new Uint8Array(arr);
        }
        {%- if show_loading_progress %}
        async function decodeBase64WithProgress(base64) {
          const totalLength = base64.length;
          const chunkSize = 1024 * 1024; // 1MB chunks
          let decodedArray = new Uint8Array(Math.ceil(totalLength * 3 / 4));
          let offset = 0;

          for (let i = 0; i < totalLength; i += chunkSize) {
            const chunk = base64.slice(i, i + chunkSize);
            const decodedChunk = Uint8Array.from(atob(chunk), c => c.charCodeAt(0));
            decodedArray.set(decodedChunk, offset);
            offset += decodedChunk.length;

            const progress = Math.min(100, Math.round((i + chunkSize) / totalLength * 100));
            self.postMessage({ type: 'progress', progress: progress });

            // Allow other operations to occur
            await new Promise(resolve => setTimeout(resolve, 0));
          }

          return decodedArray.slice(0, offset);
        }
        {%- else %}
        async function decodeBase64WithProgress(base64) {
            return Uint8Array.from(atob(base64), c => c.charCodeAt(0));
        }
        {%- endif %}
        const binaryData = await decodeBase64WithProgress(encodedData).then(buffer => DecompressBytes(buffer));

        if (JSONParse) {
          const parsedData = JSON.parse(new TextDecoder("utf-8").decode(binaryData));
          self.postMessage({ data: parsedData });
        } else {
          // Send the parsed table back to the main thread
          self.postMessage({ type: "data", data: binaryData });
        }
      }
    `], { type: 'application/javascript' });
    const workerUrl = URL.createObjectURL(parsingWorkerBlob);
    {% else %}
    const currentURL = self.location.pathname;
    const directoryPath = currentURL.substring(0, currentURL.lastIndexOf('/') + 1);
    const originURL = self.location.origin + directoryPath;

    const pointDataEncoded = `${originURL}/{{file_prefix}}_point_data.zip`;
    const hoverDataEncoded = `${originURL}/{{file_prefix}}_meta_data.zip`;
    const labelDataEncoded = `${originURL}/{{file_prefix}}_label_data.zip`;
    {% if enable_histogram %}
    const histogramBinDataEncoded = `${originURL}/{{file_prefix}}_histogram_bin_data.zip`;
    const histogramIndexDataEncoded = `${originURL}/{{file_prefix}}_histogram_index_data.zip`;
    {% endif %}

    // Blob for the parsing worker
    const parsingWorkerBlob = new Blob([`
      self.onmessage = async function(event) {
        const { encodedData, JSONParse } = event.data;
        async function decompressFile(filename) {
          try {
            const response = await fetch(filename);
            if (!response.ok) {
              throw new Error(\`HTTP error! status: \${response.status}\`);
            }
            const reader = response.body
              .pipeThrough(new DecompressionStream("gzip"))
              .getReader();

            let chunks = [];
            let totalSize = 0;

            while (true) {
              const { done, value } = await reader.read();
              if (done) {
                break;
              }
              chunks.push(value);
              totalSize += value.length;
            }

            // Concatenate chunks into a single Uint8Array
            const decompressedData = new Uint8Array(totalSize);
            let position = 0;
            for (const chunk of chunks) {
              decompressedData.set(chunk, position);
              position += chunk.length;
            }

            return decompressedData;
          } catch (error) {
            console.error('Decompression failed:', error);
            throw error;
          }
        }
        const binaryData = await decompressFile(encodedData);
        if (JSONParse) {
          const parsedData = JSON.parse(new TextDecoder("utf-8").decode(binaryData));
          self.postMessage({ type: "data", data: parsedData });
        } else {
          // Send the parsed table back to the main thread
          self.postMessage({ type: "data", data: binaryData });
        }
      }
    `], { type: 'application/javascript' });
    const workerUrl = URL.createObjectURL(parsingWorkerBlob);
    {% endif -%}

    const searchItemId = "text-search";
    const histogramItemId = "d3histogram-container";
    const selectionItemId = "lasso-select";
    const searchItem = document.getElementById(searchItemId);
    let histogramItem = null;
    
    const container = document.getElementById('deck-container');

    const labelDataWorker = new Worker(workerUrl);
    const pointDataWorker = new Worker(workerUrl);
    const metaDataWorker = new Worker(workerUrl);
    {% if enable_histogram %}
    const histogramBinDataWorker = new Worker(workerUrl);
    const histogramIndexDataWorker = new Worker(workerUrl);
    {% endif -%}

    const datamap = new DataMap({
      container: container,
      bounds: {{data_bounds}},
      searchItemId: searchItemId,
      lassoSelectionItemId: selectionItemId,      
    });

    function updateProgressBar(id, progress) {
      const progressBar = document.querySelector(`#${id} .datamapplot-progress-bar-fill`);
      const progressText = document.querySelector(`#${id} .datamapplot-progress-bar-text`);
      progressBar.style.width = `${progress}%`;
      progressText.textContent = `${id.replace('-progress', '').replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}: ${progress}%`;
    }

    function checkAllDataLoaded() {
      const progressBars = document.querySelectorAll('.datamapplot-progress-bar-fill');
      const allLoaded = Array.from(progressBars).every(bar => bar.style.width === '100%');
      if (allLoaded) {
        document.getElementById("loading").style.display = "none";
        document.getElementById("progress-container").style.display = "none";
      }
    }

    function loadPointDataLayer() {
      pointDataWorker.postMessage({encodedData: pointDataEncoded, JSONParse: false});

      pointDataWorker.onmessage = async function(event) {
        if (event.data.type === "progress") {
          updateProgressBar('point-data-progress', event.data.progress);
        } else {
          const { data } = event.data;
          const pointData = await simpleArrowParser(data);
          datamap.addPoints(
            pointData,
            {
              pointSize: {{point_size}},
              pointOutlineColor: {{point_outline_color}},
              pointLineWidth: {{point_line_width}},
              pointHoverColor: {{point_hover_color}},
              pointLineWidthMaxPixels: {{point_line_width_max_pixels}},
              pointLineWidthMinPixels: {{point_line_width_min_pixels}},
              pointRadiusMaxPixels: {{point_radius_max_pixels}},
              pointRadiusMinPixels: {{point_radius_min_pixels}},
            }
          );

          {%- if enable_lasso_selection %}
          /* Lasso Selection */
          const lassoSelector = new LassoSelectionTool(
            datamap,
            lassoSelectionCallback
          );
          {% endif -%}
          document.getElementById("loading").style.display = "none";
          updateProgressBar('point-data-progress', 100);
          checkAllDataLoaded();
        }
      };
    }

    function loadLabelDataLayer() {
      labelDataWorker.postMessage({encodedData: labelDataEncoded, JSONParse: true});

      labelDataWorker.onmessage = async function(event) {
        if (event.data.type === "progress") {
          updateProgressBar('label-data-progress', event.data.progress);
        } else {
          const { data } = event.data;
          const labelData = data;
          datamap.addLabels(labelData, {
            labelTextColor: {{label_text_color}},
            textMinPixelSize: {{text_min_pixel_size}},
            textMaxPixelSize: {{text_max_pixel_size}},
            textOutlineWidth: {{text_outline_width}},
            textOutlineColor: {{text_outline_color}},
            textBackgroundColor: {{text_background_color}},
            fontFamily: "{{font_family}}",
            fontWeight: {{font_weight}},
            lineSpacing: {{line_spacing}},
            textCollisionSizeScale: {{text_collision_size_scale}},
          });

          {% if cluster_boundary_polygons %}
          datamap.addBoundaries(labelData, {
            clusterBoundaryLineWidth: {{cluster_boundary_line_width}},
          });
          {% endif %}
          document.getElementById("loading").style.display = "none";
          updateProgressBar('label-data-progress', 100);
          checkAllDataLoaded();
        }
      };
    }

    function loadMetaData() {
      metaDataWorker.postMessage({encodedData: hoverDataEncoded, JSONParse: true});

      metaDataWorker.onmessage = async function(event) {
        if (event.data.type === "progress") {
          updateProgressBar('meta-data-progress', event.data.progress);
        } else {
          const { data } = event.data;
          const hoverData = data;
          datamap.addMetaData(hoverData, {
            tooltipFunction: {{get_tooltip}},
            onClickFunction: {{on_click if on_click else "null"}},
            searchField: "{{search_field}}",
          });

          {% if search %}
          function debounce(func, timeout = 250){
              let timer;
              return (...args) => {
                  clearTimeout(timer);
                  timer = setTimeout(() => { func.apply(this, args); }, timeout);
              };
          }

          /* Search */
          searchItem.addEventListener("input", debounce(event => datamap.searchText(event.target.value)));
          {% endif -%}
          updateProgressBar('meta-data-progress', 100);
          checkAllDataLoaded();
        }
      };
    }

    {% if enable_histogram %}
    function loadHistogramBinData() {
      return new Promise((resolve, reject) => {
        histogramBinDataWorker.postMessage({encodedData: histogramBinDataEncoded, JSONParse: true});

        histogramBinDataWorker.onmessage = async function(event) {
          if (event.data.type === "progress") {
            updateProgressBar('histogram-bin-data-progress', event.data.progress);
          } else {
            const { data } = event.data;
            const histogramBinData = data;
            resolve(histogramBinData);
            updateProgressBar('histogram-bin-data-progress', 100);
            checkAllDataLoaded();
          }
        };
      });
    }

    function loadHistogramIndexData() {
      return new Promise((resolve, reject) => {
        histogramIndexDataWorker.postMessage({encodedData: histogramIndexDataEncoded, JSONParse: false});

        histogramIndexDataWorker.onmessage = async function(event) {
          if (event.data.type === "progress") {
            updateProgressBar('histogram-index-data-progress', event.data.progress);
          } else {
            const { data } = event.data;
            const histogramIndexData = simpleArrowParser(data);
            resolve(histogramIndexData);
            updateProgressBar('histogram-index-data-progress', 100);
            checkAllDataLoaded();
          }
        };
      });
    }

    function loadHistogramData() {
      const chartSelectionCallback = chartSelectedIndices => {
        // Update data manager
        if (chartSelectedIndices === null) {
          datamap.removeSelection(histogramItemId);
        } else {
          datamap.addSelection(chartSelectedIndices, histogramItemId);
        }
      };

      Promise.all([loadHistogramBinData(), loadHistogramIndexData()]).then(([histogramBinData, histogramIndexData]) => {
        const histogramData = { rawBinData: histogramBinData, rawIndexData: histogramIndexData };
        histogramItem = D3Histogram.create({
          data: histogramData, 
          chartContainerId: histogramItemId,
          {%- if histogram_width %}
          chartWidth: {{ histogram_width }},
          {%- endif %}
          {%- if histogram_height %}
          chartHeight: {{ histogram_height }},
          {%- endif %}
          {%- if histogram_bin_count %}
          binCount: {{ histogram_bin_count }},
          {%- endif %}
          {%- if histogram_title %}
          title: "{{ histogram_title }}",
          {%- endif %}
          {%- if histogram_bin_fill_color %}
          binDefaultFillColor: "{{ histogram_bin_fill_color }}",
          {%- endif %}
          {%- if histogram_bin_selected_fill_color %}
          binSelectedFillColor: "{{ histogram_bin_selected_fill_color }}",
          {%- endif %}
          {%- if histogram_bin_unselected_fill_color %}
          binUnselectedFillColor: "{{ histogram_bin_unselected_fill_color }}",
          {%- endif %}
          {%- if histogram_bin_context_fill_color %}
          binContextFillColor: "{{ histogram_bin_context_fill_color }}",
          {%- endif %}
          {%- if histogram_log_scale %}
          logScale: {{ histogram_log_scale|lower }},
          {%- endif %}
          chartSelectionCallback
        });

        datamap.connectHistogram(histogramItem);
      });
    }
    {% endif -%}

    loadPointDataLayer();
    loadLabelDataLayer();
    loadMetaData();
    {%- if enable_histogram %}
    loadHistogramData();
    {% endif -%}
    {% if custom_js %}
    {{ custom_js }}
    {% endif -%}
  </script>
</html>
