/*
  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',
    }],
    ['Estradiol SourceHut:        ', { colour: '#cdcdcd' }],
  );
  wait(100);
  replaceLast((v) => [...v, ['sh.estrogen.zone/~memdmp', {
    colour: '#99aaff',
    underlined: true,
    weight: 700,
    url: 'newtab:https://sh.estrogen.zone/~memdmp',
  }]]);
  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://sh.estrogen.zone/~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.sh.estrogen.zone/~memdmp/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'),
    },
  ]);

  //

  return lines;
})();