// Attempt to get bootstrap peer to run under Deno
// Check if RTCPeerConnection is supported.
// TODO: virtual list, only rerender what's needed so things can keep playing.
/*
Problems
  1. Can't delete, very annoying
    Tombstones. Send all IDs and all Tombstones. Ask only for posts that we don't get a tombstone for. Don't send posts we have a tombstone for?

  Posts don't propagate, you need to refresh to see new posts.
    Broadcast when we post to all peers we know about.

  3. Posting is slow because too long to render
  2. Can't follow people
  4. Can't like or reply to posts

user
    posts
    media
    tombstones
    following
    profile
        name
        description
        profile pic


Restruucture the app around the data. App/WS split is messy. Clean it up.

*/
// import * as ForceGraph3D from "3d-force-graph";
// import { openDatabase, getData, addData, addDataArray, clearData, deleteData, mergeDataArray, getAllData, checkPostIds, getAllIds, getPostsByIds } from "db";
import { generateID } from "./IDUtils.js";
import { PeerManager } from "./PeerManager.js";
import { log } from "./log.js";
// import {PeerConnection} from "webRTC";
// declare let WebTorrent: any;
// declare let ForceGraph3D: any;
// declare let marked: any;
// declare let QRCode: any;
// let posts:any;
// let keyBase = "dandelion_posts_v1_"
// let key:string = "";
// interface PostTimestamp {
//   year: number,
//   month: number,
//   day: number,
//   hour: number,
//   minute: number,
//   second: number,
// }
// function waitMs(durationMs: number) {
//   return new Promise(resolve => setTimeout(resolve, durationMs));
// }
// function uuidToBytes(uuid: string): Uint8Array {
//   return new Uint8Array(uuid.match(/[a-fA-F0-9]{2}/g)!.map((hex) => parseInt(hex, 16)));
// }
// // Base58 character set
// const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
// // Base58 encoding
// // Base58 encoding
// function encodeBase58(buffer: Uint8Array): string {
//   let carry;
//   const digits = [0];
//   for (const byte of buffer) {
//     carry = byte;
//     for (let i = 0; i < digits.length; i++) {
//       carry += digits[i] << 8;
//       digits[i] = carry % 58;
//       carry = Math.floor(carry / 58);
//     }
//     while (carry > 0) {
//       digits.push(carry % 58);
//       carry = Math.floor(carry / 58);
//     }
//   }
//   let result = '';
//   for (const digit of digits.reverse()) {
//     result += BASE58_ALPHABET[digit];
//   }
//   // Handle leading zero bytes
//   for (const byte of buffer) {
//     if (byte === 0x00) {
//       result = BASE58_ALPHABET[0] + result;
//     } else {
//       break;
//     }
//   }
//   return result;
// }
// // Convert UUID v4 to Base58
// function uuidToBase58(uuid: string): string {
//   const bytes = uuidToBytes(uuid);
//   return encodeBase58(bytes);
// }
// // function log(message:string) {
// //   console.log.apply(null, log(message);
// //   let log = document.getElementById("log");
// //   let newlog = document.createElement('span');
// //   newlog.innerHTML = `<pre>${message}</pre>`;
// //   log?.appendChild(newlog);
// // }
// interface StoragePost {
//   data: Post;
// }
// class Post {
//   post_timestamp: Date;
//   post_id: string;
//   author: string;
//   author_id: string;
//   text: string;
//   image_data: ArrayBuffer | null;
//   importedFrom: "twitter" | null;
//   importSource: any;
//   constructor(
//     author: string,
//     author_id: string,
//     text: string,
//     post_timestamp: Date,
//     imageData: ArrayBuffer | null = null,
//     importedFrom: "twitter" | null = null,
//     importSource: any = null) {
//     this.post_timestamp = post_timestamp;
//     this.post_id = generateID();
//     this.author = author;
//     this.author_id = author_id;
//     this.text = text;
//     this.image_data = imageData;
//     this.importedFrom = importedFrom;
//     this.importSource = importSource;
//   }
// }
// globalThis.addEventListener('scroll', () => {
//   // Total height of the document
//   const totalPageHeight = document.body.scrollHeight;
//   // Current scroll position
//   const scrollPoint = globalThis.scrollY + globalThis.innerHeight;
//   // Check if scrolled to bottom
//   if (scrollPoint >= totalPageHeight) {
//     // console.log.apply(null, log('Scrolled to the bottom!'));
//     // console.log.apply(null, log(scrollPoint, totalPageHeight));
//   }
// });
// // let peer = await new PeerConnection(peer_id);
// // let connectionReply = await wsConnection.send('hello');
// // for (let peer of connectionReply) {
// //   let peerConnection = await wsConnection.send('connect', peer.id);
// //   if (peerConnection) {
// //     this.peers.push(peerConnection);
// //     let postIDs = await peerConnection.getPostIDs();
// //     let postsWeDontHave = this.diffPostIDs(postIDs);
// //     let newPosts = await peerConnection.getPosts(postsWeDontHave);
// //     this.addPosts(newPosts);
// //   }
// // }
// async function bytesToBase64DataUrl(bytes: Uint8Array, type = "application/octet-stream") {
//   return await new Promise((resolve, reject) => {
//     const reader = Object.assign(new FileReader(), {
//       onload: () => resolve(reader.result),
//       onerror: () => reject(reader.error),
//     });
//     reader.readAsDataURL(new File([bytes], "", { type }));
//   });
// }
// async function arrayBufferToBase64(buffer: ArrayBuffer) {
//   var bytes = new Uint8Array(buffer);
//   return (await bytesToBase64DataUrl(bytes) as string).replace("data:application/octet-stream;base64,", "");
// }
// async function base64ToArrayBuffer(base64String: string) {
//   let response = await fetch("data:application/octet-stream;base64," + base64String);
//   let arrayBuffer = await response.arrayBuffer();
//   return arrayBuffer;
// }
// async function compressString(input: string) {
//   // Convert the string to a Uint8Array
//   const textEncoder = new TextEncoder();
//   const inputArray = textEncoder.encode(input);
//   // Create a CompressionStream
//   const compressionStream = new CompressionStream('gzip');
//   const writer = compressionStream.writable.getWriter();
//   // Write the data and close the stream
//   writer.write(inputArray);
//   writer.close();
//   // Read the compressed data from the stream
//   const compressedArray = await new Response(compressionStream.readable).arrayBuffer();
//   // Convert the compressed data to a Uint8Array
//   return new Uint8Array(compressedArray);
// }
// interface PeerMessage {
//   type: string;
//   from: string;
//   to: string;
//   from_peername: string;
//   from_username: string;
//   message: any;
// }
// // class Signaler {
// //   websocket: WebSocket | null = null;
// //   websocketPingInterval: number = 0;
// //   connect() {
// //   if (this.websocket?.readyState === WebSocket.OPEN) {
// //     return;
// //   }
// //   globalThis.clearInterval(this.websocketPingInterval);
// //   if (this.websocket) { this.websocket.close() };
// //   try {
// //     this.websocket = new WebSocket(`wss://${globalThis.location.hostname}:${globalThis.location.port}/ws`);
// //   } catch (error: any) {
// //     console.log.apply(null, log(error.message);
// //     return;
// //   }
// //   this.websocket.onopen = async (event) => {
// //   console.log.apply(null,   log("ws:connected"));;
// //     await this.sendHello();
// //     // If we're running as a headless peer, send a hello message every N seconds to refresh the posts we have.
// //     let helloRefreshIntervalPeriod = 120;
// //     if (app.isHeadless) {
// //       console.log.apply(null, log("wsConnection: Setting hello refresh interval to ", helloRefreshIntervalPeriod)
// //       this.helloRefreshInterval = globalThis.setInterval(() => {
// //         console.log.apply(null, log("wsConnection: Hello refresh.")
// //         if (!navigator.onLine) {
// //           return;
// //         }
// //         this.sendHello();
// //       }, helloRefreshIntervalPeriod * 1000);
// //     }
// //     this.websocketPingInterval = globalThis.setInterval(() => {
// //       if (!navigator.onLine) {
// //         return;
// //       }
// //       this.send({ type: "ping", peer_id: this.peerID, peer_name: app.peername, user_id: app.userID, user_name: app.username });
// //     }, 10_000)
// //   };
// //   this.websocket.onclose = (event) => {
// //   console.log.apply(null,   log("ws:disconnected"));;
// //     // this.retry *= 2;
// //   console.log.apply(null,   log(`Retrying in ${this.retry} seconds`));;
// //     globalThis.setTimeout(() => { this.connect(); }, this.retry * 1000);
// //   };
// //   this.websocket.onmessage = (event) => {
// //     // log('ws:<-' + event.data.slice(0, 240));
// //     let data = JSON.parse(event.data);
// //     let { type } = data;
// //     let handler = this.messageHandlers.get(type);
// //     if (!handler) {
// //       console.warn(`Got a message we can't handle:`, type);
// //       return;
// //     }
// //     handler(data);
// //   };
// //   this.websocket.onerror = (event) => {
// //   console.log.apply(null,   log('ws:error: ' + event));;
// //   };
// // }
// // }
// // disconnect() {
// //   this.websocket?.close();
// // }
// // }
// // Connect websocket
// // send hello
// // get bootstrap peer ID
// // WebRTC connect to bootstrap peer
// // ask Bootstrap peer for peers that have users we care about.
// // for now, bootstrap peer will connect to all peers and will tell us about them, moving all logic from the server to the BSP
// // WebRTC Connect to peers that might have posts we need
// // query those peers and do existing logic.
// class wsConnection {
//   websocket: WebSocket | null = null;
//   sessionID = "";
//   userID = "";
//   peerID = "";
//   rtcPeerDescription: RTCSessionDescription | null = null;
//   UserIDsToSync: Set<string>;
//   websocketPingInterval: number = 0;
//   helloRefreshInterval: number = 0;
//   retry = 10;
//   state = 'disconnected';
//   // peers: Map<string, string[]> = new Map();
//   messageHandlers: Map<string, (event: any) => void> = new Map();
//   peerMessageHandlers: Map<string, (data: any) => void> = new Map();
//   seenPeers: Map<string, any> = new Map();
//   constructor(userID: string, peerID: string, IDsToSync: Set<string>, rtcPeerDescription: RTCSessionDescription) {
//     this.rtcPeerDescription = rtcPeerDescription;
//     this.sessionID = generateID();
//     this.userID = userID;
//     this.peerID = peerID;
//     this.UserIDsToSync = new Set(IDsToSync);
//     this.messageHandlers.set('hello', this.helloResponseHandler.bind(this));
//     this.messageHandlers.set('hello2', this.hello2ResponseHandler.bind(this));
//     this.messageHandlers.set('pong', this.pongHandler);
//     this.messageHandlers.set('peer_message', this.peerMessageHandler.bind(this));
//     // 
//     this.peerMessageHandlers.set('get_post_ids_for_user', this.getPostIdsForUserHandler.bind(this));
//     this.peerMessageHandlers.set('get_post_ids_for_user_response', this.getPostIdsForUserResponseHandler.bind(this));
//     this.peerMessageHandlers.set('get_posts_for_user', this.getPostsForUserHandler.bind(this));
//     this.peerMessageHandlers.set('get_posts_for_user_response', this.getPostsForUserReponseHandler.bind(this));
//     // this.peerMessageHandlers.set('send_webrtc_offer', this.sendWebRTCOfferHandler.bind(this));
//     // this.peerMessageHandlers.set('send_webrtc_offer_response', this.getPostIdsForUserResponseHandler.bind(this));
//     globalThis.addEventListener('beforeunload', () => this.disconnect());
//     this.connect();
//   }
//   // So we don't need custom logic everywhere we use this, I just wrapped it.
//   shouldSyncUserID(userID: string) {
//     if (app.isHeadless) {
//       return true;
//     }
//     return this.UserIDsToSync.has(userID);
//   }
//   async send(message: any) {
//     let json = ""
//     try {
//       json = JSON.stringify(message);
//       // console.log.apply(null, log("*******", (await compressString(json)).byteLength, json.length);
//     } catch (e) {
//       console.log.apply(null, log(e, "wsConnection send: Couldn't serialize message", message));
//     }
//     // log(`ws->${json.slice(0, 240)}`)
//     this.websocket!.send(json);
//   }
//   pongHandler(data: any) {
//   }
//   async sendWebRTCDescription(description: RTCSessionDescription | null) {
//     console.log.apply(null, log("description:", description));
//     this.send({ type: "rtc_session_description", description: description });
//   }
//   async getPostIdsForUserResponseHandler(data: any) {
//     // log(`getPostsForUserResponse: ${data}`)
//     let message = data.message;
//   console.log.apply(null,   log(`Net: got ${message.post_ids.length} post IDs for user ${logID(message.user_id)} from peer ${logID(data.from)}`));;
//     let startTime = app.timerStart();
//     let postIds = await checkPostIds(message.user_id, message.post_ids);
//   console.log.apply(null,   log(`ID Check for user ${logID(message.user_id)} took ${app.timerDelta().toFixed(2)}ms`));;
//   console.log.apply(null,   log(`Need ${postIds.length} posts for user ${logID(message.user_id)} from peer ${logID(data.from)}`));;
//     if (postIds.length === 0) {
//       return;
//     }
//   console.log.apply(null,   log(`Net: Req ${postIds.length} posts for user ${logID(message.user_id)} from peer ${logID(data.from)}`));
//     let responseMessage = { type: "peer_message", from: app.peerID, to: data.from, from_username: app.username, from_peername: app.peername, message: { type: "get_posts_for_user", post_ids: postIds, user_id: message.user_id } }
//     this.send(responseMessage);
//   }
//   //   static async compressArrayBuffer(data: ArrayBuffer): Promise<ArrayBuffer> {
//   //     const compressionStream = new CompressionStream('gzip'); // You can also use 'deflate', 'deflate-raw', etc.
//   //     const compressedStream = new Response(
//   //         new Blob([data]).stream().pipeThrough(compressionStream)
//   //     );
//   //     const compressedArrayBuffer = await compressedStream.arrayBuffer();
//   //     return compressedArrayBuffer;
//   // }
//   postBlockList = new Set([
//     '1c71f53c-c467-48e4-bc8c-39005b37c0d5',
//     '64203497-f77b-40d6-9e76-34d17372e72a',
//     '243130d8-4a41-471e-8898-5075f1bd7aec',
//     'e01eff89-5100-4b35-af4c-1c1bcb007dd0',
//     '194696a2-d850-4bb0-98f7-47416b3d1662',
//     'f6b21eb1-a0ff-435b-8efc-6a3dd70c0dca',
//     'dd1d92aa-aa24-4166-a925-94ba072a9048'
//   ]);
//   async getPostIdsForUserHandler(data: any) {
//     let message = data.message;
//     let postIds = await getAllIds(message.user_id) ?? [];
//     postIds = postIds.filter((postID: string) => !this.postBlockList.has(postID));
//     if (postIds.length === 0) {
//     console.log.apply(null,   log(`Net: I know about user ${logID(message.user_id)} but I have 0 posts, so I'm not sending any to to peer ${logID(data.from)}`));;
//       return;
//     }
//   console.log.apply(null,   log(`Net: Sending ${postIds.length} post Ids for user ${logID(message.user_id)} to peer ${logID(data.from)}`));
//     let responseMessage = { type: "peer_message", from: app.peerID, to: data.from, from_username: app.username, from_peername: app.peername, message: { type: "get_post_ids_for_user_response", post_ids: postIds, user_id: message.user_id } }
//     this.send(responseMessage);
//   }
//   async broadcastNewPost(userID: string, post: any) {
//     let newPost = { ...post }
//     if (post.image_data) {
//       newPost.image_data = await arrayBufferToBase64(post.image_data);
//     }
//     for (let [peerID, peerInfo] of this.seenPeers.entries()) {
//     console.log.apply(null,   log(`broadcastNewPost: sending new post to ${logID(peerID)}:${peerInfo.peerName}:${peerInfo.userName}`));;
//       this.sendPostsForUser(peerID, app.userID, [newPost])
//     }
//   }
//   async sendPostsForUser(toPeerID: string, userID: string, posts: any) {
//     let responseMessage = {
//       type: "peer_message",
//       from: app.peerID,
//       to: toPeerID,
//       from_username: app.username,
//       from_peername: app.peername,
//       message: {
//         type: "get_posts_for_user_response",
//         posts: posts,
//         user_id: userID
//       }
//     }
//     return this.send(responseMessage);
//   }
//   // Send posts to peer
//   async getPostsForUserHandler(data: any) {
//     let message = data.message;
//     let posts = await getPostsByIds(message.user_id, message.post_ids) ?? [];
//   console.log.apply(null,   log(`Net: Sending ${posts.length} posts for user ${logID(message.user_id)} to peer ${logID(data.from)}`));;
//     app.timerStart();
//     let output = [];
//     console.log.apply(null, log("Serializing images"));
//     for (let post of posts) {
//       let newPost = (post as any).data;
//       if (newPost.image_data) {
//         // let compressedData = await wsConnection.compressArrayBuffer(newPost.image_data);
//         // console.log.apply(null, log((newPost.image_data.byteLength - compressedData.byteLength) / 1024 / 1024);
//         // TODO don't do this, use Blobs direclty!
//         // https://developer.chrome.com/blog/blob-support-for-Indexeddb-landed-on-chrome-dev
//         newPost.image_data = await arrayBufferToBase64(newPost.image_data);
//       }
//       // let megs = JSON.stringify(newPost).length/1024/1024;
//       // console.log.apply(null, log(`getPostsForUserHandler id:${newPost.post_id} post length:${megs}`);
//       output.push(newPost);
//     }
//     let responseMessage = { type: "peer_message", from: app.peerID, to: data.from, from_username: app.username, from_peername: app.peername, message: { type: "get_posts_for_user_response", posts: output, user_id: message.user_id } }
//     console.log.apply(null, log("Sending posts"));
//     await this.sendPostsForUser(data.from, message.user_id, output);
//     let sendTime = app.timerDelta();
//   console.log.apply(null,   log(`getPostsForUserHandler send took: ${sendTime.toFixed(2)}ms`));;
//   }
//   // Got posts from peer
//   async getPostsForUserReponseHandler(data: any) {
//     app.timerStart();
//     let message = data.message;
//     console.log.apply(null, log(`Net: got ${message.posts.length} posts for user ${logID(message.user_id)} from peer ${logID(data.from)}`));
//     for (let post of message.posts) {
//       // HACK: Some posts have insanely large images, so I'm gonna skip them.
//       // Once we support delete then we we could delete these posts in a sensible way.
//       if (this.postBlockList.has(post.post_id)) {
//       console.log.apply(null,   log(`Skipping blocked post: ${post.post_id}`));;
//         continue;
//       }
//       // HACK - some posts had the wrong author ID
//       if (message.user_id === app.userID) {
//         post.author_id = app.userID;
//       }
//       post.post_timestamp = new Date(post.post_timestamp);
//       if (post.image_data) {
//         post.image_data = await base64ToArrayBuffer(post.image_data);
//       }
//     }
//     console.log.apply(null, log(`Merging same user peer posts...`));
//     await mergeDataArray(message.user_id, data.message.posts);
//     let receiveTime = app.timerDelta();
//   console.log.apply(null,   log(`getPostsForUserReponseHandler receive took: ${receiveTime.toFixed(2)}ms`));;
//     if (message.user_id === app.getPreferentialUserID() || app.following.has(message.user_id)) {
//       app.render();
//     }
//   }
//   async peerMessageHandler(data: PeerMessage) {
//     // log(`peerMessageHandler ${JSON.stringify(data)}`)
//     this.seenPeers.set(data.from, { peerName: data.from_peername, userName: data.from_username });
//     let peerMessageType = data.message.type;
//     let handler = this.peerMessageHandlers.get(peerMessageType);
//     if (!handler) {
//       console.error(`got peer message type we don't have a handler for: ${peerMessageType}`);
//       return;
//     }
//     handler(data);
//   }
//   userBlockList = new Set([
//     '5d63f0b2-a842-41bf-bf06-e0e4f6369271',
//     '5f1b85c4-b14c-454c-8df1-2cacc93f8a77',
//     // 'bba3ad24-9181-4e22-90c8-c265c80873ea'
//   ])
//   // Hello2
//   // Goal, connect to bootstrap peer, ask bootstrap peer for peers that have posts from users that we care about. get peers, connect to those peers, sync.
//   // how? do "perfect negotiation" with bootstrap peer. All logic here moves to BP.
//   async sendHello2() {
//     this.send({
//       type: "hello2",
//       user_id: this.userID,
//       user_name: app.username,
//       peer_id: this.peerID,
//       session_id: this.sessionID,
//       peer_name: app.peername,
//       is_bootstrap_peer: app.isBootstrapPeer,
//       peer_description: this.rtcPeerDescription
//     });
//   }
//   async sendHello() {
//     // TODO only get users you're following here. ✅
//     let knownUsers = [...(await indexedDB.databases())].map(db => db.name?.replace('user_', '')).filter(userID => userID !== undefined);
//     knownUsers = knownUsers
//       .filter(userID => this.shouldSyncUserID(userID))
//       .filter(userID => !this.userBlockList.has(userID))
//       .filter(async userID => (await getAllIds(userID)).length > 0); // TODO:EASYOPT getting all the IDs is unecessary, replace it with a test to get a single ID.
//     console.log.apply(null, log('Net: Sending known users', knownUsers.map(userID => logID(userID ?? ""))));
//     return await this.send({ type: "hello", user_id: this.userID, user_name: app.username, peer_id: this.peerID, peer_name: app.peername, known_users: knownUsers });
//   }
//   hello2ResponseHandler(data: any) {
//   }
//   helloResponseHandler(data: any) {
//     let users = [];
//     let receivedUsers = Object.entries(data.userPeers);
//   console.log.apply(null,   log(`Net: got ${receivedUsers.length} users from bootstrap peer.`));
//     try {
//       let preferentialUserID = app.getPreferentialUserID();
//       let currentUserPeers = data.userPeers[preferentialUserID];
//       users.push([preferentialUserID, currentUserPeers]);
//       delete data.userPeers[preferentialUserID];
//     } catch (e) {
//       console.log.apply(null, log('helloResponseHandler', e));
//     }
//     let getAllUsers = app.router.route !== App.Route.USER
//     if (getAllUsers) {
//       users = [...users, ...Object.entries(data.userPeers).filter(userID => this.shouldSyncUserID(userID[0]))];
//     }
//     // log(`Net: got ${users.length} users from bootstrap peer. \n${users.map((user)=>user[0]).join('\n')}`)
//     for (let [userID, peerIDs] of users) {
//       if (this.userBlockList.has(userID)) {
//         console.log.apply(null, log("Skipping user on blocklist:", userID));
//         continue;
//       }
//       // this.peers.set(userID, [...peerIDs]);
//       for (let peerID of [...peerIDs]) {
//         if (peerID === this.peerID) {
//           continue;
//         }
//       console.log.apply(null,   log(`Net: Req post IDs for user ${logID(userID)} from peer ${logID(peerID)}`));;
//         this.send({
//           type: "peer_message",
//           from: this.peerID,
//           from_username: app.username,
//           from_peername: app.peername,
//           to: peerID,
//           message: { type: "get_post_ids_for_user", user_id: userID }
//         })
//       }
//     }
//   }
//   connect(): void {
//     if (this.websocket?.readyState === WebSocket.OPEN) {
//       return;
//     }
//     globalThis.clearInterval(this.websocketPingInterval);
//     if (this.websocket) { this.websocket.close() };
//     try {
//       this.websocket = new WebSocket(`wss://${globalThis.location.hostname}:${globalThis.location.port}/ws`);
//     } catch (error: any) {
//       console.log.apply(null, log(error.message));
//       return;
//     }
//     this.websocket.onopen = async (event) => {
//     console.log.apply(null,   log("ws:connected"));;
//       await this.sendHello2();
//       // If we're running as a headless peer, send a hello message every N seconds to refresh the posts we have.
//       // let helloRefreshIntervalPeriod = 120;
//       // if (app.isHeadless) {
//       //   console.log.apply(null, log("wsConnection: Setting hello refresh interval to ", helloRefreshIntervalPeriod)
//       //   this.helloRefreshInterval = globalThis.setInterval(() => {
//       //     console.log.apply(null, log("wsConnection: Hello refresh.")
//       //     if (!navigator.onLine) {
//       //       return;
//       //     }
//       //     this.sendHello();
//       //   }, helloRefreshIntervalPeriod * 1000);
//       // }
//       this.websocketPingInterval = globalThis.setInterval(() => {
//         if (!navigator.onLine) {
//           return;
//         }
//         this.send({ type: "ping", peer_id: this.peerID, peer_name: app.peername, user_id: app.userID, user_name: app.username });
//       }, 10_000)
//     };
//     // this.websocket.onopen = async (event) => {
//     // console.log.apply(null,   log("ws:connected"));;
//     //   await this.sendHello();
//     //   // If we're running as a headless peer, send a hello message every N seconds to refresh the posts we have.
//     //   let helloRefreshIntervalPeriod = 120;
//     //   if (app.isHeadless) {
//     //     console.log.apply(null, log("wsConnection: Setting hello refresh interval to ", helloRefreshIntervalPeriod)
//     //     this.helloRefreshInterval = globalThis.setInterval(() => {
//     //       console.log.apply(null, log("wsConnection: Hello refresh.")
//     //       if (!navigator.onLine) {
//     //         return;
//     //       }
//     //       this.sendHello();
//     //     }, helloRefreshIntervalPeriod * 1000);
//     //   }
//     //   this.websocketPingInterval = globalThis.setInterval(() => {
//     //     if (!navigator.onLine) {
//     //       return;
//     //     }
//     //     this.send({ type: "ping", peer_id: this.peerID, peer_name: app.peername, user_id: app.userID, user_name: app.username });
//     //   }, 10_000)
//     // };
//     this.websocket.onclose = (event) => {
//     console.log.apply(null,   log("ws:disconnected"));;
//       // this.retry *= 2;
//     console.log.apply(null,   log(`Retrying in ${this.retry} seconds`));;
//       globalThis.setTimeout(() => { this.connect(); }, this.retry * 1000);
//     };
//     this.websocket.onmessage = (event) => {
//       // log('ws:<-' + event.data.slice(0, 240));
//       let data = JSON.parse(event.data);
//       let { type } = data;
//       let handler = this.messageHandlers.get(type);
//       if (!handler) {
//         console.warn(`Got a message we can't handle:`, type);
//         return;
//       }
//       handler(data);
//     };
//     this.websocket.onerror = (event) => {
//     console.log.apply(null,   log('ws:error: ' + event));;
//     };
//   }
//   disconnect() {
//     this.websocket?.close();
//   }
// }
class App {
    constructor() {
        this.username = '';
        this.peername = '';
        this.userID = '';
        this.peerID = '';
        this.following = new Set();
        //   posts: StoragePost[] = [];
        this.isHeadless = false;
        this.isBootstrapPeer = false;
        this.showLog = false;
        this.markedAvailable = false;
        this.limitPosts = 50;
        //   websocket: wsConnection | null = null;
        // vizGraph: any | null = null;
        this.qrcode = null;
        this.connectURL = "";
        this.firstRun = false;
        this.peerManager = null;
        this.animals = ['shrew', 'jerboa', 'lemur', 'weasel', 'possum', 'possum', 'marmoset', 'planigale', 'mole', 'narwhal'];
        this.adjectives = ['snazzy', 'whimsical', 'jazzy', 'bonkers', 'wobbly', 'spiffy', 'chirpy', 'zesty', 'bubbly', 'perky', 'sassy'];
        this.snakes = ['mamba', 'cobra', 'python', 'viper', 'krait', 'sidewinder', 'constrictor', 'boa', 'asp', 'anaconda', 'krait'];
        // this.registerRPCs();
        // this.testPeerManager();
        //     // let peer: RTCPeerConnection | null = null;
        //     // //  if (globalThis.RTCPeerConnection) {
        //     // peer = new RTCPeerConnection({
        //     //   iceServers: [
        //     //     { urls: "stun:ddln.app" },
        //     //     // { urls: "turn:ddln.app", username: "a", credential: "b" },
        //     //     { urls: "stun:stun.l.google.com" }, // keeping this for now as my STUN server is not return ipv6
        //     //     //  { urls: "stun:stun1.l.google.com" },
        //     //     //  { urls: "stun:stun2.l.google.com" },
        //     //     //  { urls: "stun:stun3.l.google.com" },
        //     //     //  { urls: "stun:stun4.l.google.com" },
        //     //   ]
        //     // });
        //     // peer.createDataChannel('boop');
        //     // peer.onicecandidate = ({ candidate }) => { log(`WRTC:${candidate?.address} ${candidate?.protocol} ${candidate?.type} ${(candidate as any)?.url}`) };
        //     // peer.onnegotiationneeded = async (event) => {
        //     // console.log.apply(null,   log("on negotiation needed fired"));;
        //     //   let makingOffer = false;
        //     //   try {
        //     //     makingOffer = true;
        //     //     await peer.setLocalDescription();
        //     //     let IDsToSync = this.following;
        //     //     if (this.router.route === App.Route.USER) {
        //     //       IDsToSync = new Set([this.router.userID]);
        //     //     }
        //     //     if (!peer.localDescription) {
        //     //       return;
        //     //     }
        //     //     // this.websocket = new wsConnection(this.userID, this.peerID, IDsToSync, peer.localDescription);
        //     //     // log(peer.localDescription.type + ":" + peer.localDescription.sdp);
        //     //     // this.initOffline(this.websocket);
        //     //     // this.websocket?.sendWebRTCDescription(peer.localDescription);
        //     //   } catch (err) {
        //     //     console.error(err);
        //     //   } finally {
        //     //     makingOffer = false;
        //     //   }
        //     // }
        //     // peer.createOffer().then((description)=>{
        //     //   peer.setLocalDescription(description)
        //     // console.log.apply(null,   log("RTC: " + description.sdp + description.type));;
        //     // });
        //     //   }
        //     // await this.exportPostsForUser('b38b623c-c3fa-4351-9cab-50233c99fa4e');
        //     // Get initial state and route from URL and user agent etc
        //     // Set local state (userid etc) based on that.
        //     // Init libraries
        //     // Render
        //     // Load all images async
        //     // Start the process of figuring out what posts we need
        //     // Download posts once all current images are loaded
        //     // globalThis.resizeTo(645, 900);
        //     // this.initLogo()
        //     this.getRoute();
        //     if (this.router.route === App.Route.CONNECT) {
        //       console.log.apply(null, log('connect', this.router.userID));
        //       localStorage.setItem("dandelion_id", this.router.userID);
        //       localStorage.removeItem("dandelion_username");
        //     }
        //     await this.initDB();
        //     this.connectURL = `${document.location.origin}/connect/${this.userID}`;
        //     document.getElementById('connectURL')!.innerHTML = `<a href="${this.connectURL}">connect</a>`;
        //     this.isHeadless = urlParams.has('headless');
        //     let limitPostsParam = urlParams.get('limitPosts');
        //     if (limitPostsParam) {
        //       this.limitPosts = parseInt(limitPostsParam);
        //     }
        //     let time = 0;
        //     let delta = 0;
        //     // let isPersisted = await navigator?.storage?.persisted();
        //     // if (!isPersisted) {
        //     //   debugger;
        //     //   const isPersisted = await navigator.storage.persist();
        //     // console.log.apply(null,   log(`Persisted storage granted: ${isPersisted}`));;
        //     // }
        //     // log(`Persisted: ${(await navigator?.storage?.persisted())?.toString()}`);
        //     this.initMarkdown();
        //     // let main = await fetch("/main.js");
        //     // let code = await main.text();
        //     //   console.log.apply(null, log(code);
        //     // registration.active.postMessage({type:"updateMain", code:code});
        //     // this.posts = await this.loadPosts(userID) ?? [];
        //     // debugger;
        //     await this.render(); // , (postID:string)=>{this.deletePost(userID, postID)}
        //     if ((performance as any)?.memory) {
        //     console.log.apply(null,   log(`memory used: ${((performance as any).memory.usedJSHeapSize / 1024 / 1024).toFixed(2)}Mb`));
        //     }
        //     // if (navigator?.storage) {
        //     //   let storageUsed = (await navigator?.storage?.estimate())?.usage/1024/1024
        //     // }
        //     // if (urlParams.get("sw") === "true") {
        //     let registration;
        //     registration = await this.registerServiceWorker();
        //     // }
        //     document.getElementById('username')!.innerText = `${this.username}`;
        //     document.getElementById('peername')!.innerText = `peername:${this.peername}`;
        //     document.getElementById('user_id')!.innerText = `user_id:${this.userID}`;
        //     document.getElementById('peer_id')!.innerText = `peer_id:${this.peerID}`;
        //     this.initButtons(this.userID, this.posts, registration);
        //   console.log.apply(null,   log(`username:${this.username} user:${this.userID} peername:${this.peername} peer:${this.peerID}`));;
        //     // await this.purgeEmptyUsers();
        //     // this.listUsers()
        //     // this.createNetworkViz();
        //     // const client = new WebTorrent()
        //     // // Sintel, a free, Creative Commons movie
        //     // const torrentId = 'magnet:?xt=urn:btih:6091e199a8d9272a40dd9a25a621a5c355d6b0be&dn=WING+IT!+-+Blender+Open+Movie+1080p.mp4&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337&tr=udp%3A%2F%2Fexplodie.org%3A6969&tr=udp%3A%2F%2Ftracker.empire-js.us%3A1337';
        //     // client.add(torrentId, function (torrent: any) {
        //     //   // Torrents can contain many files. Let's use the .mp4 file
        //     //   const file = torrent.files.find(function (file: any) {
        //     //     return file.name.endsWith('.mp4')
        //     //   })
        //     //   // Display the file by adding it to the DOM.
        //     //   // Supports video, audio, image files, and more!
        //     //   file.appendTo(document.getElementById('torrent-content'));
        //     // })
        //   }
        //   renderWelcome(contentDiv: HTMLDivElement) {
        //     contentDiv.innerHTML = `<div style="font-size:24px">
        //       Welcome to Dandelion v0.1!<br>
        //       Loading posts for the default feed...
        //     </div>
        //     `;
        //   }
        //   // keep a map of posts to dom nodes.
        //   // on re-render
        //   // posts that are not in our list that we need at add
        //   // posts that are in our list that we need to remove
        //   private renderedPosts = new Map();
        //   async render() {
        //     if (this.isHeadless) {
        //       console.log.apply(null, log('Headless so skipping render...'));
        //       return;
        //     }
        //     performance.mark("render-start");
        //     this.timerStart();
        //     let existingPosts = this.posts;
        //     this.posts = [];
        //     switch (this.router.route) {
        //       case App.Route.HOME:
        //       case App.Route.CONNECT: {
        //         this.following = new Set(await this.loadFollowersFromStorage(this.userID) ?? []);
        //         this.posts = await this.getPostsForFeed();
        //         // this.posts = await this.loadPostsFromStorage(this.userID) ?? [];
        //         // let compose = document.getElementById('compose');
        //         // if (!compose) {
        //         //   break;
        //         // }
        //         // compose.style.display = "block";
        //         break;
        //       }
        //       case App.Route.USER: {
        //         this.posts = await this.loadPostsFromStorage(this.router.userID) ?? [];
        //         let compose = document.getElementById('compose');
        //         if (!compose) {
        //           break;
        //         }
        //         compose.style.display = "none";
        //         break;
        //       }
        //       case App.Route.POST: {
        //         this.posts = await this.loadPostsFromStorage(this.router.userID, this.router.postID) ?? [];
        //         let compose = document.getElementById('compose');
        //         if (!compose) {
        //           break;
        //         }
        //         compose.style.display = "none";
        //         break;
        //       }
        //       default: {
        //         console.log.apply(null, log("Render: got a route I didn't understand. Rendering HOME:", this.router.route));
        //         this.posts = await this.loadPostsFromStorage(this.userID) ?? [];
        //         break;
        //       }
        //     }
        //     let contentDiv = document.getElementById("content");
        //     if (!contentDiv) {
        //       throw new Error();
        //     }
        //     if (this.posts.length === 0) {
        //       this.renderWelcome(contentDiv as HTMLDivElement);
        //       return;
        //     }
        //     // let existingPostSet = new Set(existingPosts.map(post => post.post_id));
        //     // let incomingPostSet = new Set(this.posts.map(post => post.post_id));
        //     // let addedPosts = [];
        //     // for (let post of this.posts) {
        //     //   if (!existingPostSet.has(post.post_id)) {
        //     //     addedPosts.push(post);
        //     //   }
        //     // }
        //     // let deletedPosts = [];
        //     // for (let post of existingPosts) {
        //     //   if (!incomingPostSet.has(post.post_id)) {
        //     //     deletedPosts.push(post);
        //     //   }
        //     // }
        //     // console.log.apply(null, log("added:", addedPosts, "removed:", deletedPosts);
        //     const fragment = document.createDocumentFragment();
        //     contentDiv.innerHTML = "";
        //     let count = 0;
        //     this.renderedPosts.clear();
        //     let first = true;
        //     for (let i = this.posts.length - 1; i >= 0; i--) {
        //       let postData = this.posts[i];
        //       // this.postsSet.add(postData);
        //       // TODO return promises for all image loads and await those.
        //       let post = this.renderPost(postData.data, first);
        //       first = false;
        //       // this.renderedPosts.set(postData.post_id, post);
        //       if (post) {
        //         fragment.appendChild(post);
        //         count++;
        //       }
        //       if (count > this.limitPosts) {
        //         break;
        //       }
        //     }
        //     if (!contentDiv) {
        //       throw new Error("Couldn't get content div!");
        //     }
        //     contentDiv.appendChild(fragment);
        //     let renderTime = this.timerDelta();
        //   console.log.apply(null,   log(`render took: ${renderTime.toFixed(2)}ms`));;
        //     performance.mark("render-end");
        //     performance.measure('render-time', 'render-start', 'render-end');
        //     // if ((performance as any)?.memory) {
        //     // console.log.apply(null,   log(`memory used: ${((performance as any).memory.usedJSHeapSize / 1024 / 1024).toFixed(2)}Mb`));
        //     // }
        //   }
        //   async deletePost(userID: string, postID: string) {
        //     deleteData(userID, postID)
        //     this.render();
        //   }
        //   renderPost(post: Post, first: boolean) {
        //     if (!(post.hasOwnProperty("text"))) {
        //       throw new Error("Post is malformed!");
        //     }
        //     let containerDiv = document.createElement("div");
        //     let timestamp = `${post.post_timestamp.toLocaleTimeString()} · ${post.post_timestamp.toLocaleDateString()}`;
        //     let deleteButton = document.createElement('button'); deleteButton.innerText = 'delete';
        //     deleteButton.onclick = () => { this.deletePost(post.author_id, post.post_id) };
        //     // let editButton = document.createElement('button'); editButton.innerText = 'edit';
        //     let shareButton = document.createElement('button'); shareButton.innerText = 'share';
        //     shareButton.onclick = async () => {
        //       let shareUrl = `${document.location.origin}/user/${post.author_id}/post/${post.post_id}`;
        //       await navigator.clipboard.writeText(shareUrl)
        //     };
        //     let ownPost = post.author_id === this.userID;
        //     let markdown = post.text;
        //     if (this.markedAvailable) {
        //       markdown = marked.parse(post.text);
        //     }
        //     // if (markdown.includes("<iframe")) {
        //     //   debugger;
        //     // }
        //     // markdown = this.replaceIframeWithDiv(markdown);
        //     if (markdown.includes("<iframe") && markdown.includes(`src="https://dotbigbang`)) {
        //       markdown = markdown.replace("<iframe", `<iframe style="width:100%;height:50px;display:none" onblur="this.style.display = 'inline';"`);
        //     }
        //     let userURL = `${document.location.origin}/user/${post.author_id}/`
        //     let postTemplate =
        //       `<div>${first ? '' : '<hr>'}
        //         <div>
        //           <span class='header' title='${timestamp}'><img class="logo" src="/static/favicon.ico"><a class="username" href="${userURL}">@${post.author}</a> -
        //             <span style="color:rgb(128,128,128)">${post.post_timestamp.toLocaleDateString()}</span>
        //           </span>
        //           ${ownPost ? `<span id="deleteButton"></span>` : ''}
        //           ${ownPost ? `<span id="editButton"></span>` : ''}
        //           <span id="shareButton"></span>
        //         </div>
        //         <div>${markdown}</div>
        //       </div>`
        //     containerDiv.innerHTML = postTemplate;
        //     if (ownPost) {
        //       containerDiv.querySelector('#deleteButton')?.appendChild(deleteButton);
        //       // containerDiv.querySelector('#editButton')?.appendChild(editButton);
        //     }
        //     containerDiv.querySelector('#shareButton')?.appendChild(shareButton);
        //     if (!("image_data" in post && post.image_data)) {
        //       // containerDiv.appendChild(timestampDiv);
        //       return containerDiv;
        //       // return null;
        //     }
        //     let image = document.createElement("img");
        //     image.title = `${(post.image_data.byteLength / 1024 / 1024).toFixed(2)}MBytes`;
        //     // const blob = new Blob([post.image_data as ArrayBuffer], { type: 'image/png' });
        //     const blob = new Blob([post.image_data as ArrayBuffer]);
        //     const url = URL.createObjectURL(blob);
        //     image.onload = () => {
        //       URL.revokeObjectURL(url);
        //     };
        //     image.src = url;
        //     // image.src = image.src = "data:image/png;base64," + post.image;
        //     image.className = "postImage";
        //     // image.onclick = () => { App.maximizeElement(image) };
        //     containerDiv.appendChild(image);
        //     // containerDiv.appendChild(timestampDiv);
        //     return containerDiv;
        //   }
        //   static maximizeElement(element: HTMLImageElement) {
        //     element.style.transform = "scale(2.0)"
        //   }
        //   router = {
        //     route: App.Route.HOME,
        //     userID: '',
        //     postID: '',
        //     mediaID: ''
        //   }
        //   getRoute() {
        //     let path = document.location.pathname;
        //     console.log.apply(null, log("router: path ", path));
        //     const regex = "(user/([a-zA-Z0-9\-]+)/?(post/([a-zA-Z0-9\-]+)?/?)?(media/([0-9]+)?)?)|(connect/([a-zA-Z0-9\-]+))";
        //     const match = path.match(new RegExp(regex));
        //     if (match) {
        //       if (match[8]) {  // Check for the connect route
        //         this.router.userID = match[8];
        //         this.router.route = App.Route.CONNECT;
        //       } else {
        //         this.router.userID = match[2];
        //         this.router.postID = match[4];
        //         this.router.mediaID = match[6];
        //         if (this.router.mediaID) {
        //           this.router.route = App.Route.MEDIA;
        //         } else if (this.router.postID) {
        //           this.router.route = App.Route.POST;
        //         } else {
        //           this.router.route = App.Route.USER;
        //         }
        //       }
        //     }
        //     console.log.apply(null, log("router: ", this.router.userID, this.router.postID, this.router.mediaID, App.Route[this.router.route]));
        //     // user = /user/<ID>
        //     // post = /user/<ID>/post/<ID>
        //     // media = /user/<ID>/post/<ID>/media/<index>
        //     // group = /group/ID/post/<ID>
        //     // hashtag = /hashtag/ -- maybe only hastags in groups
        //     // home = /
        //   }
        // }
        // namespace App {
        //   export enum Route {
        //     USER,
        //     POST,
        //     MEDIA,
        //     GROUP,
        //     HOME,
        //     CONNECT,
        //   };
        //   // export function connect() {
        //   //   throw new Error("Function not implemented.");
        //   // }
        //   // export function connect() {
        //   //   throw new Error("Function not implemented.");
        //   // }
    }
    async connect() {
        this.peerManager = new PeerManager(this.userID, this.peerID, this.isBootstrapPeer);
        this.registerRPCs();
        console.log.apply(null, log("*************** before peerManager.connect"));
        ;
        // We use promises here to only return from this call once we're connected to the boostrap peer
        // and the datachannel is open.
        // Might want to take this a step further and only return once we're connected to an initial set of peers?
        // we could return progress information as we connect and have the app subscribe to that?
        // Would be lovely to show a little display of peers connecting, whether you're connected directly to a friend's peer etc.
        // Basically that live "dandelion" display.
        this.peerManager.registerRPC('getPostIDsForUser', (userID) => {
            return [1, 2, 3, 4, 5];
        });
        await this.peerManager.connect();
        console.log.apply(null, log("*************** after peerManager.connect"));
        ;
        if (!this.isBootstrapPeer) {
            let postIDs = await this.peerManager.rpc.getPostIDsForUser(this.peerManager.bootstrapPeerID, this.userID);
            console.log.apply(null, log("peerManager.rpc.getPostIDsForUser", postIDs));
        }
    }
    //   getPreferentialUserID() {
    //     return this.router.userID.length !== 0 ? this.router.userID : this.userID;
    //   }
    //   initMarkdown() {
    //     if (typeof marked === "undefined") {
    //       return;
    //     }
    //     const renderer = new marked.Renderer();
    //     renderer.link = (href: any, title: string, text: string) => {
    //       return `<a href="${href}" target="_blank"${title ? ` title="${title}"` : ''}>${text}</a>`;
    //     };
    //     marked.setOptions({ renderer: renderer });
    //     this.markedAvailable = true;
    //   }
    // arrayBufferToBase64(buffer: ArrayBuffer) {
    //   return new Promise((resolve, reject) => {
    //     const blob = new Blob([buffer], { type: 'application/octet-stream' });
    //     const reader = new FileReader();
    //     reader.onloadend = () => {
    //       const dataUrl = reader.result as string;
    //       if (!dataUrl) {
    //         resolve(null);
    //         return;
    //       }
    //       const base64 = dataUrl.split(',')[1];
    //       resolve(base64);
    //     };
    //     reader.onerror = (error) => {
    //       reject(error);
    //     };
    //     reader.readAsDataURL(blob);
    //   });
    // }
    //   async createTestData() {
    //     let postsTestData = await (await fetch("./postsTestData.json")).json();
    //     return postsTestData;
    //   }
    //   time = 0;
    //   timerStart() {
    //     this.time = performance.now();
    //   }
    //   timerDelta() {
    //     return performance.now() - this.time;
    //   }
    //   getFixedTweetText(entry: any) {
    //     let fullText = entry.tweet.full_text;
    //     let linkMarkdown = "";
    //     for (const url of entry.tweet.entities.urls) {
    //       linkMarkdown = `[${url.display_url}](${url.expanded_url})`;
    //       fullText = fullText.replace(url.url, linkMarkdown);
    //     }
    //     return fullText
    //   }
    //   downloadBinary(data: ArrayBuffer, filename: string, mimeType: string = 'application/octet-stream') {
    //     // Create a blob from the ArrayBuffer with the specified MIME type
    //     const blob = new Blob([data], { type: mimeType });
    //     // Create object URL from the blob
    //     const url = globalThis.URL.createObjectURL(blob);
    //     // Create temporary link element
    //     const link = document.createElement('a');
    //     link.href = url;
    //     link.download = filename;
    //     // Append link to body, click it, and remove it
    //     document.body.appendChild(link);
    //     link.click();
    //     document.body.removeChild(link);
    //     // Clean up the object URL
    //     globalThis.URL.revokeObjectURL(url);
    //   }
    //   downloadJson(data: any, filename = 'data.json') {
    //     const jsonString = JSON.stringify(data);
    //     const blob = new Blob([jsonString], { type: 'application/json' });
    //     const url = globalThis.URL.createObjectURL(blob);
    //     const link = document.createElement('a');
    //     link.href = url;
    //     link.download = filename;
    //     document.body.appendChild(link);
    //     link.click();
    //     document.body.removeChild(link);
    //     globalThis.URL.revokeObjectURL(url);
    //   }
    //   async importPostsForUser(userID: string, posts: string) {
    //   }
    //   async exportPostsForUser(userID: string) {
    //     let posts = await getAllData(userID);
    //     let output = [];
    //     console.log.apply(null, log("Serializing images"));
    //     for (let post of posts) {
    //       let newPost = (post as any).data;
    //       if (newPost.image_data) {
    //         newPost.image_data = await arrayBufferToBase64(newPost.image_data);
    //       }
    //       output.push(newPost);
    //     }
    //     let compressedData = await compressString(JSON.stringify(output));
    //     const d = new Date();
    //     const timestamp = `${d.getFullYear()
    //       }_${String(d.getMonth() + 1).padStart(2, '0')
    //       }_${String(d.getDate()).padStart(2, '0')
    //       }_${String(d.getHours()).padStart(2, '0')
    //       }_${String(d.getMinutes()).padStart(2, '0')
    //       }_${String(d.getSeconds()).padStart(2, '0')}`;
    //     this.downloadBinary(compressedData, `ddln_${this.username}_export_${timestamp}.json.gz`);
    //   }
    //   async importTweetArchive(userID: string, tweetArchive: any[]) {
    //   console.log.apply(null,   log("Importing tweet archive"));
    //     let postsTestData: any[] = [];
    //     // let response = await fetch("./tweets.js");
    //     // let tweetsText = await response.text();
    //     // tweetsText = tweetsText.replace("globalThis.YTD.tweets.part0", "globalThis.tweetData");
    //     // new Function(tweetsText)();
    //     // let tweets = JSON.parse(tweetJSON);
    //     let count = 0;
    //     for (let entry of tweetArchive) {
    //       // if (entry.tweet.hasOwnProperty("in_reply_to_screen_name") || entry.tweet.retweeted || entry.tweet.full_text.startsWith("RT")) {
    //       //   continue;
    //       // }
    //       let mediaURL: string = entry.tweet?.entities?.media?.[0]?.media_url_https;
    //       let isImage = false;
    //       if (mediaURL) {
    //         isImage = mediaURL.includes('jpg');
    //       }
    //       let imageData = null;
    //       // if (isImage) {
    //       //   try {
    //       //     let response = await fetch(mediaURL);
    //       //     await waitMs(100);
    //       //     if (response.status === 200) {
    //       //       imageData = await response.arrayBuffer();
    //       //     }
    //       //     console.log.apply(null, log(imageData);
    //       //   } catch (e) {
    //       //     console.log.apply(null, log(e);
    //       //   }
    //       // }
    //       let timeStamp = new Date(entry.tweet.created_at);
    //       let tweetText = this.getFixedTweetText(entry);
    //       let newPost = new Post('bobbydigitales', userID, tweetText, timeStamp, imageData, 'twitter', entry);
    //       postsTestData.push(newPost);
    //       count++;
    //       if (count % 100 === 0) {
    //       console.log.apply(null,   log(`Imported ${count} posts...`));;
    //         // render(postsTestData);
    //       }
    //       // if (count == 100-1) {
    //       //   break;
    //       // }
    //     }
    //     return postsTestData;
    //   }
    //   async createTestData3(userID: string) {
    //     let posts = await (await (fetch('./posts.json'))).json();
    //     return posts;
    //   }
    //   async registerServiceWorker() {
    //     if (!("serviceWorker" in navigator)) {
    //       return;
    //     }
    //     let registrations = await navigator.serviceWorker.getRegistrations();
    //     if (registrations.length > 0) {
    //       console.log.apply(null, log("Service worker already registered."));
    //       return registrations[0];
    //     }
    //     navigator.serviceWorker
    //       .register("/sw.js")
    //       .then((registration) => {
    //         console.log.apply(null, log("Service Worker registered with scope:", registration.scope));
    //         return registration;
    //       })
    //       .catch((error) => {
    //         console.error("Service Worker registration failed:", error);
    //       });
    //   }
    //   async compressImage(imageData: ArrayBuffer, mimeType: string, quality = 0.5): Promise<ArrayBuffer | null> {
    //     let uncompressedByteLength = imageData.byteLength;
    //   console.log.apply(null,   log(`compressImage input:${mimeType} size:${(uncompressedByteLength / 1024).toFixed(2)}KBi quality:${quality}`));;
    //     try {
    //       // Convert ArrayBuffer to Blob
    //       const blob = new Blob([imageData], { type: mimeType });
    //       const bitmap = await createImageBitmap(blob, {
    //         imageOrientation: 'none',
    //         // resizeWidth: desiredWidth,
    //         // resizeHeight: desiredHeight,
    //         // resizeQuality: 'high',
    //       });
    //       // const bitmap = await createImageBitmap(bitmapTemp, {
    //       //   imageOrientation: 'none',
    //       //     resizeWidth: 600,
    //       //     resizeHeight: 800,
    //       //     // resizeHeight: (bitmapTemp.height / bitmapTemp.width) * 600,
    //       //     resizeQuality: 'high',
    //       // })
    //       //drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)
    //       // Create a canvas and draw the image onto it
    //       // let scale = 1/32;
    //       // let scaledWidth = bitmap.width*scale;
    //       // let scaledHeight = bitmap.height*scale;
    //       // let scale = 1/32;
    //       let scaledWidth = bitmap.width;
    //       let scaledHeight = bitmap.height;
    //       let resizeThreshold = 600;
    //       if (scaledWidth > resizeThreshold) {
    //         scaledWidth = resizeThreshold;
    //         scaledHeight = (bitmap.height / bitmap.width) * resizeThreshold;
    //       }
    //       const canvas = document.createElement('canvas');
    //       canvas.width = scaledWidth;
    //       canvas.height = scaledHeight;
    //       const ctx = canvas.getContext('2d');
    //       ctx!.imageSmoothingEnabled = true;
    //       ctx!.imageSmoothingQuality = 'high';
    //       canvas!.getContext('2d')!.drawImage(bitmap, 0, 0, bitmap.width, bitmap.height, 0, 0, scaledWidth, scaledHeight);
    //       // Compress the image and get the result as an ArrayBuffer
    //       const compressedBlob = await new Promise((resolve, reject) =>
    //         canvas.toBlob(
    //           (blob) => (blob ? resolve(blob as Blob) : reject(new Error('Compression failed.'))),
    //           'image/jpeg',
    //           quality
    //         )
    //       );
    //       // TODO: Don't need to do this as we'll be storing blobs directly.
    //       let compressedArrayBuffer = await (compressedBlob as Blob).arrayBuffer();
    //       let compressedByteLength = compressedArrayBuffer.byteLength;
    //       let percent = (uncompressedByteLength / compressedByteLength)
    //     console.log.apply(null,   log(`compressImage: compressedSize:${(compressedArrayBuffer.byteLength / 1024).toFixed(2)}KBi ${percent.toFixed(2)}:1 compression`));;
    //       return compressedArrayBuffer;
    //     } catch (e) {
    //       console.error(e);
    //       return null;
    //     }
    //   }
    //   async createNewPost(userID: string, postText: string, mediaData?: ArrayBuffer, mimeType?: "image/png" | "image/gif" | "image/jpg" | "image/jpeg" | "video/mp4") {
    //     if ((typeof postText !== "string") || postText.length === 0) {
    //     console.log.apply(null,   log("Not posting an empty string..."));
    //       return;
    //     }
    //     if (mediaData &&
    //       (mimeType === 'image/jpg' || mimeType === 'image/jpeg' || mimeType === 'image/png') &&
    //       (mediaData as ArrayBuffer).byteLength > 500 * 1024) {
    //       let compressedImage = await this.compressImage(mediaData as ArrayBuffer, mimeType, 0.9);
    //       if (compressedImage) {
    //         mediaData = compressedImage as ArrayBuffer;
    //       }
    //     }
    //     let post = new Post(this.username, userID, postText, new Date(), mediaData);
    //     // this.posts.push(post);
    //     // localStorage.setItem(key, JSON.stringify(posts));
    //     addData(userID, post);
    //     this.websocket?.broadcastNewPost(userID, post);
    //     this.render();
    //   }
    getPeerID() {
        let id = localStorage.getItem("peer_id");
        if (!id) {
            console.log.apply(null, log(`Didn't find a peer ID, generating one`));
            ;
            id = generateID();
            localStorage.setItem("peer_id", id);
        }
        return id;
    }
    getUserID() {
        let id = localStorage.getItem("dandelion_id");
        if (!id) {
            console.log.apply(null, log(`Didn't find a user ID, generating one`));
            ;
            id = generateID();
            localStorage.setItem("dandelion_id", id);
        }
        return id;
    }
    hashIdToIndices(id) {
        let indices = [];
        for (let char of id) {
            if (char !== '0' && char !== '-') {
                indices.push(parseInt(char, 16));
                if (indices.length == 2) {
                    break;
                }
            }
        }
        return [indices[0], indices[1]];
    }
    funkyName(id, listOne, listTwo) {
        let [one, two] = this.hashIdToIndices(id);
        let first = listOne[one % this.adjectives.length];
        let second = listTwo[two % this.animals.length];
        return { first, second };
    }
    getUsername() {
        let username = localStorage.getItem("dandelion_username");
        if (username && username !== "not_set") {
            return username;
        }
        let { first: adjective, second: animal } = this.funkyName(this.userID, this.adjectives, this.animals);
        username = `${adjective}_${animal}`;
        localStorage.setItem("dandelion_username", username);
        return username;
    }
    getPeername() {
        let { first: adjective, second: snake } = this.funkyName(this.peerID, this.adjectives, this.snakes);
        let peername = `${adjective}_${snake}`;
        return peername;
    }
    //   setFont(fontName: string, fontSize: string) {
    //     let content = document.getElementById('content');
    //     if (!content) {
    //       return;
    //     }
    //     content.style.fontFamily = fontName;
    //     content.style.fontSize = fontSize;
    //     let textArea = document.getElementById('textarea_post');
    //     if (!textArea) {
    //       return;
    //     }
    //     textArea.style.fontFamily = fontName;
    //     textArea.style.fontSize = fontSize;
    //   }
    //   initOffline(connection: wsConnection) {
    //     // Event listener for going offline
    //     globalThis.addEventListener('offline', () => {
    //     console.log.apply(null,   log("offline"));
    //     });
    //     // Event listener for going online
    //     globalThis.addEventListener('online', async () => {
    //     console.log.apply(null,   log("online"));
    //       // connection.connect();
    //       this.render();
    //     });
    //   console.log.apply(null,   log(`Online status: ${navigator.onLine ? "online" : "offline"}`));
    //   }
    //   selectFile(contentType: string): Promise<File | null> {
    //     return new Promise(resolve => {
    //       let input = document.createElement('input');
    //       input.type = 'file';
    //       // input.multiple = multiple;
    //       input.accept = contentType;
    //       input.onchange = () => {
    //         if (input.files == null) {
    //           resolve(null);
    //           return;
    //         }
    //         let files = Array.from(input.files);
    //         // if (multiple)
    //         //     resolve(files);
    //         // else
    //         resolve(files[0]);
    //       };
    //       input.click();
    //     });
    //   }
    //   readFile(file: File): Promise<string> {
    //     // Always return a Promise
    //     return new Promise((resolve, reject) => {
    //       let content = '';
    //       const reader = new FileReader();
    //       // Wait till complete
    //       reader.onloadend = function (e: any) {
    //         content = e.target.result;
    //         resolve(content);
    //       };
    //       // Make sure to handle error states
    //       reader.onerror = function (e: any) {
    //         reject(e);
    //       };
    //       reader.readAsText(file);
    //     });
    //   }
    //   async lazyCreateQRCode() {
    //     if (this.qrcode != null) {
    //       return;
    //     }
    //     this.qrcode = await new QRCode(document.getElementById('qrcode'), {
    //       text: this.connectURL,
    //       width: 150,
    //       height: 150,
    //       colorDark: "#000000",
    //       colorLight: "#ffffff",
    //       correctLevel: QRCode.CorrectLevel.H
    //     });
    //   }
    //   showInfo() {
    //     let infoElement = document.getElementById('info');
    //     if (infoElement === null) {
    //       return;
    //     }
    //     infoElement.style.display == 'none' ? infoElement.style.display = 'block' : infoElement.style.display = 'none';
    //     setLogVisibility(infoElement.style.display == 'block');
    //     renderLog();
    //     this.lazyCreateQRCode();
    //     (document.querySelector('#qrcode > img') as HTMLImageElement).classList.add('qrcode_image');
    //     (document.querySelector('#qrcode > canvas') as HTMLImageElement).classList.add('qrcode_image');
    //     this.showLog = true;
    //   }
    //   button(elementName: string) {
    //     return document.getElementById(elementName) as HTMLButtonElement;
    //   }
    //   div(elementName: string) {
    //     return document.getElementById(elementName) as HTMLDivElement;
    //   }
    //   initButtons(userID: string, posts: StoragePost[], registration: ServiceWorkerRegistration | undefined) {
    //     // let font1Button = document.getElementById("button_font1") as HTMLButtonElement;
    //     // let font2Button = document.getElementById("button_font2") as HTMLButtonElement;
    //     // let importTweetsButton = document.getElementById("import_tweets") as HTMLButtonElement;
    //     // let toggleDark = document.getElementById('toggle_dark') as HTMLButtonElement;
    //     // let clearPostsButton = document.getElementById("clear_posts") as HTMLButtonElement;
    //     // let updateApp = document.getElementById("update_app") as HTMLButtonElement;
    //     // let ddlnLogoButton = document.getElementById('ddln_logo_button') as HTMLDivElement;
    //     // let addPic = document.getElementById('button_add_pic') as HTMLDivElement;
    //     // toggleDark.addEventListener('click', () => {
    //     //   document.documentElement.style.setProperty('--main-bg-color', 'white');
    //     //   document.documentElement.style.setProperty('--main-fg-color', 'black');
    //     // })
    //     let homeButton = this.div('home-button');
    //     homeButton.addEventListener('click', e => globalThis.location.href = `${globalThis.location.origin}/`)
    //     let profileButton = this.div('profile-button');
    //     profileButton.addEventListener('click', e => globalThis.location.href = `${globalThis.location.origin}/user/${this.userID}`)
    //     let monitorButton = this.div('monitor_button');
    //     monitorButton.addEventListener('click', async () => {
    //       navContainer.classList.toggle('active');
    //       this.showInfo()
    //     });
    //     let navContainer = this.div('nav-container');
    //     let burgerMenuButton = this.div('burger-menu-button');
    //     burgerMenuButton.addEventListener('click', e => navContainer.classList.toggle('active'));
    //     let exportButton = this.button("export-button");
    //     exportButton.addEventListener('click', async e => {
    //       await this.exportPostsForUser(this.userID)
    //     });
    //     let composeButton = this.div('compose-button');
    //     composeButton.addEventListener('click', e => {
    //       document.getElementById('compose')!.style.display = 'block';
    //       document.getElementById('textarea_post')?.focus();
    //     });
    //     let filePicker = document.getElementById('file-input') as HTMLInputElement;
    //     filePicker?.addEventListener('change', async (event: any) => {
    //       for (let file of filePicker.files as any) {
    //         let buffer = await file.arrayBuffer();
    //         await this.createNewPost(this.userID, 'image...', buffer, file.type);
    //       }
    //       // Reset so that if they pick the same image again, we still get the change event.
    //       filePicker.value = '';
    //     });
    //     let filePickerLabel = document.getElementById('file-input-label');
    //     filePickerLabel?.addEventListener('click', () => {
    //       console.log.apply(null, log("Add pic..."));
    //     })
    //     let usernameField = document.getElementById('username');
    //     usernameField?.addEventListener('input', (event: any) => {
    //       this.username = event.target.innerText;
    //       localStorage.setItem("dandelion_username", this.username);
    //     })
    //     // importTweetsButton.addEventListener('click', async () => {
    //     //   let file = await this.selectFile('text/*');
    //     //   console.log.apply(null, log(file);
    //     //   if (file == null) {
    //     //     return;
    //     //   }
    //     //   let tweetData = await this.readFile(file);
    //     //   tweetData = tweetData.replace('globalThis.YTD.tweets.part0 = ', '');
    //     //   const tweets = JSON.parse(tweetData);
    //     //   let imported_posts = await this.importTweetArchive(userID, tweets);
    //     //   clearData(userID);
    //     //   // posts = posts.reverse();
    //     //   addDataArray(userID, imported_posts);
    //     //   this.render();
    //     // });
    //     // clearPostsButton.addEventListener('click', () => { clearData(userID); posts = []; this.render() });
    //     let postButton = document.getElementById("button_post") as HTMLButtonElement;
    //     let postText = document.getElementById("textarea_post") as HTMLTextAreaElement;
    //     if (!(postButton && postText)) {
    //       throw new Error();
    //     }
    //     postText.addEventListener('paste', async (e) => {
    //       const dataTransfer = e.clipboardData
    //       const file = dataTransfer!.files[0];
    //       let buffer = await file.arrayBuffer();
    //       await this.createNewPost(this.userID, 'image...', buffer, file.type as any);
    //     });
    //     postButton.addEventListener("click", () => {
    //       this.createNewPost(userID, postText.value);
    //       postText.value = "";
    //       document.getElementById('compose')!.style.display = 'none';
    //     });
    //     // updateApp.addEventListener("click", () => {
    //     //   registration?.active?.postMessage({ type: "update_app" });
    //     // });
    //     // ddlnLogoButton.addEventListener('click', async () => {
    //     //   this.showInfo()
    //     // });
    //   }
    //   async getPostsForFeed() {
    //     // get N posts from each user and sort them by date.
    //     // This isn't really going to work very well.
    //     // Eventually we'll need a db that only has followed user posts so we can get them chronologically
    //     //
    //     let posts: StoragePost[] = [];
    //     for (let followedID of this.following.keys()) {
    //       posts = posts.concat(await getData(followedID, new Date(2022, 8), new Date()));
    //       // console.log.apply(null, log(followedID);
    //     }
    //     // @ts-ignore
    //     posts = posts.sort((a, b) => a.post_timestamp - b.post_timestamp);
    //     return posts;
    //   }
    //   async loadFollowersFromStorage(userID: string): Promise<string[]> {
    //     // Rob
    //     if (userID === 'b38b623c-c3fa-4351-9cab-50233c99fa4e') {
    //       return [
    //         'b38b623c-c3fa-4351-9cab-50233c99fa4e',
    //         '6d774268-16cd-4e86-8bbe-847a0328893d', // Sean
    //         '05a495a0-0dd8-4186-94c3-b8309ba6fc4c', // Martin
    //         'a0e42390-08b5-4b07-bc2b-787f8e5f1297', // BMO
    //         'bba3ad24-9181-4e22-90c8-c265c80873ea', // Harry
    //         '8f6802be-c3b6-46c1-969c-5f90cbe01479', // Fiona
    //       ]
    //     }
    //     // Martin
    //     if (userID === '05a495a0-0dd8-4186-94c3-b8309ba6fc4c') {
    //       return [
    //         'b38b623c-c3fa-4351-9cab-50233c99fa4e',
    //         'a0e42390-08b5-4b07-bc2b-787f8e5f1297', // BMO
    //       ]
    //     }
    //     // Fiona
    //     if (userID === '8f6802be-c3b6-46c1-969c-5f90cbe01479') {
    //       return [
    //         'b38b623c-c3fa-4351-9cab-50233c99fa4e', // Rob
    //         'a0e42390-08b5-4b07-bc2b-787f8e5f1297', // BMO
    //         '05a495a0-0dd8-4186-94c3-b8309ba6fc4c', // Martin
    //       ]
    //     }
    //     return ['a0e42390-08b5-4b07-bc2b-787f8e5f1297']; // Follow BMO by default :)
    //   }
    //   async loadPostsFromStorage(userID: string, postID?: string) {
    //     this.timerStart();
    //     let posts: StoragePost[] = [];
    //     // if (postID) {
    //     //   posts = await gePostForUser(userID, postID);
    //     // }
    //     posts = await getData(userID, new Date(2022, 8), new Date());
    //     if (posts.length > 0) {
    //     console.log.apply(null,   log(`Loaded ${posts.length} posts in ${this.timerDelta().toFixed(2)}ms`));;
    //       return posts;
    //     }
    //     // posts = await createTestData2(userID);
    //     // log("Adding test data...");
    //     // addDataArray(userID, posts);
    //     // return await getData(userID, new Date(2022, 8), new Date());
    //   }
    //   async listUsers() {
    //     let knownUsers = [...(await indexedDB.databases())].map((db) => db.name?.replace('user_', ''));
    //     if (knownUsers.length === 0) {
    //       return;
    //     }
    //     let preferredId = app.getPreferentialUserID()
    //     for (let userID of knownUsers as string[]) {
    //       // if (userID === preferredId) {
    //       //   continue;
    //       // }
    //       // let ids = await getAllIds(userID);
    //       // if (ids.length === 0) {
    //       //   console.log.apply(null, log(`Purging user ${userID}`);
    //       //   indexedDB.deleteDatabase(`user_${userID}`);
    //       //   continue;
    //       // }
    //       console.log.apply(null, log(`${document.location.origin}/user/${userID}`));
    //       // console.log.apply(null, log(`https://ddln.app/${this.username}/${uuidToBase58(userID)}`, userID);
    //     }
    //   }
    //   async initDB() {
    //     let db = await openDatabase(this.userID);
    //   }
    //   query_findPeersForUser(message: any) {
    //     let havePostsForUser = true;
    //     if (havePostsForUser) {
    //       return this.peerID;
    //     }
    //     return false;
    //   }
    async registerRPCs() {
        if (!this.peerManager) {
            throw new Error();
        }
        this.peerManager.registerRPC('ping', (args) => {
            return { id: this.peerID, user: this.userID, user_name: this.username, peer_name: this.peername };
        });
        if (!this.isBootstrapPeer) {
            let pong = await this.peerManager.rpc.ping(this.peerManager.bootstrapPeerID);
            console.log.apply(null, log('pong from: ', pong));
        }
        // this.peerManager.registerRPC('getPostIDsForUser', (args: any) => {
        //   this.sync.getPostsForUser
        // });
    }
    //   async testPeerManager() {
    //     if (!this.peerManager) {
    //       throw new Error();
    //     }
    //     this.peerManager.registerRPC('getPostIDsForUser', (args: any) => {
    //       return [1, 2, 3, 4, 5]
    //     });
    //     let postIDs = await this.peerManager.rpc.getPostIDsForUser("dummy_peer", "bloop");
    //     console.log.apply(null, log("peerManager.rpc.getPostIDsForUser", postIDs));
    //     // this.peerManager.registerSearchQuery('find_peers_for_user', this.query_findPeersForUser);
    //     // let peers = await this.peerManager.search('find_peers_for_user', { 'user_id': 'bloop' });
    //   }
    async main() {
        // Do capability detection here and report in a simple way if things we need don't exist with guidance on how to resolve it.
        // let urlParams = (new URL(globalThis.location.href)).searchParams;
        // if (urlParams.has('log')) {
        //   this.showInfo();
        // }
        this.isHeadless = true; ///\bHeadlessChrome\//.test(navigator.userAgent);
        this.isBootstrapPeer = true; //urlParams.has("bootstrap");
        if (this.isBootstrapPeer) {
            console.log.apply(null, log(`This is a bootstrap peer`));
            ;
        }
        this.peerID = this.getPeerID();
        this.peername = this.getPeername();
        this.userID = this.getUserID();
        this.username = this.getUsername();
        this.connect();
    }
}
let app = new App();
app.main();
// globalThis.addEventListener("load", app.main.bind(app));
