/*
  Copyright (C) 2024 memdmp

  This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

  This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more details.

  You should have received a copy of the GNU Affero General Public License along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/
export const biosSteps = 3;
export const biosStepInterval = 1000;

const wpmToTypingSpeed = (wpm: number, avgWordLen: number) =>
  (1 / ((wpm * avgWordLen) / 60)) * 1000;

const averageWordLength = 4.793;
const typingInfo = (wpm = 80) => ({
  typingSpeedAvg: wpmToTypingSpeed(wpm, averageWordLength),
  typingDeviation: 20, // typing speed deviation is often correlated to the typing speed - TODO: at way lower speeds, the correlation is inverse; the more deviation there is (is this also the case for fast typing? what is the inflection point if not? i forgor i went down a rabbit hole yrs ago)
});
export type TypingSpeed = ReturnType<typeof typingInfo>;
export const getDelay = (typingInfo: TypingSpeed) =>
  (Math.random() * 2 - 1) * typingInfo.typingDeviation +
  typingInfo.typingSpeedAvg;
export const login = {
  username: "lain",
  passwordLength: 12,
  ...typingInfo(100), // one usually has muscle memory for well-known frequently-typed credentials, hence higher wpm
};
export const ttyTypingInfo = typingInfo();

export type RenderBlock = {
  value: string;
  colour?: string;
  weight?: 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900;
  italic?: boolean;
  underlined?: boolean;
  url?:
    | `newtab:${string}`
    | `currenttab:${string}`
    | ((textObj: TTYText & { kind: "text" }) => void);
  bg?: string;
  raw?: boolean;
  dl?: string;
};
export type TTYText =
  | {
      kind: "text";
      renderrestriction?: "everywhere" | "js-only" | "noscript";
      value: RenderBlock[];
      id: string;
      classes: string[];
    }
  | {
      kind: "removeNode";
      removedId: string;
      removedItemClassList: string[];
    }
  | {
      kind: "time";
      delay: number;
    }
  | {
      kind: "cursorVisibility";
      visible: boolean;
    }
  | {
      kind: "clear";
    };
export type Only<Obj, Keys extends keyof Obj> = {
  [k in Keys]: Obj[k];
};
export type Diff<
  T extends {
    [k: string]: unknown;
  },
  U extends keyof T,
> = ({ [P in keyof T]: P } & { [P in U]: never } & {
  [x: string]: never;
})[keyof T];
export type Omit<
  T extends {
    [k: string]: unknown;
  },
  K extends keyof T,
> = Pick<T, Diff<T, K>>;
export const ttyLines: TTYText[] = (() => {
  const lines: TTYText[] = [];
  let ids: string[] = [];
  const byId = new Map<string, TTYText>();
  let i = 69420;
  let defaultRenderRestriction: Only<
    TTYText & { kind: "text" },
    "renderrestriction"
  >["renderrestriction"] = "everywhere";
  type LimitedRenderBlock = Omit<RenderBlock, "value">;
  type RenderBlockArg = [text: string, options?: LimitedRenderBlock];
  /** due to hellish css constraints, each text() call creates its own line - however, each block in a text() is joined on the same line. */
  const text = (
    globalOptions:
      | Only<TTYText & { kind: "text" }, "renderrestriction">
      | RenderBlockArg,
    ...blocks: RenderBlockArg[]
  ) => {
    const id = (++i).toString();
    ids.push(id);
    if (Array.isArray(globalOptions)) {
      blocks.unshift(globalOptions);
      globalOptions = {};
    }
    const txt = {
      kind: "text",
      renderrestriction:
        globalOptions.renderrestriction ?? defaultRenderRestriction,
      value: blocks.map(([t, o]) => ({
        value: t,
        colour: "#b9b9b9",
        ...o,
      })),
      id,
      classes: [`ttytext-${id}`],
    } as TTYText;
    lines.push(txt);
    byId.set(id, txt);
    return id;
  };
  const wait = (time: number) =>
    lines.push({
      kind: "time",
      delay: time,
    });
  const del = (id: string | number) => {
    const removedId =
      typeof id === "string" ? id : id >= 0 ? ids[id] : ids[ids.length + id];
    const r = byId.get(removedId);
    if (r?.kind === "text") {
      lines.push({
        kind: "removeNode",
        removedId,
        removedItemClassList: r.classes,
      });
      r.classes.push("ttytext-removed-after-done");
    }
    ids = ids.filter((v) => v !== removedId);
  };
  const delSince = (id: string | number) => {
    const removedId =
      typeof id === "string" ? id : id >= 0 ? ids[id] : ids[ids.length + id];
    const idIndex = ids.indexOf(removedId);
    for (let i = idIndex; i < ids.length; i++) {
      const removedId = ids[i];
      const r = byId.get(removedId);
      if (r?.kind === "text") {
        r.classes.push("ttytext-removed-after-done");
        lines.push({
          kind: "removeNode",
          removedId,
          removedItemClassList: r.classes,
        });
      }
    }
    ids = ids.filter((_, i) => i < idIndex);
  };
  const getLast = () => {
    const v = byId.get(ids[ids.length - 1]);
    if (v?.kind == "text") {
      return v.value;
    } else return null;
  };
  const overwriteLast = (...newValue: RenderBlockArg[]) => {
    const v = byId.get(ids[ids.length - 1]);
    const l = v?.kind == "text" ? v.renderrestriction : "everywhere";
    del(-1);
    return text(
      {
        renderrestriction: l,
      },
      ...newValue,
    );
  };
  const replaceLast = (
    handler: (newValue: RenderBlockArg[]) => RenderBlockArg[],
  ) =>
    overwriteLast(
      ...handler(
        getLast()!.map(
          (v) =>
            [
              v.value,
              Object.fromEntries(
                Object.entries(v).filter(([k]) => k !== "value"),
              ),
            ] as const,
        ),
      ),
    );
  const everyTTYClear: (() => void)[] = [];
  const clear = () => {
    if (lines.find((v) => v.kind === "clear")) {
      delSince(lines.findLastIndex((v) => v.kind === "clear"));
    }
    lines.push({ kind: "clear" });
    everyTTYClear.forEach((v) => v());
  };

  everyTTYClear.push(() => {
    text(
      {
        renderrestriction: "noscript",
      },
      [
        "This browser does not support JS. Your experience may be degraded.",
        {
          bg: "#ff0000",
          colour: "#dedede",
        },
      ],
      ["\n", {}],
    );
  });
  everyTTYClear.forEach((v) => v());

  //

  wait(300);
  text({ renderrestriction: "js-only" }, [
    ((v) => v[Math.floor(Math.random() * v.length)])([
      "cool beats are stored in the balls",
      "found xml documents in the firmware",
      'overhead: "I don\'t consent" "hey thats my line"',
      "uwu",
      "i regret making this hellhole of a codebase",
    ]),
    {
      colour: "#777777",
    },
  ]);
  wait(1000);
  clear();
  text(
    [
      "lain",
      {
        colour: "#FFFF53",
        weight: 700,
      },
    ],
    [" in "],
    [
      "mem.estrogen.zone",
      {
        colour: "#17B117",
        weight: 700,
      },
    ],
    [" in "],
    [
      "~",
      {
        colour: "#53FFFF",
        weight: 700,
      },
    ],
  );
  text(
    ["❯ ", { colour: "#53FF53", weight: 600 }],
    [""],
    ["", { colour: "#777777" }],
  );
  {
    const targetstr = "/usr/bin/env wel";
    const completions = [
      '/bin/sh -c "$(/usr/bin/env curl -fsSL https://blahaj.estrogen.zone/)"',
      "/usr/local/bin/become-estradiol",
      "/usr/bin/shellutil ansi cheatsheet 24bit",
      "/usr/bin/env sh",
      "/usr/bin/env wc -l src/routes/anim.css",
      "/usr/bin/env welcome -c ~/.local/share/welcome.toml",
    ];
    for (const c of [...targetstr.split(""), "COMPLETE"]) {
      replaceLast((l) => {
        const prompt = l[l.length - 2];
        const suggestions = l[l.length - 1];
        if (c === "COMPLETE") {
          prompt[0] += suggestions[0];
          suggestions[0] = "";
        } else {
          prompt[0] += c;
          const completion = completions.find((v) => v.startsWith(prompt[0]));
          if (completion) {
            suggestions[0] = completion.substring(prompt[0].length);
          } else suggestions[0] = "";
        }
        return l;
      });
      if (c === "COMPLETE") {
        wait(100);
      } else wait(50 + Math.random() * 100);
    }
    text(["Preparing..."]);
    wait(200);
  }
  wait(1000);
  clear();
  text(["Welcome to"], [""], [" the Estrogen Zone!\n\n"]);
  wait(300);
  for (const c of " my corner of".split("")) {
    replaceLast((l) => {
      l[1][0] += c;
      return l;
    });
    wait(70);
  }
  text([
    "Places you can find me:",
    {
      colour: "#7a7a7a",
    },
  ]);
  wait(300);
  text(
    [
      " - ",
      {
        colour: "#7a7a7a",
      },
    ],
    ["estrogen.zone git:          ", { colour: "#cdcdcd" }],
  );
  wait(100);
  replaceLast((v) => [
    ...v,
    [
      "git.estrogen.zone",
      {
        colour: "#99aaff",
        underlined: true,
        weight: 700,
        url: "newtab:https://git.estrogen.zone",
      },
    ],
  ]);
  wait(100);
  text(
    [
      " - ",
      {
        colour: "#7a7a7a",
      },
    ],
    ["On jsr:                     ", { colour: "#cdcdcd" }],
  );
  wait(600);
  replaceLast((v) => [
    ...v,
    [
      "jsr.io/@memdmp",
      {
        colour: "#f7df23",
        underlined: true,
        weight: 700,
        url: "newtab:https://jsr.io/@memdmp",
      },
    ],
  ]);
  wait(200);
  text(
    [
      " - ",
      {
        colour: "#7a7a7a",
      },
    ],
    ["In random hackerspaces", { colour: "#aaaaaa" }],
  );
  wait(600);
  text([
    "\nYou may find these useful:",
    {
      colour: "#7a7a7a",
    },
  ]);
  wait(400);
  text(
    [
      " - ",
      {
        colour: "#7a7a7a",
      },
    ],
    ["GPG Root Key:               ", { colour: "#cdcdcd" }],
  );
  wait(1000);
  replaceLast((v) => [
    ...v,
    [
      "B546778F06BBCC8EC167DB3CD919706487B8B6DE",
      {
        colour: "#58C7F3",
        underlined: true,
        weight: 700,
        url: "currenttab:/keys/memdmp/primary.pgp",
        dl: "memdmp-primary.pgp",
      },
    ],
  ]);
  wait(100);
  replaceLast((v) => [
    ...v,
    [
      " (root key)",
      {
        colour: "#999999",
      },
    ],
  ]);
  wait(400);
  text([
    "   - ",
    {
      colour: "#7a7a7a",
    },
  ]);
  wait(70);
  replaceLast((v) => [
    ...v,
    ["GPG Git Key:                ", { colour: "#cdcdcd" }],
  ]);
  wait(100);
  replaceLast((v) => [
    ...v,
    [
      "5134F05BD8D9DB8C6C0E1515A9439D346AB6DF4E",
      {
        colour: "#F0A3B3",
        underlined: true,
        weight: 700,
        url: "currenttab:/keys/memdmp/git.pgp",
        dl: "memdmp-git.pgp",
      },
    ],
  ]);
  wait(100);
  replaceLast((v) => [
    ...v,
    [
      " (",
      {
        colour: "#999999",
      },
    ],
    [
      "signed",
      {
        colour: "#F0A3B3",
        underlined: true,
        weight: 500,
        url: "currenttab:/keys/memdmp/git.pgp.sig",
        dl: "memdmp-git.pgp.sig",
      },
    ],
    [")", { colour: "#999999" }],
  ]);
  wait(45);
  text([
    "   - ",
    {
      colour: "#7a7a7a",
    },
  ]);
  wait(70);
  replaceLast((v) => [
    ...v,
    [
      "GPG Release Signing Key:    ",
      {
        colour: "#cdcdcd",
      },
    ],
  ]);
  wait(100);
  replaceLast((v) => [
    ...v,
    [
      "0D93102265071798C7B65A4C9F0739B9E0C8FD60",
      {
        colour: "#ffffff",
        underlined: true,
        weight: 700,
        url: "currenttab:/keys/memdmp/release.pgp",
        dl: "memdmp-release.pgp",
      },
    ],
  ]);
  wait(100);
  replaceLast((v) => [
    ...v,
    [
      " (",
      {
        colour: "#999999",
      },
    ],
    [
      "signed",
      {
        colour: "#ffffff",
        underlined: true,
        weight: 500,
        url: "currenttab:/keys/memdmp/release.pgp.sig",
        dl: "memdmp-release.pgp.sig",
      },
    ],
    [")", { colour: "#999999" }],
  ]);
  wait(100);
  text([
    " - ",
    {
      colour: "#7a7a7a",
    },
  ]);
  wait(35);
  replaceLast((v) => [
    ...v,
    [
      "Source Code:                ",
      {
        colour: "#cdcdcd",
      },
    ],
  ]);
  wait(100);
  replaceLast((v) => [
    ...v,
    [
      "git.estrogen.zone/mem-estrogen-zone",
      {
        colour: "#F0A3B3",
        underlined: true,
        weight: 700,
        url: "newtab:/upstream/",
      },
    ],
  ]);
  wait(100);
  text([
    " - ",
    {
      colour: "#7a7a7a",
    },
  ]);
  wait(35);
  replaceLast((v) => [
    ...v,
    [
      "Canaries:                   ",
      {
        colour: "#cdcdcd",
      },
    ],
  ]);
  wait(100);
  replaceLast((v) => [
    ...v,
    [
      "/canaries/",
      {
        colour: "#58C7F3",
        underlined: true,
        weight: 700,
        url: "currenttab:/canaries/",
      },
    ],
  ]);
  wait(5000);
  text([
    `<button style="padding: 12px 12px;background: #fff2;margin-top: 0.4rem;border-radius: 0.7rem;opacity:0.1;margin-top:3rem;" data-el="le funny button">have a button :3</button>`,
    {
      raw: true,
      url: () => alert("i abused too much css for this i wanna cry now"),
    },
  ]);
  wait(200);
  text(
    { renderrestriction: "js-only" },
    [
      "next time, you may want to ",
      {
        colour: "#fff2",
      },
    ],
    [
      `skip the animation<img style="opacity:0;position:fixed;top:0;left:0;width:0px;height:0px;pointer-events:none;" src='about:blank' onerror='setTimeout(()=>{if(location.pathname==="/skip-animation")document.querySelector(${JSON.stringify(`a[href=${JSON.stringify("/skip-animation")}]`)})?.parentElement?.remove();else document.querySelector(${JSON.stringify(`a[href=${JSON.stringify("about:blank")}]`)})?.remove();},1)' />`,
      {
        url: "currenttab:/skip-animation",
        colour: "#fff2",
        underlined: true,
        raw: true,
      },
    ],
  );

  //

  return lines;
})();