// 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, deleteData, mergeDataArray, getAllData, checkPostIds, getAllIds, getPostsByIds } from "db";
// 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) {
    return new Promise(resolve => setTimeout(resolve, durationMs));
}
function uuidv4() {
    return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, (c) => (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16));
}
function uuidToBytes(uuid) {
    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) {
    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) {
    const bytes = uuidToBytes(uuid);
    return encodeBase58(bytes);
}
function logID(ID) {
    return ID.substring(0, 5);
}
// function log(message:string) {
//   console.log(message);
//   let log = document.getElementById("log");
//   let newlog = document.createElement('span');
//   newlog.innerHTML = `<pre>${message}</pre>`;
//   log?.appendChild(newlog);
// }
let logLines = [];
let logLength = 30;
let logVisible = false;
function renderLog() {
    if (!logVisible) {
        return;
    }
    let log = document.getElementById("log");
    if (!log) {
        throw new Error();
    }
    log.innerText = logLines.join("\n");
}
function log(message) {
    console.log(message);
    logLines.push(`${new Date().toLocaleTimeString()}: ${message}`);
    if (logLines.length > logLength) {
        logLines = logLines.slice(logLines.length - logLength);
    }
    renderLog();
}
function generateID() {
    if (self.crypto.hasOwnProperty("randomUUID")) {
        return self.crypto.randomUUID();
    }
    return uuidv4();
}
class Post {
    constructor(author, author_id, text, post_timestamp, imageData = null, importedFrom = null, importSource = 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;
    }
}
window.addEventListener('scroll', () => {
    // Total height of the document
    const totalPageHeight = document.body.scrollHeight;
    // Current scroll position
    const scrollPoint = window.scrollY + window.innerHeight;
    // Check if scrolled to bottom
    if (scrollPoint >= totalPageHeight) {
        console.log('Scrolled to the bottom!');
        console.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, 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) {
    var bytes = new Uint8Array(buffer);
    return (await bytesToBase64DataUrl(bytes)).replace("data:application/octet-stream;base64,", "");
}
async function base64ToArrayBuffer(base64String) {
    let response = await fetch("data:application/octet-stream;base64," + base64String);
    let arrayBuffer = await response.arrayBuffer();
    return arrayBuffer;
}
async function compressString(input) {
    // 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);
}
// class Signaler {
//   websocket: WebSocket | null = null;
//   websocketPingInterval: number = 0;
//   connect() {
//   if (this.websocket?.readyState === WebSocket.OPEN) {
//     return;
//   }
//   window.clearInterval(this.websocketPingInterval);
//   if (this.websocket) { this.websocket.close() };
//   try {
//     this.websocket = new WebSocket(`wss://${window.location.hostname}:${window.location.port}/ws`);
//   } catch (error: any) {
//     console.log(error.message);
//     return;
//   }
//   this.websocket.onopen = async (event) => {
//     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("wsConnection: Setting hello refresh interval to ", helloRefreshIntervalPeriod)
//       this.helloRefreshInterval = window.setInterval(() => {
//         console.log("wsConnection: Hello refresh.")
//         if (!navigator.onLine) {
//           return;
//         }
//         this.sendHello();
//       }, helloRefreshIntervalPeriod * 1000);
//     }
//     this.websocketPingInterval = window.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) => {
//     log("ws:disconnected");
//     // this.retry *= 2;
//     log(`Retrying in ${this.retry} seconds`);
//     window.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) => {
//     log('ws:error: ' + event);
//   };
// }
// }
// disconnect() {
//   this.websocket?.close();
// }
// }
class wsConnection {
    constructor(userID, peerID, IDsToSync) {
        this.websocket = null;
        this.userID = "";
        this.peerID = "";
        this.websocketPingInterval = 0;
        this.helloRefreshInterval = 0;
        this.retry = 10;
        this.state = 'disconnected';
        // peers: Map<string, string[]> = new Map();
        this.messageHandlers = new Map();
        this.peerMessageHandlers = new Map();
        this.seenPeers = new Map();
        //   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;
        // }
        this.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'
        ]);
        this.userBlockList = new Set([
            '5d63f0b2-a842-41bf-bf06-e0e4f6369271',
            '5f1b85c4-b14c-454c-8df1-2cacc93f8a77',
            // 'bba3ad24-9181-4e22-90c8-c265c80873ea'
        ]);
        this.userID = userID;
        this.peerID = peerID;
        this.UserIDsToSync = new Set(IDsToSync);
        this.messageHandlers.set('hello', this.helloResponseHandler.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));
        window.addEventListener('beforeunload', () => this.disconnect());
        this.connect();
    }
    // So we don't need custom logic everywhere we use this, I just wrapped it.
    shouldSyncUserID(userID) {
        if (app.isHeadless) {
            return true;
        }
        return this.UserIDsToSync.has(userID);
    }
    async send(message) {
        let json = "";
        try {
            json = JSON.stringify(message);
            // console.log("*******", (await compressString(json)).byteLength, json.length);
        }
        catch (e) {
            console.log(e, "wsConnection send: Couldn't serialize message", message);
        }
        // log(`ws->${json.slice(0, 240)}`)
        this.websocket.send(json);
    }
    pongHandler(data) {
    }
    async sendWebRTCOfferHandler(data) {
    }
    async getPostIdsForUserResponseHandler(data) {
        // log(`getPostsForUserResponse: ${data}`)
        let message = data.message;
        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);
        log(`ID Check for user ${logID(message.user_id)} took ${app.timerDelta().toFixed(2)}ms`);
        log(`Need ${postIds.length} posts for user ${logID(message.user_id)} from peer ${logID(data.from)}`);
        if (postIds.length === 0) {
            return;
        }
        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);
    }
    async getPostIdsForUserHandler(data) {
        let message = data.message;
        let postIds = await getAllIds(message.user_id) ?? [];
        postIds = postIds.filter((postID) => !this.postBlockList.has(postID));
        if (postIds.length === 0) {
            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;
        }
        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, post) {
        let newPost = { ...post };
        if (post.image_data) {
            newPost.image_data = await arrayBufferToBase64(post.image_data);
        }
        for (let [peerID, peerInfo] of this.seenPeers.entries()) {
            log(`broadcastNewPost: sending new post to ${logID(peerID)}:${peerInfo.peerName}:${peerInfo.userName}`);
            this.sendPostsForUser(peerID, app.userID, [newPost]);
        }
    }
    async sendPostsForUser(toPeerID, userID, posts) {
        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) {
        let message = data.message;
        let posts = await getPostsByIds(message.user_id, message.post_ids) ?? [];
        log(`Net: Sending ${posts.length} posts for user ${logID(message.user_id)} to peer ${logID(data.from)}`);
        app.timerStart();
        let output = [];
        console.log("Serializing images");
        for (let post of posts) {
            let newPost = post.data;
            if (newPost.image_data) {
                // let compressedData = await wsConnection.compressArrayBuffer(newPost.image_data);
                // console.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(`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("Sending posts");
        await this.sendPostsForUser(data.from, message.user_id, output);
        let sendTime = app.timerDelta();
        log(`getPostsForUserHandler send took: ${sendTime.toFixed(2)}ms`);
    }
    // Got posts from peer
    async getPostsForUserReponseHandler(data) {
        app.timerStart();
        let message = data.message;
        console.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)) {
                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(`Merging same user peer posts...`);
        await mergeDataArray(message.user_id, data.message.posts);
        let receiveTime = app.timerDelta();
        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) {
        // 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);
    }
    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('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 });
    }
    helloResponseHandler(data) {
        let users = [];
        let receivedUsers = Object.entries(data.userPeers);
        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('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("Skipping user on blocklist:", userID);
                continue;
            }
            // this.peers.set(userID, [...peerIDs]);
            for (let peerID of [...peerIDs]) {
                if (peerID === this.peerID) {
                    continue;
                }
                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() {
        if (this.websocket?.readyState === WebSocket.OPEN) {
            return;
        }
        window.clearInterval(this.websocketPingInterval);
        if (this.websocket) {
            this.websocket.close();
        }
        ;
        try {
            this.websocket = new WebSocket(`wss://${window.location.hostname}:${window.location.port}/ws`);
        }
        catch (error) {
            console.log(error.message);
            return;
        }
        this.websocket.onopen = async (event) => {
            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("wsConnection: Setting hello refresh interval to ", helloRefreshIntervalPeriod);
                this.helloRefreshInterval = window.setInterval(() => {
                    console.log("wsConnection: Hello refresh.");
                    if (!navigator.onLine) {
                        return;
                    }
                    this.sendHello();
                }, helloRefreshIntervalPeriod * 1000);
            }
            this.websocketPingInterval = window.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 });
            }, 10000);
        };
        this.websocket.onclose = (event) => {
            log("ws:disconnected");
            // this.retry *= 2;
            log(`Retrying in ${this.retry} seconds`);
            window.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) => {
            log('ws:error: ' + event);
        };
    }
    disconnect() {
        this.websocket?.close();
    }
}
class App {
    constructor() {
        this.username = '';
        this.peername = '';
        this.userID = '';
        this.peerID = '';
        this.following = new Set();
        this.posts = [];
        this.isHeadless = false;
        this.showLog = false;
        this.markedAvailable = false;
        this.limitPosts = 50;
        this.websocket = null;
        this.vizGraph = null;
        this.qrcode = null;
        this.connectURL = "";
        this.firstRun = false;
        this.time = 0;
        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'];
        // 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
        this.renderedPosts = new Map();
        this.router = {
            route: App.Route.HOME,
            userID: '',
            postID: '',
            mediaID: ''
        };
    }
    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, title, text) => {
            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;
    }
    timerStart() {
        this.time = performance.now();
    }
    timerDelta() {
        return performance.now() - this.time;
    }
    getFixedTweetText(entry) {
        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, filename, mimeType = '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 = window.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
        window.URL.revokeObjectURL(url);
    }
    downloadJson(data, filename = 'data.json') {
        const jsonString = JSON.stringify(data);
        const blob = new Blob([jsonString], { type: 'application/json' });
        const url = window.URL.createObjectURL(blob);
        const link = document.createElement('a');
        link.href = url;
        link.download = filename;
        document.body.appendChild(link);
        link.click();
        document.body.removeChild(link);
        window.URL.revokeObjectURL(url);
    }
    async importPostsForUser(userID, posts) {
    }
    async exportPostsForUser(userID) {
        let posts = await getAllData(userID);
        let output = [];
        console.log("Serializing images");
        for (let post of posts) {
            let newPost = post.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, tweetArchive) {
        log("Importing tweet archive");
        let postsTestData = [];
        // let response = await fetch("./tweets.js");
        // let tweetsText = await response.text();
        // tweetsText = tweetsText.replace("window.YTD.tweets.part0", "window.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 = 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(imageData);
            //   } catch (e) {
            //     console.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) {
                log(`Imported ${count} posts...`);
                // render(postsTestData);
            }
            // if (count == 100-1) {
            //   break;
            // }
        }
        return postsTestData;
    }
    async createTestData3(userID) {
        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("Service worker already registered.");
            return registrations[0];
        }
        navigator.serviceWorker
            .register("/sw.js")
            .then((registration) => {
            console.log("Service Worker registered with scope:", registration.scope);
            return registration;
        })
            .catch((error) => {
            console.error("Service Worker registration failed:", error);
        });
    }
    async compressImage(imageData, mimeType, quality = 0.5) {
        let uncompressedByteLength = imageData.byteLength;
        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) : 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.arrayBuffer();
            let compressedByteLength = compressedArrayBuffer.byteLength;
            let percent = (uncompressedByteLength / compressedByteLength);
            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, postText, mediaData, mimeType) {
        if ((typeof postText !== "string") || postText.length === 0) {
            log("Not posting an empty string...");
            return;
        }
        if (mediaData &&
            (mimeType === 'image/jpg' || mimeType === 'image/jpeg' || mimeType === 'image/png') &&
            mediaData.byteLength > 500 * 1024) {
            let compressedImage = await this.compressImage(mediaData, mimeType, 0.9);
            if (compressedImage) {
                mediaData = compressedImage;
            }
        }
        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();
    }
    // What happens with multiple tabs/connections from the same peer ID? I don't think this works, we also need a session ID for each connection
    getPeerID() {
        let id = localStorage.getItem("peer_id");
        if (!id) {
            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) {
            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, fontSize) {
        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) {
        // Event listener for going offline
        window.addEventListener('offline', () => {
            log("offline");
        });
        // Event listener for going online
        window.addEventListener('online', async () => {
            log("online");
            connection.connect();
            this.render();
        });
        log(`Online status: ${navigator.onLine ? "online" : "offline"}`);
    }
    selectFile(contentType) {
        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) {
        // Always return a Promise
        return new Promise((resolve, reject) => {
            let content = '';
            const reader = new FileReader();
            // Wait till complete
            reader.onloadend = function (e) {
                content = e.target.result;
                resolve(content);
            };
            // Make sure to handle error states
            reader.onerror = function (e) {
                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';
        logVisible = infoElement.style.display == 'block';
        renderLog();
        this.lazyCreateQRCode();
        document.querySelector('#qrcode > img').classList.add('qrcode_image');
        document.querySelector('#qrcode > canvas').classList.add('qrcode_image');
        this.showLog = true;
    }
    button(elementName) {
        return document.getElementById(elementName);
    }
    div(elementName) {
        return document.getElementById(elementName);
    }
    initButtons(userID, posts, registration) {
        // 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 => window.location.href = `${window.location.origin}/`);
        let profileButton = this.div('profile-button');
        profileButton.addEventListener('click', e => window.location.href = `${window.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');
        filePicker?.addEventListener('change', async (event) => {
            for (let file of filePicker.files) {
                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("Add pic...");
        });
        let usernameField = document.getElementById('username');
        usernameField?.addEventListener('input', (event) => {
            this.username = event.target.innerText;
            localStorage.setItem("dandelion_username", this.username);
        });
        // importTweetsButton.addEventListener('click', async () => {
        //   let file = await this.selectFile('text/*');
        //   console.log(file);
        //   if (file == null) {
        //     return;
        //   }
        //   let tweetData = await this.readFile(file);
        //   tweetData = tweetData.replace('window.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");
        let postText = document.getElementById("textarea_post");
        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);
        });
        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 = [];
        for (let followedID of this.following.keys()) {
            posts = posts.concat(await getData(followedID, new Date(2022, 8), new Date()));
            // console.log(followedID);
        }
        // @ts-ignore
        posts = posts.sort((a, b) => a.post_timestamp - b.post_timestamp);
        return posts;
    }
    async loadFollowersFromStorage(userID) {
        // 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, postID) {
        this.timerStart();
        let posts = [];
        // if (postID) {
        //   posts = await gePostForUser(userID, postID);
        // }
        posts = await getData(userID, new Date(2022, 8), new Date());
        if (posts.length > 0) {
            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) {
            // if (userID === preferredId) {
            //   continue;
            // }
            // let ids = await getAllIds(userID);
            // if (ids.length === 0) {
            //   console.log(`Purging user ${userID}`);
            //   indexedDB.deleteDatabase(`user_${userID}`);
            //   continue;
            // }
            console.log(`${document.location.origin}/user/${userID}`);
            // console.log(`https://ddln.app/${this.username}/${uuidToBase58(userID)}`, userID);
        }
    }
    // createLogoCanvas() {
    //   let logoElement = document.getElementById('ddln_logo_button');
    //   if (!logoElement) {
    //     return;
    //   }
    //   let canvas = document.createElement('canvas');
    //   canvas.width = 32;
    //   canvas.height = 32;
    //   logoElement.appendChild(canvas);
    //   let ctx = canvas.getContext('2d');
    //   if (!ctx) {
    //     return;
    //   }
    //   if (ctx) {
    //     ctx.fillStyle = "red"; // Set fill color to red
    //     ctx.fillRect(0, 0, canvas.width, canvas.height); // Fill entire canvas
    //   }
    //   return canvas;
    // }
    // initLogo() {
    //   const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)");
    //   if (prefersReducedMotion.matches) {
    //     return;
    //   }
    //   // let canvas = this.createLogoCanvas();
    //   // if (!canvas) {
    //     // return;
    //   // }
    //   prefersReducedMotion.addEventListener("change", event => {
    //     let preferReducedMotion = event.matches == true;
    //     if (preferReducedMotion) {
    //       // canvas.remove();
    //       return;
    //     }
    //     // this.createLogoCanvas();
    //   });
    // }
    createNetworkViz() {
        let timer = this.timerStart();
        this.vizGraph = ForceGraph3D();
        let vizElement = document.getElementById('ddln_logo_button');
        if (!vizElement) {
            return;
        }
        const N = 8;
        const gData = {
            nodes: [{ id: 0 }],
            links: []
        };
        this.vizGraph(vizElement)
            .graphData(gData)
            // .width(32)
            // .height(32)
            .width(600)
            .height(300)
            .showNavInfo(false)
            .nodeRelSize(8)
            .nodeOpacity(1.0)
            .linkWidth(2)
            .linkOpacity(1.0)
            .backgroundColor('rgba(0,0,0,0)');
        let angle = 0;
        const distance = 400;
        setInterval(() => {
            const { nodes, links } = this.vizGraph.graphData();
            const id = nodes.length;
            this.vizGraph.graphData({
                nodes: [...nodes, { id }],
                links: [...links, { source: id, target: Math.round(Math.random() * (id - 1)) }]
            });
        }, 1000);
        setInterval(() => {
            this.vizGraph.cameraPosition({
                x: distance * Math.sin(angle),
                z: distance * Math.cos(angle)
            });
            angle += Math.PI / 300;
        }, 10);
        // .backgroundColor('rgba(0,0,0,0)');
        // .enablePointerInteraction(false)
        // .enableNavigationControls(false);
        console.log(`create viz network took ${this.timerDelta()}ms`);
    }
    async initDB() {
        let db = await openDatabase(this.userID);
    }
    async main() {
        let urlParams = (new URL(window.location.href)).searchParams;
        if (urlParams.has('log')) {
            this.showInfo();
        }
        let peer = null;
        //  if (window.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?.url}`); };
        peer.onnegotiationneeded = (event) => { console.log("on negotiation needed: ", event); };
        peer.createOffer().then((description) => {
            peer.setLocalDescription(description);
            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
        // window.resizeTo(645, 900);
        // this.initLogo()
        this.isHeadless = /\bHeadlessChrome\//.test(navigator.userAgent);
        this.getRoute();
        if (this.router.route === App.Route.CONNECT) {
            console.log('connect', this.router.userID);
            localStorage.setItem("dandelion_id", this.router.userID);
            localStorage.removeItem("dandelion_username");
        }
        this.peerID = this.getPeerID();
        this.peername = this.getPeername();
        this.userID = this.getUserID();
        this.username = this.getUsername();
        await this.initDB();
        this.connectURL = `${document.location.origin}/connect/${this.userID}`;
        document.getElementById('connectURL').innerHTML = `<a href="${this.connectURL}">connect</a>`;
        if (urlParams.has('headless')) {
            this.isHeadless = true;
        }
        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();
        //   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(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?.memory) {
            log(`memory used: ${(performance.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);
        log(`username:${this.username} user:${this.userID} peername:${this.peername} peer:${this.peerID}`);
        // await this.purgeEmptyUsers();
        let IDsToSync = this.following;
        if (this.router.route === App.Route.USER) {
            IDsToSync = new Set([this.router.userID]);
        }
        this.websocket = new wsConnection(this.userID, this.peerID, IDsToSync);
        this.initOffline(this.websocket);
        // 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) {
        contentDiv.innerHTML = `<div style="font-size:24px">
      Welcome to Dandelion v0.1!<br>
      Loading posts for the default feed...
    </div>
    `;
    }
    async render() {
        if (this.isHeadless) {
            console.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("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);
            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("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();
        log(`render took: ${renderTime.toFixed(2)}ms`);
        performance.mark("render-end");
        performance.measure('render-time', 'render-start', 'render-end');
        // if ((performance as any)?.memory) {
        //   log(`memory used: ${((performance as any).memory.usedJSHeapSize / 1024 / 1024).toFixed(2)}Mb`)
        // }
    }
    async deletePost(userID, postID) {
        deleteData(userID, postID);
        this.render();
    }
    renderPost(post, first) {
        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]);
        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) {
        element.style.transform = "scale(2.0)";
    }
    getRoute() {
        let path = document.location.pathname;
        console.log(">>>>>>>>>>>>>>>>>>>>>>>>>", 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(">>>>>>>>>>>>>>>>>>>>>>>>>", 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 = /
    }
}
(function (App) {
    let Route;
    (function (Route) {
        Route[Route["USER"] = 0] = "USER";
        Route[Route["POST"] = 1] = "POST";
        Route[Route["MEDIA"] = 2] = "MEDIA";
        Route[Route["GROUP"] = 3] = "GROUP";
        Route[Route["HOME"] = 4] = "HOME";
        Route[Route["CONNECT"] = 5] = "CONNECT";
    })(Route = App.Route || (App.Route = {}));
    ;
})(App || (App = {}));
let app = new App();
window.addEventListener("load", app.main.bind(app));
