import { toAsyncIterable } from "jsr:@core/iterutil@0.9/async/to-async-iterable";

export interface Page {
  /** ページタイトル */
  title: string;
  /** このページに含める画像のインデックス */
  indice: number[];
  /** 前ページのタイトル
   *
   * なければ空文字
   */
  prev: string;
  /** 次ページのタイトル
   *
   * なければ空文字
   */
  next: string;

  /** 子ページのタイトル */
  children: string[];
}

export interface PageSource {
  url: string | URL;
  text: string;
}

/** ページ情報と画像URLとテキストから、scrapboxのページを作る TransformStream */
export class PageStringifyStream extends TransformStream<Page, string[]> {
  #stack: PageSource[] = [];
  #iter: AsyncIterator<PageSource>;
  #iterReady = Promise.resolve();

  async #at(index: number) {
    const item = this.#stack.at(index);
    if (item) return item;
    await this.#iterReady;
    const ready = (async () => {
      while (true) {
        const { done, value } = await this.#iter.next();
        if (done) return;
        this.#stack.push(value);
        if (index === this.#stack.length - 1) return value;
      }
    })();

    this.#iterReady = ready as Promise<void>;
    return ready;
  }
  /**
   * @param source 書籍に使う画像のURLとテキストのセット
   * @param [offset=0] 書籍formatで指定したページ番号が0以外で始まる場合、この数値を変えて調節する
   */
  constructor(
    source: Iterable<PageSource> | AsyncIterable<PageSource>,
    offset=0,
  ) {
    super({
      transform: async (page, controller) => {
        controller.enqueue([
          page.title,
          ...(await Promise.all(page.indice.map(async (i) => {
            const source = await this.#at(i+offset);
            if (!source) {
              console.warn(`Could not find the image source for ${i}.`);
              return [];
            }
            return [
              ...source.text.split("\n"),
              "",
              `[${source.url}]`,
              "",
            ];
          }))).flat(),
          ...page.children.map((title) => `[${title}]`),
          "",
          `<= ${page.prev !== "" ? `[${page.prev}]` : ""} | ${
            page.next !== "" ? `[${page.next}]` : ""
          } =>`,
          "",
        ]);
      },
    });
    this.#iter = (Symbol.iterator in source ? toAsyncIterable(source) : source)
      [Symbol.asyncIterator]();
  }
}

/** 生成したページデータを並び替える
 * @example
 * ```ts
 * import { CsvParseStream } from "jsr:@std/csv@1/parse-stream";
 * import { assertEquals } from "jsr:@std/assert@1/equals";
 *
 * const csv = `1,0,2,3
 * 1.1,3,3,
 * 1.2,x,x,2
 * 1.2.1,4,6,0
 * 1.2.2,6,12,0
 * column1,13,14,0
 * 2,,,2
 * 2.1,15,16,
 * 2.2,17,20,1
 * column2,19,19,0
 * 3,20,24,0`;
 * const sorted = ReadableStream.from([csv])
 *   .pipeThrough(new CsvParseStream())
 *   .pipeThrough(new IndexParseStream())
 *   .pipeThrough(new PageSortStream());
 *
 * assertEquals<Page[]>(await Array.fromAsync(sorted), [
 *   {
 *     title: "1",
 *     next: "2",
 *     prev: "",
 *     indice: [0, 1, 2],
 *     children: ["1.1", "1.2", "column1"],
 *   },
 *   { title: "1.1", next: "1.2", prev: "1", indice: [3], children: [] },
 *   {
 *     title: "1.2",
 *     next: "column1",
 *     prev: "1.1",
 *     indice: [],
 *     children: ["1.2.1", "1.2.2"],
 *   },
 *   {
 *     title: "1.2.1",
 *     next: "1.2.2",
 *     prev: "1.2",
 *     indice: [4, 5, 6],
 *     children: [],
 *   },
 *   {
 *     title: "1.2.2",
 *     next: "column1",
 *     prev: "1.2.1",
 *     indice: [6, 7, 8, 9, 10, 11, 12],
 *     children: [],
 *   },
 *   {
 *     title: "column1",
 *     next: "2",
 *     prev: "1.2",
 *     indice: [13, 14],
 *     children: [],
 *   },
 *   {
 *     title: "2",
 *     next: "3",
 *     prev: "1",
 *     indice: [],
 *     children: ["2.1", "2.2"],
 *   },
 *   { title: "2.1", next: "2.2", prev: "2", indice: [15, 16], children: [] },
 *   {
 *     title: "2.2",
 *     next: "3",
 *     prev: "2.1",
 *     indice: [17, 18, 19, 20],
 *     children: ["column2"],
 *   },
 *   { title: "column2", next: "3", prev: "2.2", indice: [19], children: [] },
 *   {
 *     title: "3",
 *     next: "",
 *     prev: "2",
 *     indice: [20, 21, 22, 23, 24],
 *     children: [],
 *   },
 * ]);
 * ```
 */
export class PageSortStream extends TransformStream<Page, Page> {
  #stack: Page[] = [];

  #find(title: string): Page | undefined {
    const index = this.#stack.findIndex((page) => page.title === title);
    if (index < 0) return;
    const [target] = this.#stack.splice(index, 1);
    return target;
  }
  constructor() {
    let returnedFirst = false;
    let next = "";
    const enqueueAndSetNext = (
      page: Page,
      controller: TransformStreamDefaultController<Page>,
    ) => {
      controller.enqueue(page);
      next = page.children.at(0) ?? page.next;
    };
    super({
      transform: (page, controller) => {
        if (page.title === next || (!returnedFirst && page.prev === "")) {
          if (!returnedFirst && page.prev === "") returnedFirst = true;
          enqueueAndSetNext(page, controller);
          return;
        }
        this.#stack.push(page);
      },
      flush: (controller) => {
        if (!returnedFirst) {
          const first = this.#stack.find((page) => page.prev === "");
          if (!first) throw new Error("Could not find the first page.");
          enqueueAndSetNext(first, controller);
        }
        while (next !== "") {
          const page = this.#find(next);
          if (!page) throw new Error(`Could not find the next page "${next}".`);
          enqueueAndSetNext(page, controller);
        }
      },
    });
  }
}

/**
 * @example
 * ```ts
 * import { CsvParseStream } from "jsr:@std/csv@1/parse-stream";
 * import { assertEquals } from "jsr:@std/assert@1/equals";
 *
 * const csv = `1,0,2,3
 * 1.1,3,3,
 * 1.2,x,x,2
 * 1.2.1,4,6,0
 * 1.2.2,6,12,0
 * column1,13,14,0
 * 2,,,2
 * 2.1,15,16,
 * 2.2,17,20,1
 * column2,19,19,0
 * 3,20,24,0`;
 * const data = ReadableStream.from([csv])
 *   .pipeThrough(new CsvParseStream())
 *   .pipeThrough(new IndexParseStream());
 *
 * assertEquals<Page[]>(await Array.fromAsync(data), [
 *   { title: "1.1", next: "1.2", prev: "1", indice: [3], children: [] },
 *   {
 *     title: "1.2.1",
 *     next: "1.2.2",
 *     prev: "1.2",
 *     indice: [4, 5, 6],
 *     children: [],
 *   },
 *   {
 *     title: "1.2.2",
 *     next: "column1",
 *     prev: "1.2.1",
 *     indice: [6, 7, 8, 9, 10, 11, 12],
 *     children: [],
 *   },
 *   {
 *     title: "1.2",
 *     next: "column1",
 *     prev: "1.1",
 *     indice: [],
 *     children: ["1.2.1", "1.2.2"],
 *   },
 *   {
 *     title: "column1",
 *     next: "2",
 *     prev: "1.2",
 *     indice: [13, 14],
 *     children: [],
 *   },
 *   {
 *     title: "1",
 *     next: "2",
 *     prev: "",
 *     indice: [0, 1, 2],
 *     children: ["1.1", "1.2", "column1"],
 *   },
 *   { title: "2.1", next: "2.2", prev: "2", indice: [15, 16], children: [] },
 *   { title: "column2", next: "3", prev: "2.2", indice: [19], children: [] },
 *   {
 *     title: "2.2",
 *     next: "3",
 *     prev: "2.1",
 *     indice: [17, 18, 19, 20],
 *     children: ["column2"],
 *   },
 *   {
 *     title: "2",
 *     next: "3",
 *     prev: "1",
 *     indice: [],
 *     children: ["2.1", "2.2"],
 *   },
 *   {
 *     title: "3",
 *     next: "",
 *     prev: "2",
 *     indice: [20, 21, 22, 23, 24],
 *     children: [],
 *   },
 * ]);
 * ```
 */
export class IndexParseStream extends TransformStream<string[], Page> {
  constructor() {
    const parents: { page: Page; childNum: number }[] = [];
    super({
      transform(row, controller) {
        // for loopの都合上、parentsには先頭から要素を追加する
        // 階層の深い順にparentが並ぶ
        const [title, start, end, childNumText] = row;
        const s = /^\d+$/.test(start) ? Math.abs(parseInt(start)) : -1;
        const e = /^\d+$/.test(end) ? Math.abs(parseInt(end)) : -1;
        const indice = s !== -1 && e !== -1 ? [...range(s, e)] : [];
        const childNum = /^\d+$/.test(childNumText)
          ? Math.abs(parseInt(childNumText))
          : 0;
        const page: Page = { title, indice, prev: "", next: "", children: [] };

        // 親見出しから処理する
        let i = 0;
        for (; i < parents.length; i++) {
          const parent = parents[i];
          // `page`を親見出しの子とする場合は、追加処理を行ってループを抜ける
          if (
            parent.childNum !== 0 &&
            parent.childNum > parent.page.children.length
          ) {
            // 親見出しの子にする
            parent.page.children.push(page.title);
            // 最初の子の場合のみ、親見出しとつなげる
            if (parent.page.children.length === 1) {
              page.prev = parent.page.title;
            }
            break;
          }

          // 見出しが完成しているときは、pageを同階層の次の見出しとみなす
          // 小見出しがない見出しもここで処理される
          parent.page.next = page.title;
          page.prev = parent.page.title;
        }

        // 完成済みの見出しをparentsから削って返す
        // 添字がi未満のparentsが、完成した見出しとなる
        for (const { page } of parents.splice(0, i)) {
          controller.enqueue(page);
        }

        // 一旦parentsにためてから処理する
        parents.unshift({ page, childNum });
      },
      flush(controller) {
        for (const { page } of parents) {
          controller.enqueue(page);
        }
      },
    });
  }
}

function* range(start: number, end: number) {
  for (let i = start; i <= end; i++) {
    yield i;
  }
}