/*!
 * Matomo - free/libre analytics platform
 *
 * @link    https://matomo.org
 * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
 */

import { computed, ref, readonly } from 'vue';
import Matomo from '../Matomo/Matomo';
import { Periods, format } from '../Periods'; // important to load all periods here

const { piwik, broadcast } = window;

function isValidPeriod(periodStr: string, dateStr: string) {
  try {
    Periods.parse(periodStr, dateStr);
    return true;
  } catch (e) {
    return false;
  }
}

// using unknown since readonly does not work well with recursive types like QueryParameters
type ParsedQueryParameters = Record<string, unknown>;

/**
 * URL store and helper functions.
 */
class MatomoUrl {
  readonly url = ref<URL|null>(null);

  readonly urlQuery = computed(
    () => (this.url.value ? this.url.value.search.replace(/^\?/, '') : ''),
  );

  readonly hashQuery = computed(
    () => (this.url.value ? this.url.value.hash.replace(/^[#/?]+/, '') : ''),
  );

  readonly urlParsed = computed(() => readonly(
    this.parse(this.urlQuery.value) as ParsedQueryParameters,
  ));

  readonly hashParsed = computed(() => readonly(
    this.parse(this.hashQuery.value) as ParsedQueryParameters,
  ));

  readonly parsed = computed(() => readonly({
    ...this.urlParsed.value,
    ...this.hashParsed.value,
  } as ParsedQueryParameters));

  constructor() {
    this.url.value = new URL(window.location.href);

    window.addEventListener('hashchange', (event) => {
      this.url.value = new URL(event.newURL);
      this.updatePeriodParamsFromUrl();
      this.updatePageTitle();
    });

    this.updatePeriodParamsFromUrl();
    this.updatePageTitle();
  }

  updateHashToUrl(urlWithoutLeadingHash: string) {
    const wholeHash = `#${urlWithoutLeadingHash}`;

    if (window.location.hash === wholeHash) { // trigger event manually since the url is the same
      window.dispatchEvent(new HashChangeEvent('hashchange', {
        newURL: window.location.href,
        oldURL: window.location.href,
      }));
    } else {
      window.location.hash = wholeHash;
    }
  }

  updateHash(params: QueryParameters|string) {
    const modifiedParams = this.getFinalHashParams(params);
    const serializedParams = this.stringify(modifiedParams);

    this.updateHashToUrl(`?${serializedParams}`);
  }

  updateUrl(params: QueryParameters|string, hashParams: QueryParameters|string = {}) {
    const serializedParams: string = typeof params !== 'string' ? this.stringify(params) : params;

    const modifiedHashParams = Object.keys(hashParams).length
      ? this.getFinalHashParams(hashParams, params)
      : {};

    const serializedHashParams: string = this.stringify(modifiedHashParams);

    let url = `?${serializedParams}`;
    if (serializedHashParams.length) {
      url = `${url}#?${serializedHashParams}`;
    }

    window.broadcast.propagateNewPage('', undefined, undefined, undefined, url);
  }

  private getFinalHashParams(
    params: QueryParameters|string,
    urlParams: QueryParameters|string = {},
  ) {
    const paramsObj = typeof params !== 'string'
      ? params as QueryParameters
      : this.parse(params as string);

    const urlParamsObj = typeof params !== 'string'
      ? urlParams as QueryParameters
      : this.parse(urlParams as string);

    return {
      // these params must always be present in the hash
      period: urlParamsObj.period || this.parsed.value.period,
      date: urlParamsObj.date || this.parsed.value.date,
      segment: urlParamsObj.segment || this.parsed.value.segment,

      ...paramsObj,
    };
  }

  // if we're in an embedded context, loads an entire new URL, otherwise updates the hash
  updateLocation(params: QueryParameters|string) {
    if (Matomo.helper.isReportingPage()) {
      this.updateHash(params);
      return;
    }

    this.updateUrl(params);
  }

  getSearchParam(paramName: string): string {
    const hash = window.location.href.split('#');

    const regex = new RegExp(`${paramName}(\\[]|=)`);
    if (hash && hash[1] && regex.test(decodeURIComponent(hash[1]))) {
      const valueFromHash = window.broadcast.getValueFromHash(paramName, window.location.href);

      // for date, period and idsite fall back to parameter from url, if non in hash was provided
      if (valueFromHash
        || (paramName !== 'date' && paramName !== 'period' && paramName !== 'idSite')
      ) {
        return valueFromHash;
      }
    }

    return window.broadcast.getValueFromUrl(paramName, window.location.search);
  }

  parse(query: string): QueryParameters {
    return broadcast.getValuesFromUrl(`?${query}`, true);
  }

  stringify(search: QueryParameters): string {
    const searchWithoutEmpty = Object.fromEntries(
      Object.entries(search).filter(([, value]) => value !== '' && value !== null && value !== undefined),
    );

    // using jQuery since URLSearchParams does not handle array params the way Matomo uses them
    return $.param(searchWithoutEmpty).replace(/%5B%5D/g, '[]')
      // some browsers treat URLs w/ date=a,b differently from date=a%2Cb, causing multiple
      // entries to show up in the browser history.
      .replace(/%2C/g, ',')
      // jquery seems to encode space characters as '+', but certain parts of matomo won't
      // decode it correctly, so we make sure to use %20 instead
      .replace(/\+/g, '%20');
  }

  getMenuPathSuffix(): { category: string; subcategory: string } {
    const category = this.getSearchParam('category') as string;
    const subcategory = this.getSearchParam('subcategory') as string;
    return { category: decodeURIComponent(category), subcategory: decodeURIComponent(subcategory) };
  }

  getDateAndPeriodFromUrl(): { date: string; period: string } {
    return {
      date: this.getSearchParam('date') || '',
      period: this.getSearchParam('period') || '',
    };
  }

  updatePageTitle() {
    const { period, date } = this.getDateAndPeriodFromUrl();
    const { category, subcategory } = this.getMenuPathSuffix();
    const segment = this.getSearchParam('segment') || '';
    piwik.updateTitle(date, period, category, subcategory, segment);
  }

  updatePeriodParamsFromUrl(): void {
    const { period, date: initialDate } = this.getDateAndPeriodFromUrl();
    let date = initialDate;
    if (!isValidPeriod(period, date)) {
      // invalid data in URL
      return;
    }

    if (piwik.period === period && piwik.currentDateString === date) {
      // this period / date is already loaded
      return;
    }

    piwik.period = period;

    const dateRange = Periods.parse(period, date).getDateRange();
    piwik.startDateString = format(dateRange[0]);
    piwik.endDateString = format(dateRange[1]);
    // do not set anything to previousN/lastN, as it's more useful to plugins
    // to have the dates than previousN/lastN.
    if (piwik.period === 'range') {
      date = `${piwik.startDateString},${piwik.endDateString}`;
    }

    piwik.currentDateString = date;
  }
}

const instance = new MatomoUrl();
export default instance;

piwik.updatePeriodParamsFromUrl = instance.updatePeriodParamsFromUrl.bind(instance);
