<template>

    <!-- nomadnetwork sidebar -->
    <NomadNetworkSidebar
        v-if="!isPopoutMode"
        :nodes="nodes"
        :favourites="favourites"
        :selected-destination-hash="selectedNode?.destination_hash"
        @node-click="onNodeClick"
        @rename-favourite="onRenameFavourite"
        @remove-favourite="onRemoveFavourite"/>

   <div class="flex flex-col flex-1 overflow-hidden min-w-full sm:min-w-[500px] dark:bg-zinc-950">
    <!-- node -->
    <div v-if="selectedNode" class="flex flex-col h-full min-h-0 bg-white dark:bg-zinc-950 overflow-hidden sm:m-2 sm:border dark:border-zinc-800 sm:rounded-xl sm:shadow dark:shadow-zinc-900">
        <!-- header -->
        <div class="flex p-2 border-b border-gray-300 dark:border-zinc-800">

            <!-- favourite button -->
            <div class="my-auto mr-2">
                <div v-if="isFavourite(selectedNode.destination_hash)" @click="removeFavourite(selectedNode)" class="cursor-pointer">
                    <div class="flex text-yellow-500 dark:text-yellow-300 bg-gray-100 dark:bg-zinc-800 hover:bg-gray-200 dark:hover:bg-zinc-700 p-1 rounded-full">
                        <div>
                            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-5">
                                <path fill-rule="evenodd" d="M10.788 3.21c.448-1.077 1.976-1.077 2.424 0l2.082 5.006 5.404.434c1.164.093 1.636 1.545.749 2.305l-4.117 3.527 1.257 5.273c.271 1.136-.964 2.033-1.96 1.425L12 18.354 7.373 21.18c-.996.608-2.231-.29-1.96-1.425l1.257-5.273-4.117-3.527c-.887-.76-.415-2.212.749-2.305l5.404-.434 2.082-5.005Z" clip-rule="evenodd" />
                            </svg>
                        </div>
                    </div>
                </div>
                <div v-else @click="addFavourite(selectedNode)" class="cursor-pointer">
                    <div class="flex text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-zinc-800 hover:bg-gray-200 dark:hover:bg-zinc-700 p-1 rounded-full">
                        <div>
                            <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-5">
                                <path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z" />
                            </svg>
                        </div>
                    </div>
                </div>
            </div>

            <!-- node info -->
            <div class="my-auto dark:text-gray-100 flex-1 min-w-0 flex items-baseline gap-1">
                <span class="font-semibold truncate inline-block max-w-xs sm:max-w-sm" :title="selectedNode.display_name">{{ selectedNode.display_name }}</span>
                <span v-if="selectedNodePath" @click="onDestinationPathClick(selectedNodePath)" class="text-sm cursor-pointer whitespace-nowrap"> - {{ selectedNodePath.hops }} {{ selectedNodePath.hops === 1 ? 'hop' : 'hops' }} away</span>
            </div>

            <!-- identify button -->
            <div class="my-auto ml-auto mr-2">
                <div @click="identify(selectedNode.destination_hash)" class="cursor-pointer">
                    <div class="flex text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-zinc-800 hover:bg-gray-200 dark:hover:bg-zinc-700 p-1 rounded-full">
                        <div>
                            <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-5">
                                <path stroke-linecap="round" stroke-linejoin="round" d="M7.864 4.243A7.5 7.5 0 0 1 19.5 10.5c0 2.92-.556 5.709-1.568 8.268M5.742 6.364A7.465 7.465 0 0 0 4.5 10.5a7.464 7.464 0 0 1-1.15 3.993m1.989 3.559A11.209 11.209 0 0 0 8.25 10.5a3.75 3.75 0 1 1 7.5 0c0 .527-.021 1.049-.064 1.565M12 10.5a14.94 14.94 0 0 1-3.6 9.75m6.633-4.596a18.666 18.666 0 0 1-2.485 5.33" />
                            </svg>
                        </div>
                    </div>
                </div>
            </div>

            <!-- popout button -->
            <div class="my-auto mr-2">
                <div @click="openNomadnetPopout" class="cursor-pointer">
                    <div class="flex text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-zinc-800 hover:bg-gray-200 dark:hover:bg-zinc-700 p-1 rounded-full">
                        <div>
                            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-5">
                                <path d="M17 3.75h3.25A.75.75 0 0 1 21 4.5v3.25a.75.75 0 0 1-1.5 0V6.31l-4.97 4.97a.75.75 0 1 1-1.06-1.06l4.97-4.97H17a.75.75 0 0 1 0-1.5Z"/>
                                <path d="M5.25 6A2.25 2.25 0 0 0 3 8.25v10.5A2.25 2.25 0 0 0 5.25 21h10.5A2.25 2.25 0 0 0 18 18.75v-6a.75.75 0 0 0-1.5 0v6c0 .414-.336.75-.75.75H5.25a.75.75 0 0 1-.75-.75V8.25c0-.414.336-.75.75-.75h6a.75.75 0 0 0 0-1.5h-6Z"/>
                            </svg>
                        </div>
                    </div>
                </div>
            </div>

            <!-- close button -->
            <div class="my-auto mr-2">
                <div @click="onCloseNodeViewer" class="cursor-pointer">
                    <div class="flex text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-zinc-800 hover:bg-gray-200 dark:hover:bg-zinc-700 p-1 rounded-full">
                        <div>
                            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5">
                                <path d="M6.28 5.22a.75.75 0 0 0-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 1 0 1.06 1.06L10 11.06l3.72 3.72a.75.75 0 1 0 1.06-1.06L11.06 10l3.72-3.72a.75.75 0 0 0-1.06-1.06L10 8.94 6.28 5.22Z" />
                            </svg>
                        </div>
                    </div>
                </div>
            </div>
        </div>

        <!-- browser navigation -->
        <div class="flex w-full border-gray-300 dark:border-zinc-800 border-b p-2">
            <button @click="loadNodePage(selectedNode.destination_hash, defaultNodePagePath)" type="button" class="my-auto text-gray-500 dark:text-gray-300 bg-gray-200 dark:bg-zinc-800 hover:bg-gray-300 dark:hover:bg-zinc-700 rounded p-1 cursor-pointer">
                <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5">
                    <path fill-rule="evenodd" d="M9.293 2.293a1 1 0 0 1 1.414 0l7 7A1 1 0 0 1 17 11h-1v6a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1v-3a1 1 0 0 0-1-1H9a1 1 0 0 0-1 1v3a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-6H3a1 1 0 0 1-.707-1.707l7-7Z" clip-rule="evenodd" />
                </svg>
            </button>
            <button @click="reloadNodePage" type="button" class="ml-1 my-auto text-gray-500 dark:text-gray-300 bg-gray-200 dark:bg-zinc-800 hover:bg-gray-300 dark:hover:bg-zinc-700 rounded p-1 cursor-pointer">
                <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5">
                    <path fill-rule="evenodd" d="M15.312 11.424a5.5 5.5 0 0 1-9.201 2.466l-.312-.311h2.433a.75.75 0 0 0 0-1.5H3.989a.75.75 0 0 0-.75.75v4.242a.75.75 0 0 0 1.5 0v-2.43l.31.31a7 7 0 0 0 11.712-3.138.75.75 0 0 0-1.449-.39Zm1.23-3.723a.75.75 0 0 0 .219-.53V2.929a.75.75 0 0 0-1.5 0V5.36l-.31-.31A7 7 0 0 0 3.239 8.188a.75.75 0 1 0 1.448.389A5.5 5.5 0 0 1 13.89 6.11l.311.31h-2.432a.75.75 0 0 0 0 1.5h4.243a.75.75 0 0 0 .53-.219Z" clip-rule="evenodd" />
                </svg>
            </button>
            <button @click="toggleNodePageSource" type="button" title="Toggle Source Code" class="ml-1 my-auto text-gray-500 dark:text-gray-300 rounded p-1 cursor-pointer" :class="[ isShowingNodePageSource ? 'bg-green-500 hover:bg-green-600 text-white' : 'bg-gray-200 dark:bg-zinc-800 hover:bg-gray-300 dark:hover:bg-zinc-700' ]">
                <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-5">
                    <path stroke-linecap="round" stroke-linejoin="round" d="M17.25 6.75 22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3-4.5 16.5" />
                </svg>
            </button>
            <button @click="loadPreviousNodePage" type="button" :disabled="nodePagePathHistory.length === 0" :class="[ nodePagePathHistory.length > 0 ? 'text-gray-500 dark:text-gray-300 bg-gray-200 dark:bg-zinc-800 hover:bg-gray-300 dark:hover:bg-zinc-700' : 'text-gray-400 dark:text-gray-500 bg-gray-100 dark:bg-zinc-900']" class="ml-1 my-auto rounded p-1 cursor-pointer">
                <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5">
                    <path fill-rule="evenodd" d="M17 10a.75.75 0 0 1-.75.75H5.612l4.158 3.96a.75.75 0 1 1-1.04 1.08l-5.5-5.25a.75.75 0 0 1 0-1.08l5.5-5.25a.75.75 0 1 1 1.04 1.08L5.612 9.25H16.25A.75.75 0 0 1 17 10Z" clip-rule="evenodd" />
                </svg>
            </button>
            <div class="my-auto mx-2 w-full">
                <input v-model="nodePagePathUrlInput" @keyup.enter="onNodePageUrlClick(nodePagePathUrlInput)" type="text" placeholder="Enter Destination URL" class="bg-gray-50 dark:bg-zinc-900 border border-gray-300 dark:border-zinc-700 text-gray-900 dark:text-gray-100 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full px-2.5 py-1.5 dark:placeholder-gray-400">
            </div>
            <button @click="onNodePageUrlClick(nodePagePathUrlInput)" type="button" class="my-auto text-gray-500 dark:text-gray-300 bg-gray-200 dark:bg-zinc-800 hover:bg-gray-300 dark:hover:bg-zinc-700 rounded p-1 cursor-pointer">
                <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5">
                    <path fill-rule="evenodd" d="M3 10a.75.75 0 0 1 .75-.75h10.638L10.23 5.29a.75.75 0 1 1 1.04-1.08l5.5 5.25a.75.75 0 0 1 0 1.08l-5.5 5.25a.75.75 0 1 1-1.04-1.08l4.158-3.96H3.75A.75.75 0 0 1 3 10Z" clip-rule="evenodd" />
                </svg>
            </button>
        </div>

        <!-- page content -->
        <div class="flex-1 overflow-y-auto p-3 bg-black text-white nodeContainer">
            <div class="flex" v-if="isLoadingNodePage">
                <div class="my-auto">
                    <svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
                        <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
                        <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
                    </svg>
                </div>
                <div class="my-auto flex-1">Loading {{ nodePageProgress }}%</div>
                <button @click="cancelPageDownload" type="button" class="my-auto text-white bg-red-600 hover:bg-red-700 dark:bg-red-700 dark:hover:bg-red-800 rounded px-3 py-1 text-sm font-semibold cursor-pointer ml-3">
                    Cancel
                </button>
            </div>
            <pre v-else v-html="renderedNodePageContent()" class="h-full break-words whitespace-pre-wrap"></pre>
        </div>

        <!-- file download bottom bar -->
        <div v-if="isDownloadingNodeFile" class="flex w-full border-gray-300 dark:border-zinc-800 border-t p-2 dark:text-gray-100">
            <div class="my-auto mr-2">
                <svg class="animate-spin h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
                    <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
                    <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
                </svg>
            </div>
            <div class="my-auto flex-1">
                Downloading: {{ nodeFilePath }} ({{ nodeFileProgress }}%)
                <span v-if="nodeFileDownloadSpeed !== null" class="ml-2 text-sm">
                    - {{ formatBytesPerSecond(nodeFileDownloadSpeed) }}
                </span>
            </div>
            <button @click="cancelFileDownload" type="button" class="my-auto text-white bg-red-600 hover:bg-red-700 dark:bg-red-700 dark:hover:bg-red-800 rounded px-3 py-1 text-sm font-semibold cursor-pointer">
                Cancel
            </button>
        </div>
    </div>

    <!-- no node selected -->
    <div v-else class="flex flex-col mx-auto my-auto text-center leading-5 dark:text-gray-100">
        <div class="mx-auto mb-1">
            <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 dark:text-gray-300">
                <path stroke-linecap="round" stroke-linejoin="round" d="M12 21a9.004 9.004 0 0 0 8.716-6.747M12 21a9.004 9.004 0 0 1-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 0 1 7.843 4.582M12 3a8.997 8.997 0 0 0-7.843 4.582m15.686 0A11.953 11.953 0 0 1 12 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0 1 21 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0 1 12 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 0 1 3 12c0-1.605.42-3.113 1.157-4.418" />
            </svg>
        </div>
        <div class="font-semibold">No Active Node</div>
        <div>Select a Node to start browsing!</div>
        <div class="mx-auto mt-2">
            <button @click.stop="openUrl" type="button"
                class="my-auto inline-flex items-center gap-x-1 rounded-md bg-gray-500 px-2 py-1 text-sm font-semibold text-white shadow-sm hover:bg-gray-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-500
                dark:bg-zinc-800 dark:text-white dark:hover:bg-zinc-700 dark:focus-visible:outline-zinc-500">
                Open a Nomadnet URL
            </button>
        </div>
    </div>
</div>

</template>

<style>

pre {
    font-family: Roboto Mono Nerd Font, monospace;
    line-height: normal;
}

pre.text-wrap > div {
    display: flex;
    white-space: pre;
}

pre.text-wrap > div > :last-child {
    width: 100%;
    white-space: pre-wrap;
}

pre a:hover {
    text-decoration: underline;
}

</style>

<script>

import MicronParser from "micron-parser";
import DialogUtils from "../../js/DialogUtils";
import WebSocketConnection from "../../js/WebSocketConnection";
import NomadNetworkSidebar from "./NomadNetworkSidebar.vue";
import GlobalEmitter from "../../js/GlobalEmitter";
import Utils from "../../js/Utils";

export default {
    name: 'NomadNetworkPage',
    components: {
        NomadNetworkSidebar,
    },
    props: {
        destinationHash: String,
    },
    data() {
        return {

            reloadInterval: null,

            nodes: {},
            selectedNode: null,
            selectedNodePath: null,

            favourites: [],

            isLoadingNodePage: false,
            isShowingNodePageSource: false,
            defaultNodePagePath: "/page/index.mu",
            nodePageRequestSequence: 0,
            nodePagePath: null,
            nodePagePathUrlInput: null,
            nodePageContent: null,
            nodePageProgress: 0,
            nodePagePathHistory: [],
            nodePageCache: {},
            currentPageDownloadId: null,

            isDownloadingNodeFile: false,
            nodeFilePath: null,
            nodeFileProgress: 0,
            nodeFileDownloadStartTime: null,
            nodeFileLastProgressTime: null,
            nodeFileLastProgressValue: 0,
            nodeFileDownloadSpeed: null,
            currentFileDownloadId: null,

            nomadnetPageDownloadCallbacks: {},
            nomadnetFileDownloadCallbacks: {},

        };
    },
    beforeUnmount() {

        clearInterval(this.reloadInterval);

        // stop listening for websocket messages
        WebSocketConnection.off("message", this.onWebsocketMessage);

        // stop listening for element clicks
        window.document.removeEventListener('click', this.onElementClick);

    },
    mounted() {

        // listen for websocket messages
        WebSocketConnection.on("message", this.onWebsocketMessage);

        // listen for element clicks
        window.document.addEventListener('click', this.onElementClick);

        // load nomadnetwork node if a destination hash was provided on page load
        if(this.destinationHash){
            (async () => {
                // fetch updated announce as we are probably loading node page before we loaded the announces list
                await this.getNomadnetworkNodeAnnounce(this.destinationHash);
                await this.onNodePageUrlClick(`${this.destinationHash}:${this.defaultNodePagePath}`);
            })();
        }

        this.getFavourites();
        this.getNomadnetworkNodeAnnounces();

        // update info every few seconds
        this.reloadInterval = setInterval(() => {
            this.getFavourites();
        }, 5000);

    },
    computed: {
        popoutRouteType() {
            if(this.$route?.meta?.popoutType){
                return this.$route.meta.popoutType;
            }
            return this.$route?.query?.popout ?? this.getHashPopoutValue();
        },
        isPopoutMode() {
            return this.popoutRouteType === "nomad";
        },
    },
    methods: {
        openNomadnetPopout() {
            if (!this.selectedNode) {
                return;
            }
            const destinationHash = this.selectedNode.destination_hash || "";
            const encodedHash = encodeURIComponent(destinationHash);
            const url = `${window.location.origin}${window.location.pathname}#/popout/nomadnetwork/${encodedHash}`;
            window.open(url, "_blank", "width=1100,height=800,noopener");
        },
        onElementClick(event) {

            // find the closest ancestor (or the clicked element itself) with data-action="openNode"
            const element = event.target.closest('[data-action="openNode"]');
            if(!element){
                return;
            }

            // get the destination and fields
            const destination = element.getAttribute("data-destination");
            const fields = element.getAttribute("data-fields");

            // navigate to destination
            this.onNodePageUrlClick(destination, fields);

        },
        async onWebsocketMessage(message) {
            const json = JSON.parse(message.data);
            switch(json.type){
                case 'announce': {
                    const aspect = json.announce.aspect;
                    if(aspect === "nomadnetwork.node"){
                        this.updateNodeFromAnnounce(json.announce);
                    }
                    break;
                }
                case 'nomadnet.page.download': {

                    // get data from server
                    const nomadnetPageDownload = json.nomadnet_page_download;
                    const downloadId = json.download_id;

                    // handle started status
                    if(nomadnetPageDownload.status === "started"){
                        this.currentPageDownloadId = downloadId;
                        return;
                    }

                    // find download callbacks
                    const getNomadnetPageDownloadCallbackKey = this.getNomadnetPageDownloadCallbackKey(nomadnetPageDownload.destination_hash, nomadnetPageDownload.page_path);
                    const nomadnetPageDownloadCallback = this.nomadnetPageDownloadCallbacks[getNomadnetPageDownloadCallbackKey];
                    if(!nomadnetPageDownloadCallback){
                        console.log("did not find nomadnet page download callback for key: " + getNomadnetPageDownloadCallbackKey);
                        return;
                    }

                    // handle success
                    if(nomadnetPageDownload.status === "success" && nomadnetPageDownloadCallback.onSuccessCallback){
                        nomadnetPageDownloadCallback.onSuccessCallback(nomadnetPageDownload.page_content);
                        delete this.nomadnetPageDownloadCallbacks[getNomadnetPageDownloadCallbackKey];
                        this.currentPageDownloadId = null;
                        return;
                    }

                    // handle failure
                    if(nomadnetPageDownload.status === "failure" && nomadnetPageDownloadCallback.onFailureCallback){
                        nomadnetPageDownloadCallback.onFailureCallback(nomadnetPageDownload.failure_reason);
                        delete this.nomadnetPageDownloadCallbacks[getNomadnetPageDownloadCallbackKey];
                        this.currentPageDownloadId = null;
                        return;
                    }

                    // handle progress
                    if(nomadnetPageDownload.status === "progress" && nomadnetPageDownloadCallback.onProgressCallback){
                        nomadnetPageDownloadCallback.onProgressCallback(nomadnetPageDownload.progress);
                        return;
                    }

                    break;

                }
                case 'nomadnet.file.download': {

                    // get data from server
                    const nomadnetFileDownload = json.nomadnet_file_download;
                    const downloadId = json.download_id;

                    // handle started status
                    if(nomadnetFileDownload.status === "started"){
                        this.currentFileDownloadId = downloadId;
                        return;
                    }

                    // find download callbacks
                    const getNomadnetFileDownloadCallbackKey = this.getNomadnetFileDownloadCallbackKey(nomadnetFileDownload.destination_hash, nomadnetFileDownload.file_path);
                    const nomadnetFileDownloadCallback = this.nomadnetFileDownloadCallbacks[getNomadnetFileDownloadCallbackKey];
                    if(!nomadnetFileDownloadCallback){
                        console.log("did not find nomadnet file download callback for key: " + getNomadnetFileDownloadCallbackKey);
                        return;
                    }

                    // handle success
                    if(nomadnetFileDownload.status === "success" && nomadnetFileDownloadCallback.onSuccessCallback){
                        nomadnetFileDownloadCallback.onSuccessCallback(nomadnetFileDownload.file_name, nomadnetFileDownload.file_bytes);
                        delete this.nomadnetFileDownloadCallbacks[getNomadnetFileDownloadCallbackKey];
                        this.currentFileDownloadId = null;
                        return;
                    }

                    // handle failure
                    if(nomadnetFileDownload.status === "failure" && nomadnetFileDownloadCallback.onFailureCallback){
                        nomadnetFileDownloadCallback.onFailureCallback(nomadnetFileDownload.failure_reason);
                        delete this.nomadnetFileDownloadCallbacks[getNomadnetFileDownloadCallbackKey];
                        this.currentFileDownloadId = null;
                        return;
                    }

                    // handle progress
                    if(nomadnetFileDownload.status === "progress" && nomadnetFileDownloadCallback.onProgressCallback){
                        nomadnetFileDownloadCallback.onProgressCallback(nomadnetFileDownload.progress);
                        return;
                    }

                    break;

                }
                case 'nomadnet.download.cancelled': {
                    // handle download cancellation
                    const downloadId = json.download_id;
                    
                    // clear page download if it matches
                    if(this.currentPageDownloadId === downloadId){
                        this.currentPageDownloadId = null;
                        this.isLoadingNodePage = false;
                        this.nodePageContent = "Download cancelled";
                    }
                    
                    // clear file download if it matches
                    if(this.currentFileDownloadId === downloadId){
                        this.currentFileDownloadId = null;
                        this.isDownloadingNodeFile = false;
                        this.nodeFileDownloadSpeed = null;
                    }
                    
                    break;
                }
            }
        },
        onDestinationPathClick: function(path) {
            DialogUtils.alert(`${path.hops} ${ path.hops === 1 ? 'hop' : 'hops' } away via ${path.next_hop_interface}`);
        },
        async getFavourites() {
            try {
                const response = await window.axios.get("/api/v1/favourites", {
                    params: {
                        aspect: "nomadnetwork.node",
                    },
                });
                this.favourites = response.data.favourites;
            } catch(e) {
                // do nothing if failed to load favourites
                console.log(e);
            }
        },
        isFavourite(destinationHash) {
            return this.favourites.find((favourite) => {
                return favourite.destination_hash === destinationHash;
            }) != null;
        },
        async addFavourite(node) {

            // add to favourites
            try {
                await window.axios.post("/api/v1/favourites/add", {
                    destination_hash: node.destination_hash,
                    display_name: node.display_name,
                    aspect: "nomadnetwork.node",
                });
            } catch(e) {
                console.log(e);
            }

            // update favourites
            this.getFavourites();

        },
        async removeFavourite(node) {

            // remove from favourites
            try {
                await window.axios.delete(`/api/v1/favourites/${node.destination_hash}`);
            } catch(e) {
                console.log(e);
            }

            // update favourites
            this.getFavourites();

        },
        async getNomadnetworkNodeAnnounces() {
            try {

                // fetch announces for "nomadnetwork.node" aspect
                const response = await window.axios.get(`/api/v1/announces`, {
                    params: {
                        aspect: "nomadnetwork.node",
                        limit: 500, // limit ui to showing 500 latest announces
                    },
                });

                // update ui
                const nodeAnnounces = response.data.announces;
                for(const nodeAnnounce of nodeAnnounces){
                    this.updateNodeFromAnnounce(nodeAnnounce);
                }

            } catch(e) {
                // do nothing if failed to load announces
                console.log(e);
            }
        },
        async getNomadnetworkNodeAnnounce(destinationHash) {
            try {

                // fetch announces for "nomadnetwork.node" aspect
                const response = await window.axios.get(`/api/v1/announces`, {
                    params: {
                        destination_hash: destinationHash,
                        limit: 1,
                    },
                });

                // update ui
                const nodeAnnounces = response.data.announces;
                for(const nodeAnnounce of nodeAnnounces){
                    this.updateNodeFromAnnounce(nodeAnnounce);
                }

            } catch(e) {
                // do nothing if failed to load announce
                console.log(e);
            }
        },
        updateNodeFromAnnounce: function(announce) {
            this.nodes[announce.destination_hash] = announce;
        },
        async openUrl() {

            // ask for url
            const url = await DialogUtils.prompt("Enter a Nomadnet URL");
            if(!url){
                return;
            }

            // navigate to the url
            await this.onNodePageUrlClick(url);

        },
        async loadNodePage(destinationHash, pagePath, fieldData = null, addToHistory = true, loadFromCache = true) {

            // update current route
            const routeName = this.isPopoutMode ? "nomadnetwork-popout" : "nomadnetwork";
            const routeOptions = {
                name: routeName,
                params: {
                    destinationHash: destinationHash,
                },
            };
            if(!this.isPopoutMode && this.$route?.query){
                routeOptions.query = { ...this.$route.query };
            }
            this.$router.replace(routeOptions);

            // get new sequence for this page load
            const seq = ++this.nodePageRequestSequence;

            // get previous page path
            const previousNodePagePath = this.nodePagePath;

            // update ui
            this.isLoadingNodePage = true;
            this.nodePagePath = `${destinationHash}:${pagePath}`;
            this.nodePageContent = null;
            this.nodePageProgress = 0;

            // update url bar
            this.nodePagePathUrlInput = this.nodePagePath;

            // update node path
            this.getNodePath(destinationHash);

            // add to previous page to history if we are not loading that previous page
            if(addToHistory && previousNodePagePath != null && previousNodePagePath !== this.nodePagePath){
                this.nodePagePathHistory.push(previousNodePagePath);
            }

            // check if we can load this page from the cache
            if(loadFromCache){

                // load from cache
                const nodePagePathCacheKey = `${destinationHash}:${pagePath}`;
                const cachedNodePageContent = this.nodePageCache[nodePagePathCacheKey];

                // if page is cache, we can just return it now
                if(cachedNodePageContent != null){
                    this.nodePageContent = cachedNodePageContent;
                    this.renderPageContent(pagePath, cachedNodePageContent);
                    this.isLoadingNodePage = false;
                    return;
                }

            }

            this.downloadNomadNetPage(destinationHash, pagePath, fieldData, (pageContent) => {

                // do nothing if callback is for a previous request
                if(seq !== this.nodePageRequestSequence){
                    console.log("ignoring page content callback for previous page request")
                    return;
                }

                // update page content
                this.nodePageContent = pageContent;

                // update cache
                const nodePagePathCacheKey = `${destinationHash}:${pagePath}`;
                this.nodePageCache[nodePagePathCacheKey] = this.nodePageContent;

                // update page content
                this.renderPageContent(pagePath, pageContent);
                this.isLoadingNodePage = false;

                // update node path
                this.getNodePath(destinationHash);

            }, (failureReason) => {

                // do nothing if callback is for a previous request
                if(seq !== this.nodePageRequestSequence){
                    console.log("ignoring failure callback for previous page request")
                    return;
                }

                // update page content
                this.nodePageContent = `Failed loading page: ${failureReason}`;
                this.isLoadingNodePage = false;

                // update node path
                this.getNodePath(destinationHash);

            }, (progress) => {

                // do nothing if callback is for a previous request
                if(seq !== this.nodePageRequestSequence){
                    console.log("ignoring progress callback for previous page request")
                    return;
                }

                // update page content
                this.nodePageProgress = Math.round(progress * 100);

            });
        },
        renderPageContent(path, content) {

            // render page content if we aren't viewing source
            if(!this.isShowingNodePageSource){

                // check if page url ends with .mu but remove page data first
                // address:/page/index.mu`Data=123
                const [ pagePathWithoutData ] = path.split("`");

                // convert micron to html if page ends with .mu extension
                if(pagePathWithoutData.endsWith(".mu")){
                    const muParser = new MicronParser();
                    return muParser.convertMicronToHtml(content);
                }

            }

            // otherwise, we will just serve the raw content, making sure to prevent injecting html
            return content
                .replace(/&/g, "&amp;")
                .replace(/</g, "&lt;")
                .replace(/>/g, "&gt;")
                .replace(/"/g, "&quot;")
                .replace(/'/g, "&#039;");

        },
        toggleNodePageSource() {
            this.isShowingNodePageSource = !this.isShowingNodePageSource;
        },
        async reloadNodePage() {

            // reload current node page without adding to history and without using cache
            this.onNodePageUrlClick(this.nodePagePath, null, false, false);

        },
        async loadPreviousNodePage() {

            // get the previous path from history, or do nothing
            const previousNodePagePath = this.nodePagePathHistory.pop();
            if(!previousNodePagePath){
                return;
            }

            // load the page
            this.onNodePageUrlClick(previousNodePagePath, null, null, true);

        },
        parseNomadnetworkUrl: function(url) {

            // parse relative urls
            if(url.startsWith(":")){

                // remove leading ":"
                var path = url.substring(1);

                // if page path is empty we should load default page path
                if(path === ""){
                    path = this.defaultNodePagePath;
                }

                return {
                    destination_hash: null, // node hash was not in provided url
                    path: path,
                };

            }

            // parse absolute urls such as 00000000000000000000000000000000:/page/index.mu
            if(url.includes(":")){

                // parse destination hash and url
                const [destinationHash, ...relativeUrl] = url.split(":");

                // ensure destination is expected length
                if(destinationHash.length === 32){
                    return {
                        destination_hash: destinationHash,
                        path: relativeUrl.join(":"),
                    };
                }

            }

            // parse node id only
            if(url.length === 32){
                return {
                    destination_hash: url,
                    path: this.defaultNodePagePath,
                };
            }

            // unsupported url
            return null;

        },
        async onNodePageUrlClick(url, options = null, addToHistory = true, useCache = false) {

            let fieldData = [];

            if (options === "*") {
                useCache = false; // we want to send another request with the field data
                const inputs = document.querySelectorAll('.nodeContainer input');

                const inputValues = {};

                for(const input of inputs){
                    if(input.type === 'radio' || input.type === 'checkbox'){
                        // Only add if the input is checked
                        if(input.checked){
                            inputValues[input.name] = input.value;
                        }
                    } else {
                        // For other input types, just get the value
                        inputValues[input.name || input.id || input.type] = input.value;
                    }
                }

                fieldData = inputValues;
            } else if(options !== null && options !== "") {
                useCache = false; 
                // split options into an array of names
                const validNames = options.split('|');

                // Select inputs within the container
                const inputs = document.querySelectorAll('.nodeContainer input');

                const inputValues = {};

                // Filter inputs by name and handle their values
                for(const input of inputs){
                    if(validNames.includes(input.name)){
                        if(input.type === 'radio' || input.type === 'checkbox'){
                            // Only add if the input is checked
                            if(input.checked){
                                inputValues[input.name] = input.value;
                            }
                        } else {
                            // For other input types, just get the value
                            inputValues[input.name] = input.value;
                        }
                    }
                }

                fieldData = inputValues;
            }

            console.log(fieldData);


            // open http urls in new tab
            if(url.startsWith("http://") || url.startsWith("https://")){
                window.open(url, "_blank");
                return;
            }

            // lxmf urls should open the conversation
            if(url.startsWith("lxmf@")){
                const destinationHash = url.replace("lxmf@", "");
                if(destinationHash.length === 32){
                    const routeName = this.isPopoutMode ? "messages-popout" : "messages";
                    await this.$router.push({
                        name: routeName,
                        params: {
                            destinationHash: destinationHash,
                        },
                    });
                    return;
                }
            }

            // attempt to parse url
            const parsedUrl = this.parseNomadnetworkUrl(url);
            if(parsedUrl != null){

                // use parsed destination hash, or fallback to selected node destination hash
                const destinationHash = parsedUrl.destination_hash || this.selectedNode.destination_hash;

                // download file
                if(parsedUrl.path.startsWith("/file/")){

                    // prevent simultaneous downloads
                    if(this.isDownloadingNodeFile){
                        DialogUtils.alert("An existing download is in progress. Please wait for it to finish before starting another download.");
                        return;
                    }

                    // update ui
                    this.isDownloadingNodeFile = true;
                    this.nodeFilePath = parsedUrl.path.split("/").pop();
                    this.nodeFileProgress = 0;
                    this.nodeFileDownloadStartTime = Date.now();
                    this.nodeFileLastProgressTime = Date.now();
                    this.nodeFileLastProgressValue = 0;
                    this.nodeFileDownloadSpeed = null;

                    // start file download
                    this.downloadNomadNetFile(destinationHash, parsedUrl.path, (fileName, fileBytesBase64) => {

                        // Calculate final download speed based on actual file size
                        if (this.nodeFileDownloadStartTime) {
                            const totalTime = (Date.now() - this.nodeFileDownloadStartTime) / 1000; // seconds
                            const fileSizeBytes = atob(fileBytesBase64).length;
                            if (totalTime > 0) {
                                this.nodeFileDownloadSpeed = fileSizeBytes / totalTime;
                            }
                        }

                        // no longer downloading
                        this.isDownloadingNodeFile = false;

                        // download file to browser
                        this.downloadFileFromBase64(fileName, fileBytesBase64);

                        // Clear speed after a moment
                        setTimeout(() => {
                            this.nodeFileDownloadSpeed = null;
                        }, 2000);

                    }, (failureReason) => {

                        // no longer downloading
                        this.isDownloadingNodeFile = false;
                        this.nodeFileDownloadSpeed = null;

                        // show error message
                        DialogUtils.alert(`Failed to download file: ${failureReason}`);

                    }, (progress) => {
                        const currentTime = Date.now();
                        const progressValue = progress;
                        this.nodeFileProgress = Math.round(progressValue * 100);

                        // Calculate estimated download speed based on progress rate
                        if (this.nodeFileDownloadStartTime && progressValue > 0) {
                            const elapsedTime = (currentTime - this.nodeFileDownloadStartTime) / 1000; // seconds
                            if (elapsedTime > 0.5) { // Only calculate after at least 0.5 seconds
                                // Estimate total file size based on progress rate
                                // If we've downloaded progressValue in elapsedTime, estimate total time
                                const estimatedTotalTime = elapsedTime / progressValue;
                                // Estimate file size based on average download speed assumption
                                // We'll refine this when download completes with actual size
                                // For now, estimate based on typical mesh network file sizes (100KB-10MB range)
                                // Use a conservative estimate that will be updated when download completes
                                const estimatedFileSize = 500 * 1024; // Start with 500KB estimate
                                const estimatedBytesDownloaded = estimatedFileSize * progressValue;
                                const estimatedSpeed = estimatedBytesDownloaded / elapsedTime;
                                
                                // Only update if we have a reasonable estimate
                                if (estimatedSpeed > 0 && estimatedSpeed < 100 * 1024 * 1024) { // Cap at 100MB/s
                                    this.nodeFileDownloadSpeed = estimatedSpeed;
                                }
                            }
                        }

                        this.nodeFileLastProgressTime = currentTime;
                        this.nodeFileLastProgressValue = progressValue;
                    });

                    return;

                }

                // update selected node, so relative urls work correctly when returned by the new node
                this.selectedNode = this.nodes[destinationHash] || {
                    display_name: "Unknown Node",
                    destination_hash: destinationHash,
                };

                // navigate to node page
                this.loadNodePage(destinationHash, parsedUrl.path, fieldData, addToHistory, useCache);
                return;

            }

            // unsupported url
            DialogUtils.alert("unsupported url: " + url);

        },
        downloadFileFromBase64: async function(fileName, fileBytesBase64) {

            // create blob from base64 encoded file bytes
            const byteCharacters = atob(fileBytesBase64);
            const byteNumbers = new Array(byteCharacters.length);
            for(let i = 0; i < byteCharacters.length; i++){
                byteNumbers[i] = byteCharacters.charCodeAt(i);
            }
            const byteArray = new Uint8Array(byteNumbers);
            const blob = new Blob([byteArray]);

            // create object url for blob
            const objectUrl = URL.createObjectURL(blob);

            // create link element to download blob
            const link = document.createElement('a');
            link.href = objectUrl;
            link.download = fileName;
            link.style.display = "none";
            document.body.append(link);

            // click link to download file in browser
            link.click();

            // link element is no longer needed
            link.remove();

            // revoke object url to clear memory
            setTimeout(() => URL.revokeObjectURL(objectUrl), 10000);

        },
        formatBytesPerSecond: function(bytesPerSecond) {
            return Utils.formatBytesPerSecond(bytesPerSecond);
        },
        onNodeClick: function(node) {

            // update selected node
            this.selectedNode = node;

            // load default node page
            this.loadNodePage(node.destination_hash, this.defaultNodePagePath);

        },
        async onRenameFavourite(favourite) {

            // ask user for new display name
            const displayName = await DialogUtils.prompt("Rename this favourite");
            if(displayName == null){
                return;
            }

            try {

                // rename on server
                await axios.post(`/api/v1/favourites/${favourite.destination_hash}/rename`, {
                    display_name: displayName,
                });

                // reload favourites
                await this.getFavourites();

            } catch(e) {
                console.log(e);
                DialogUtils.alert("Failed to rename favourite");
            }

        },
        async onRemoveFavourite(favourite) {

            // ask user to confirm
            if(!await DialogUtils.confirm("Are you sure you want to remove this favourite?")){
                return;
            }

            this.removeFavourite(favourite);

        },
        onCloseNodeViewer: function() {

            // clear selected node
            this.selectedNode = null;

            if(this.isPopoutMode){
                window.close();
                return;
            }

            // update current route
            const routeName = this.isPopoutMode ? "nomadnetwork-popout" : "nomadnetwork";
            const routeOptions = { name: routeName };
            if(!this.isPopoutMode && this.$route?.query){
                routeOptions.query = { ...this.$route.query };
            }
            this.$router.replace(routeOptions);

        },
        getNomadnetPageDownloadCallbackKey: function(destinationHash, pagePath) {
            return `${destinationHash}:${pagePath}`;
        },
        getNomadnetFileDownloadCallbackKey: function(destinationHash, filePath) {
            return `${destinationHash}:${filePath}`;
        },
        async getNodePath(destinationHash) {

            // clear previous known path
            this.selectedNodePath = null;

            try {

                // get path to destination
                const response = await window.axios.get(`/api/v1/destination/${destinationHash}/path`);

                // update ui
                this.selectedNodePath = response.data.path;

            } catch(e) {
                console.log(e);
            }

        },
        async identify(destinationHash) {
            try {

                // ask user to confirm
                if(!await DialogUtils.confirm("Are you sure you want to identify yourself to this NomadNetwork Node? The page will reload after your identity has been sent.")){
                    return;
                }

                // identify self to nomadnetwork node
                await window.axios.post(`/api/v1/nomadnetwork/${destinationHash}/identify`);

                // reload page
                this.reloadNodePage();

            } catch(e) {
                DialogUtils.alert(e.response?.data?.message ?? "Failed to identify!");
            }
        },
        getHashPopoutValue() {
            const hash = window.location.hash || "";
            const match = hash.match(/popout=([^&]+)/);
            return match ? decodeURIComponent(match[1]) : null;
        },
        downloadNomadNetFile(destinationHash, filePath, onSuccessCallback, onFailureCallback, onProgressCallback) {
            try {

                // set callbacks for nomadnet filePath download
                this.nomadnetFileDownloadCallbacks[this.getNomadnetFileDownloadCallbackKey(destinationHash, filePath)] = {
                    onSuccessCallback: onSuccessCallback,
                    onFailureCallback: onFailureCallback,
                    onProgressCallback: onProgressCallback,
                };

                // ask reticulum to download file from nomadnet
                WebSocketConnection.send(JSON.stringify({
                    "type": "nomadnet.file.download",
                    "nomadnet_file_download": {
                        "destination_hash": destinationHash,
                        "file_path": filePath,
                    },
                }));

            } catch(e) {
                console.error(e);
            }
        },
        downloadNomadNetPage(destinationHash, pagePath, fieldData, onSuccessCallback, onFailureCallback, onProgressCallback) {
            try {

                // set callbacks for nomadnet page download
                this.nomadnetPageDownloadCallbacks[this.getNomadnetPageDownloadCallbackKey(destinationHash, pagePath)] = {
                    onSuccessCallback: onSuccessCallback,
                    onFailureCallback: onFailureCallback,
                    onProgressCallback: onProgressCallback,
                };

                // ask reticulum to download page from nomadnet
                WebSocketConnection.send(JSON.stringify({
                    "type": "nomadnet.page.download",
                    "nomadnet_page_download": {
                        "destination_hash": destinationHash,
                        "page_path": pagePath,
                        "field_data": fieldData,
                    },
                }));

            } catch(e) {
                console.error(e);
            }
        },
        renderedNodePageContent() {
            return this.renderPageContent(this.nodePagePath, this.nodePageContent);
        },
        cancelPageDownload() {
            if(this.currentPageDownloadId !== null){
                WebSocketConnection.send(JSON.stringify({
                    "type": "nomadnet.download.cancel",
                    "download_id": this.currentPageDownloadId,
                }));
            }
        },
        cancelFileDownload() {
            if(this.currentFileDownloadId !== null){
                WebSocketConnection.send(JSON.stringify({
                    "type": "nomadnet.download.cancel",
                    "download_id": this.currentFileDownloadId,
                }));
            }
        },
    },
}
</script>
