// assign a unique ID for each TOC item
const assignIDs = (toc) => {
  let id = 0;
  const assignID = (item) => {
    item.id = id++;
    if (item.subitems) for (const subitem of item.subitems) assignID(subitem);
  };
  for (const item of toc) assignID(item);
  return toc;
};

const flatten = (items) =>
  items.flatMap((item) => (item.subitems?.length ? [item, flatten(item.subitems)].flat() : item));

export class TOCProgress {
  async init({ toc, ids, splitHref, getFragment }) {
    assignIDs(toc);
    const items = flatten(toc);
    const grouped = new Map();
    for (const [i, item] of items.entries()) {
      const [id, fragment] = (await splitHref(item?.href)) ?? [];
      const value = { fragment, item };
      if (grouped.has(id)) grouped.get(id).items.push(value);
      else grouped.set(id, { prev: items[i - 1], items: [value] });
    }
    const map = new Map();
    for (const [i, id] of ids.entries()) {
      if (grouped.has(id)) map.set(id, grouped.get(id));
      else map.set(id, map.get(ids[i - 1]));
    }
    this.ids = ids;
    this.map = map;
    this.getFragment = getFragment;
  }
  getProgress(index, range) {
    if (!this.ids) return;
    const id = this.ids[index];
    const obj = this.map.get(id);
    if (!obj) return null;
    const { prev, items } = obj;
    if (!items) return prev;
    if (!range || (items.length === 1 && !items[0].fragment)) return items[0].item;

    const doc = range.startContainer.getRootNode();
    for (const [i, { fragment }] of items.entries()) {
      const el = this.getFragment(doc, fragment);
      if (!el) continue;
      if (range.comparePoint(el, 0) > 0) return items[i - 1]?.item ?? prev;
    }
    return items[items.length - 1].item;
  }
}

export class SectionProgress {
  constructor(sections, sizePerLoc, sizePerTimeUnit) {
    this.sizes = sections.map((s) => (s.linear != "no" && s.size > 0 ? s.size : 0));
    this.sizePerLoc = sizePerLoc;
    this.sizePerTimeUnit = sizePerTimeUnit;
    this.sizeTotal = this.sizes.reduce((a, b) => a + b, 0);
    this.sectionFractions = this.#getSectionFractions();
  }
  #getSectionFractions() {
    const { sizeTotal } = this;
    const results = [0];
    let sum = 0;
    for (const size of this.sizes) results.push((sum += size) / sizeTotal);
    return results;
  }
  // get progress given index of and fractions within a section
  getProgress(index, fractionInSection, pageFraction = 0) {
    const { sizes, sizePerLoc, sizePerTimeUnit, sizeTotal } = this;
    const sizeInSection = sizes[index] ?? 0;
    const sizeBefore = sizes.slice(0, index).reduce((a, b) => a + b, 0);
    const size = sizeBefore + fractionInSection * sizeInSection;
    const nextSize = size + pageFraction * sizeInSection;
    const remainingTotal = sizeTotal - size;
    const remainingSection = (1 - fractionInSection) * sizeInSection;
    return {
      fraction: nextSize / sizeTotal,
      section: {
        current: index,
        total: sizes.length,
      },
      location: {
        current: Math.floor(size / sizePerLoc),
        next: Math.floor(nextSize / sizePerLoc),
        total: Math.ceil(sizeTotal / sizePerLoc),
      },
      time: {
        section: remainingSection / sizePerTimeUnit,
        total: remainingTotal / sizePerTimeUnit,
      },
    };
  }
  // the inverse of `getProgress`
  // get index of and fraction in section based on total fraction
  getSection(fraction) {
    if (fraction <= 0) return [0, 0];
    if (fraction >= 1) return [this.sizes.length - 1, 1];
    fraction = fraction + Number.EPSILON;
    const { sizeTotal } = this;
    let index = this.sectionFractions.findIndex((x) => x > fraction) - 1;
    if (index < 0) return [0, 0];
    while (!this.sizes[index]) index++;
    const fractionInSection =
      (fraction - this.sectionFractions[index]) / (this.sizes[index] / sizeTotal);
    return [index, fractionInSection];
  }
}
