<script>
  import { onMount } from 'svelte';
  import api from "$/client/api.js";
  import Source from "$/client/components/Source.svelte";
  import Spinner from "$/client/components/Spinner.svelte";
  import Icon from "$/client/components/Icon.svelte";
  import IsVisible from "$/client/components/IsVisible.svelte";
  import { defer, random_numbers } from "$/client/helpers.js";

  let chart_load_promise = defer();
  let stats = {};
  let by_chains = [];
  let original_data = [];
  let search_text = "";
  let search_error = "";
  let dates = [];
  let combine_results = false;
  let combined_chains = [];

  const date_key = (date) => {
    // construct a consistent date format
    const d = new Date(date);
    // month is zero based
    return `${d.getMonth()+1}-${d.getDate()}`
  }

  const new_result = () => {
    // the idea is to initialize every chain's map with dates set to zero
    const init_keys = dates.map(d => [date_key(d), 0]);

    // then when it's analyzed later we'll have a full list
    return {
      hits_by_date: Object.fromEntries(init_keys),
      sum: 0,
      id: random_numbers(1)[0]
    };
  }

  const transform_by_chains = (data, min=1) => {
    const result = {};

    // sort keys for dates
    dates = Object.keys(data).sort();

    for(let date of dates) {
      const chains = data[date];

      for(let [chain, chain_stats] of Object.entries(chains)) {
        const result_chain = result[chain] || new_result();
        // convert the date to get the DOW?
        result_chain.hits_by_date[date_key(date)] = chain_stats.count;
        result_chain.sum += chain_stats.count;
        result[chain] = result_chain;
      }
    }

    // now remove any stats that are too thin to care about
    const filtered = [];

    for(let chain in result) {
      // keep anything with register in it, but only if we aren't combining results
      if(combine_results || chain.includes("register") || result[chain].sum > min) {
        filtered.push([chain, result[chain]]);
      }
    }

    return filtered.sort((a, b) => b[1].sum - a[1].sum);
  }

  const calc_stats = (data) => {
    let sum = 0;
    let sumsq = 0;
    const count = data.length;
    let min = 0;
    let max = 0;

    for(let sample of data) {
      sum += sample;
      sumsq += sample * sample;
      min = sample < min ? sample : min;
      max = sample > max ? sample : max;
    }

    // sqrt( (self.sumsq - ( self.sum * self.sum / self.count )) / (self.count - 1) )
    // normally you include count - 1 for accurate stats, but that results in divide by 0 so screw it
    let stddev = Math.sqrt((sumsq - (sum * sum / count)) / (count));

    let mean = sum / count;
    let upper = mean + (2 * stddev);
    let lower = mean - (2 * stddev);

    return {sum, sumsq, count, mean, upper, lower, min, max, stddev};
  }

  const config_chart = () => {
    Chart.defaults.font.size = 16;
  }

  const create_chart = (title, data) => {
    const { hits_by_date, id } = data;
    const labels = Object.keys(hits_by_date);
    const counts = Object.values(hits_by_date);

    const ctx = document.getElementById(`chart-${id}`);

    // sometimes it scrolls by too fast or is registered but not visible
    if(!ctx) return;

    // should do this dynamically on the fly instead
    const info = calc_stats(counts);
    const chart_max = Math.max(info.max, info.upper);

    // the only problem with the data is it's sparse and missing dates

    try {
      const chart = new Chart(ctx, {
        type: 'line',
        data: {
          labels,
          datasets: [{
            data: counts,
            fill: false,
            color: 'hsl(0, 0%, 10%)',
            borderColor: 'hsl(0, 0%, 20%)',
            backgroundColor: 'hsl(0, 0%, 30%)',
            tension: 0.1,
            pointRadius: counts.length > 40 ? 0 : 2,
            borderWidth: 1,
          }]
        },
        options: {
          scales: {
            y: {
              beginAtZero: true,
              max: chart_max * 1.1,
              min: 0,
            }
          },
          plugins: {
            title: {
              display: true,
              text: `${Math.round(info.mean)} +/- ${Math.round(info.upper)}`,
              align: 'end',
              position: 'bottom',
            },
            legend: {
              display: false
            },
            annotation: {
              annotations: {
                mean: {
                  type: 'line',
                  yMin: info.mean,
                  yMax: info.mean,
                  borderColor: 'hsl(100, 50%, 50%)',
                  borderWidth: 1,
                },
                top: {
                  type: 'line',
                  yMin: info.upper,
                  yMax: info.upper,
                  borderColor: 'hsl(0, 50%, 60%)',
                  borderWidth: 1,
                },
                bottom: {
                  type: 'line',
                  yMin: info.lower,
                  yMax: info.lower,
                  borderColor: 'hsl(0, 50%, 60%)',
                  borderWidth: 1,
                },
                mid_top: {
                  type: 'line',
                  yMin: info.mean + info.stddev,
                  yMax: info.mean + info.stddev,
                  borderColor: 'hsl(50, 50%, 60%)',
                  borderWidth: 1,
                },
                mid_bottom: {
                  type: 'line',
                  yMin: info.mean - info.stddev,
                  yMax: info.mean - info.stddev,
                  borderColor: 'hsl(50, 50%, 60%)',
                  borderWidth: 1,
                },
              }
            }
          },
        }
      });
    } catch(error) {
      // sometimes you get an error about a chart needing to be destroyed,
      // most likely because the HTML node is hidden by Svelte
      // but I'm not sure how to do that, and it doesn't seem to
      // cause problems so just log it
      console.error(error.message);
    }
  }

  const calculate_combination = (result) => {
    const all_chains = [];
    const first = new_result();

    for(let [urls, stats] of result) {
      all_chains.push([stats.sum, urls]);

      for(let key in first.hits_by_date) {
        first.hits_by_date[key] += stats.hits_by_date[key];
      }
    }

    combined_chains = all_chains;
    return [["", first]];
  }

  const search = () => {
    try {
      chart_load_promise = defer();
      let result = [];

      if(search_text.trim() === "") {
        result = original_data;
      } else {
        const pattern = new RegExp(search_text);

        result = original_data.filter(data => {
          return data[0].match(pattern);
        });
      }

      if(combine_results) {
        by_chains = calculate_combination(result);
      } else {
        // easy just assign it to get svelte going
        by_chains = result;
      }

      // reset the error
      search_error = "";
    } catch(error) {
      search_error = error.message;
    } finally {
      chart_load_promise.resolve();
    }
  }

  const toggle_combined = () => {
    combine_results = !combine_results;
    search();
  }

  const reset_search = () => {
    chart_load_promise = defer();
    // reset the results
    by_chains = original_data;

    // clear the errors
    search_error = "";
    chart_load_promise.resolve();
  }

  $: if(search_text === "") {
    reset_search();
  } else {
    search();
  }

  onMount(async () => {
    const [status, data] = await api.get("/reports/chains.json");

    if(status === 200) {
      stats = data;
      // to enable search, keep an original, and by_chains for results
      original_data = transform_by_chains(stats.result);
      by_chains = original_data;
    } else {
      console.error("Invalid response", status, data);
    }
  });
</script>

<style>
  #chart-container {
    min-height: 200px;
  }

  search-bar {
    display: flex;
    align-items: center;
  }

  search-bar combine-button {
    filter: opacity(60%);
    padding: 0.5rem;
  }

  search-bar combine-button.toggled {
    filter: opacity(100%);
  }
</style>

<Source src="/js/chart.min.js" on:load={ config_chart }/>
<Source src="/js/chartjs-plugin-annotation.min.js" on:load={ () => chart_load_promise.resolve() } />

{#if stats.domain}
  <h1>{ stats.domain }</h1>
  <search-bar>
    <input type="text" placeholder="Search..." bind:value={ search_text } on:enter={ search } />
    <combine-button on:click={ toggle_combined } class:toggled={ combine_results}>
      <Icon name="shuffle" tooltip="Shuffle" />
    </combine-button>
  </search-bar>
  <div>{ by_chains.length } results{#if combine_results}<b>, combined</b>{/if}</div>
{/if}

{#await chart_load_promise}
  <Spinner />
{:then}
  <hr />
  {#if search_error}
    <callout class="error">{ search_error }</callout>
  {/if}
  {#each by_chains as data, i}
    <IsVisible on:visible={ () => setTimeout(() => create_chart(data[0], data[1]), 200) }>
      <div id="chart-container" slot="visible">
        {#if combine_results}
        <canvas id="chart-{data[1].id}" width="400" height="100"></canvas>
        <table>
          <tr><th>sum</th><th>chain</th></tr>
          {#each combined_chains as [sum, url]}
            <tr><td>{ sum }</td><td style="text-align: left;">{url}</td></tr>
          {/each}
        </table>
        {:else}
          <div><b>{data[0]}</b></div>
          <canvas id="chart-{data[1].id}" width="400" height="100"></canvas>
        {/if}
      </div>
      <div id="chart-container" slot="hidden"><p>&nbsp;</p></div>
    </IsVisible>
  {/each}
{/await}
