/*
  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(
    [`Hi! I'm a`],
    [''],
    [
      ` software developer.\nI do shit like the hellhole you're seeing right now, amongst other things.\n\n`,
    ],
  );
  wait(300);
  for (const c of ' transfem'.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(100);
  text([
    '\nDid you know: this is all pure css animation hell.',
    {
      colour: '#7a7a7a',
    },
  ]);
  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===${JSON.stringify('/~mem/skip-animation')})document.querySelector(${JSON.stringify(`a[href=${JSON.stringify('/~mem/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;
})();