const socket = io();

const GRID_ZOOM_THRESHOLD = 1.25;

// Canvas Elements
const canvas = document.getElementById('grid');
const ctx = canvas.getContext('2d');
const bufferCanvas = document.createElement('canvas');
const bufferCtx = bufferCanvas.getContext('2d');
const minimap = document.getElementById('minimap');
const minimapCtx = minimap.getContext('2d');

const usernameCookieName = 'kplace_username';
const avatarCookieName = 'kplace_avatar';
const friendsCookieName = 'kplace_friends';
const roomCookieName = 'kplace_room';
const positionCookieName = 'kplace_position';
const zoomCookieName = 'kplace_zoom';
const dmsCookieName = 'kplace_dms';
const showPixelInfoCookieName = 'kplace_showPixelInfo';
const showGridCookieName = 'kplace_showGrid';

// UI Elements
const colorPalette = document.getElementById('colorPalette');
const pixelInfo = document.getElementById('pixelInfo');
const usernameInput = document.getElementById('username');
const setUsernameBtn = document.getElementById('setUsername');
const onlineUsersSpan = document.getElementById('onlineUsers');
const customColorPicker = document.getElementById('customColor');
const chatMessages = document.getElementById('chatMessages');
const chatInput = document.getElementById('chatInput');
const roomList = document.getElementById('roomList');
const newRoomName = document.getElementById('newRoomName');
const createRoomBtn = document.getElementById('createRoom');
const friendList = document.getElementById('friendList');
const friendUsernameInput = document.getElementById('friendUsername');
const addFriendBtn = document.getElementById('addFriend');
const dmChat = document.getElementById('dmChat');
const dmMessages = document.getElementById('dmMessages');
const dmInput = document.getElementById('dmInput');
const sendDmBtn = document.getElementById('sendDm');
const avatarUpload = document.getElementById('avatarUpload');
const currentAvatarImg = document.getElementById('currentAvatarImg');
const notificationArea = document.getElementById('notificationArea');
const showPixelInfoCheckbox = document.getElementById('showPixelInfoCheckbox');
const showGridCheckbox = document.getElementById('showGridCheckbox');

// Server-Provided Configuration
let MAP_WIDTH = 10000;   // Default, will be updated from server
let MAP_HEIGHT = 10000;  // Default, will be updated from server
let CHUNK_SIZE = 100;    // Default, will be updated from server

// Constants
const pixelSize = 10;
const predefinedColors = [
    '#000000', '#FFFFFF', '#FF0000', '#00FF00', '#0000FF',
    '#FFFF00', '#00FFFF', '#FF00FF', '#800000', '#808000',
    '#008000', '#800080', '#008080', '#000080', '#FFA500',
    '#A52A2A'
];

// State Variables
let offsetX = MAP_WIDTH / 2, offsetY = MAP_HEIGHT / 2;
let zoom = 1;
let selectedColor = predefinedColors[0];
let username = loadFromLocalStorage(usernameCookieName, 'Anonymous');
let currentRoom = null;
let chunks = new Map();
let cursors = {}; // Store other users' cursor positions

// Friends and DMs
let friends = new Set();
let currentDM = null;

// Panning Variables
let isDragging = false;
let lastX, lastY;

// Initialize color palette event listeners
colorPalette.querySelectorAll('.color-btn').forEach(btn => {
    btn.addEventListener('click', () => {
        selectedColor = btn.getAttribute('data-color');
        // Highlight selected color
        colorPalette.querySelectorAll('.color-btn').forEach(b => b.classList.remove('selected'));
        btn.classList.add('selected');
        // Reset custom color picker
        customColorPicker.value = '#000000';
    });
});

// Handle custom color picker
customColorPicker.addEventListener('input', (event) => {
    selectedColor = event.target.value;
    // Remove selection from predefined colors
    colorPalette.querySelectorAll('.color-btn').forEach(b => b.classList.remove('selected'));
});

// Resize canvases based on window size
function resizeCanvas() {
    const drawerWidth = document.getElementById('drawer').offsetWidth;
    const headerHeight = document.querySelector('header').offsetHeight;
    canvas.width = window.innerWidth - drawerWidth;
    canvas.height = window.innerHeight - headerHeight;
    bufferCanvas.width = canvas.width;
    bufferCanvas.height = canvas.height;
    minimap.width = 200;
    minimap.height = 200;
    loadChunks();
    drawGrid();
    drawMinimapViewport();
}

// Update the drawGrid function
function drawGrid() {
    // Only draw grid when zoomed in enough
    if (zoom >= GRID_ZOOM_THRESHOLD && showGridCheckbox.checked) {
        ctx.strokeStyle = 'rgba(150, 150, 150, 0.5)'; // Semi-transparent gray
        ctx.lineWidth = 1;

        const gridSize = pixelSize * zoom;
        const startX = Math.floor(offsetX);
        const startY = Math.floor(offsetY);
        const endX = Math.ceil(offsetX + canvas.width / gridSize);
        const endY = Math.ceil(offsetY + canvas.height / gridSize);

        ctx.beginPath();

        // Vertical lines
        for (let x = startX; x <= endX; x++) {
            const screenX = Math.round((x - offsetX) * gridSize) - 0.5;
            ctx.moveTo(screenX, 0);
            ctx.lineTo(screenX, canvas.height);
        }

        // Horizontal lines
        for (let y = startY; y <= endY; y++) {
            const screenY = Math.round((y - offsetY) * gridSize) - 0.5;
            ctx.moveTo(0, screenY);
            ctx.lineTo(canvas.width, screenY);
        }

        ctx.stroke();
    }
}

window.addEventListener('resize', resizeCanvas);

// Fetch map configuration from server
socket.on('mapConfig', (config) => {
    MAP_WIDTH = config.MAP_WIDTH;
    MAP_HEIGHT = config.MAP_HEIGHT;
    CHUNK_SIZE = config.CHUNK_SIZE;
    // Center the camera
    offsetX = MAP_WIDTH / 2 - (canvas.width / (2 * pixelSize * zoom));
    offsetY = MAP_HEIGHT / 2 - (canvas.height / (2 * pixelSize * zoom));
    resizeCanvas();
});

window.addEventListener('load', () => {
    // Initial resize
    resizeCanvas();
});

// Function to convert grid coordinates to screen coordinates
function gridToScreen(x, y) {
    return {
        screenX: Math.floor((x - offsetX) * pixelSize * zoom),
        screenY: Math.floor((y - offsetY) * pixelSize * zoom)
    };
}

// Update the drawPixel function to draw on the main canvas
function drawPixel(chunkKey, x, y, color) {
    const [chunkX, chunkY] = chunkKey.split(',').map(Number);
    const pixelX = chunkX * CHUNK_SIZE + x;
    const pixelY = chunkY * CHUNK_SIZE + y;

    // Check if pixel is within map boundaries
    if (pixelX < 0 || pixelX >= MAP_WIDTH || pixelY < 0 || pixelY >= MAP_HEIGHT) {
        return; // Do not draw pixels outside the map
    }

    ctx.fillStyle = color;
    const pos = gridToScreen(pixelX, pixelY);
    ctx.fillRect(
        Math.floor(pos.screenX),
        Math.floor(pos.screenY),
        Math.ceil(pixelSize * zoom),
        Math.ceil(pixelSize * zoom)
    );
}

// Update the updateCanvas function
function updateCanvas() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    const visibleChunks = getVisibleChunks();
    visibleChunks.forEach(chunkKey => {
        const chunkData = chunks.get(chunkKey);
        if (chunkData) {
            Object.values(chunkData).forEach(pixel => {
                drawPixel(chunkKey, pixel.x, pixel.y, pixel.color);
            });
        }
    });

    drawGrid();
    drawMinimap();
    drawMinimapViewport();
}

// Update the minimap
function drawMinimap() {
    minimapCtx.clearRect(0, 0, minimap.width, minimap.height);

    const scaleFactor = minimap.width / MAP_WIDTH; // Scale minimap based on map size
    chunks.forEach((chunkData, chunkKey) => {
        Object.values(chunkData).forEach(pixel => {
            const pixelX = chunkKey.split(',')[0] * CHUNK_SIZE + pixel.x;
            const pixelY = chunkKey.split(',')[1] * CHUNK_SIZE + pixel.y;

            // Ensure pixel is within map boundaries
            if (pixelX < 0 || pixelX >= MAP_WIDTH || pixelY < 0 || pixelY >= MAP_HEIGHT) {
                return;
            }

            minimapCtx.fillStyle = pixel.color;
            minimapCtx.fillRect(
                pixelX * scaleFactor,
                pixelY * scaleFactor,
                1,
                1
            );
        });
    });
}

// Draw the viewport rectangle on minimap
function drawMinimapViewport() {
    minimapCtx.strokeStyle = 'red';
    minimapCtx.lineWidth = 1;
    const scaleFactor = minimap.width / MAP_WIDTH;

    const viewportWidth = (canvas.width / (pixelSize * zoom)) * scaleFactor;
    const viewportHeight = (canvas.height / (pixelSize * zoom)) * scaleFactor;

    let viewportX = offsetX * scaleFactor;
    let viewportY = offsetY * scaleFactor;

    // Clamp viewport within minimap
    viewportX = Math.max(0, Math.min(viewportX, minimap.width - viewportWidth));
    viewportY = Math.max(0, Math.min(viewportY, minimap.height - viewportHeight));

    minimapCtx.strokeRect(
        viewportX,
        viewportY,
        viewportWidth,
        viewportHeight
    );
}

// Determine which chunks are visible based on current offset and zoom
function getVisibleChunks() {
    const startX = Math.floor(offsetX / CHUNK_SIZE);
    const startY = Math.floor(offsetY / CHUNK_SIZE);
    const endX = Math.ceil((offsetX + canvas.width / (pixelSize * zoom)) / CHUNK_SIZE);
    const endY = Math.ceil((offsetY + canvas.height / (pixelSize * zoom)) / CHUNK_SIZE);

    const visibleChunks = [];
    for (let x = startX; x <= endX; x++) {
        for (let y = startY; y <= endY; y++) {
            // Ensure chunk coordinates are within map boundaries
            if (x * CHUNK_SIZE >= 0 && x * CHUNK_SIZE < MAP_WIDTH &&
                y * CHUNK_SIZE >= 0 && y * CHUNK_SIZE < MAP_HEIGHT) {
                visibleChunks.push(`${x},${y}`);
            }
        }
    }
    return visibleChunks;
}

// Request chunks from the server
function loadChunks() {
    const visibleChunks = getVisibleChunks();
    const chunksToLoad = visibleChunks.filter(chunk => !chunks.has(chunk));
    if (chunksToLoad.length > 0) {
        socket.emit('getChunks', { room: currentRoom, chunks: chunksToLoad });
    }
    updateCanvas();
}

// Handle incoming chunk data from the server
socket.on('chunkData', (data) => {
    const { room, chunkData } = data;
    if (room !== currentRoom) return; // Ignore data from other rooms

    Object.entries(chunkData).forEach(([chunkKey, pixels]) => {
        if (!chunks.has(chunkKey)) {
            chunks.set(chunkKey, {});
        }
        Object.entries(pixels).forEach(([pixelKey, pixelData]) => {
            const [x, y] = pixelKey.split(',').map(Number);
            // Ensure pixel is within map boundaries
            const pixelX = chunkKey.split(',')[0] * CHUNK_SIZE + x;
            const pixelY = chunkKey.split(',')[1] * CHUNK_SIZE + y;
            if (pixelX < 0 || pixelX >= MAP_WIDTH || pixelY < 0 || pixelY >= MAP_HEIGHT) {
                return;
            }
            chunks.get(chunkKey)[`${x},${y}`] = { x, y, color: pixelData.color, username: pixelData.username, time: pixelData.time };
        });
    });
    updateCanvas();
});

// Handle pixel updates from other users
socket.on('pixelUpdate', (data) => {
    const { room, chunkKey, x, y, color } = data;
    if (room !== currentRoom) return; // Ignore updates from other rooms

    // Ensure pixel is within map boundaries
    const pixelX = chunkKey.split(',')[0] * CHUNK_SIZE + x;
    const pixelY = chunkKey.split(',')[1] * CHUNK_SIZE + y;
    if (pixelX < 0 || pixelX >= MAP_WIDTH || pixelY < 0 || pixelY >= MAP_HEIGHT) {
        return; // Do not process pixels outside the map
    }

    if (!chunks.has(chunkKey)) {
        chunks.set(chunkKey, {});
    }
    chunks.get(chunkKey)[`${x},${y}`] = { x, y, color, username: 'Anonymous', time: Date.now() };
    drawPixel(chunkKey, x, y, color);
    updateCanvas();
});

// Update online users count
socket.on('onlineUsers', (count) => {
    onlineUsersSpan.textContent = count;
});

// Place a pixel on the canvas
function placePixel(event) {
    if (!currentRoom) {
        newRoomName.value = 'default';
        createRoomBtn.click();
    }
    const rect = canvas.getBoundingClientRect();
    const screenX = event.clientX - rect.left;
    const screenY = event.clientY - rect.top;

    // Convert screen coordinates to world coordinates
    const worldX = (screenX / (pixelSize * zoom)) + offsetX;
    const worldY = (screenY / (pixelSize * zoom)) + offsetY;

    // Convert world coordinates to grid coordinates
    const x = Math.floor(worldX);
    const y = Math.floor(worldY);

    // Ensure pixel is within map boundaries
    if (x < 0 || x >= MAP_WIDTH || y < 0 || y >= MAP_HEIGHT) {
        alert('Cannot place pixel outside the map boundaries!');
        return;
    }

    socket.emit('placePixel', { room: currentRoom, x, y, color: selectedColor, username });
    loadChunks();
}

// Handle mouse down events for placing pixels and panning
canvas.addEventListener('mousedown', (event) => {
    if (event.button === 0) { // Left mouse button
        placePixel(event);
        canvas.addEventListener('mousemove', placePixel);
    } else if (event.button === 2) { // Right mouse button
        isDragging = true;
        lastX = event.clientX;
        lastY = event.clientY;
        canvas.style.cursor = 'grabbing';
    }
});

// Handle mouse up events
canvas.addEventListener('mouseup', (event) => {
    if (event.button === 0) { // Left mouse button
        canvas.removeEventListener('mousemove', placePixel);
    } else if (event.button === 2) { // Right mouse button
        isDragging = false;
        canvas.style.cursor = 'crosshair';
    }
});

// Prevent context menu on right-click
canvas.addEventListener('contextmenu', (event) => {
    event.preventDefault();
});

canvas.addEventListener('mousemove', (event) => {
    if (isDragging) {
        const deltaX = event.clientX - lastX;
        const deltaY = event.clientY - lastY;
        offsetX -= deltaX / (pixelSize * zoom);
        offsetY -= deltaY / (pixelSize * zoom);

        // Clamp offsets to map boundaries
        const maxOffsetX = MAP_WIDTH - canvas.width / (pixelSize * zoom);
        const maxOffsetY = MAP_HEIGHT - canvas.height / (pixelSize * zoom);
        offsetX = Math.max(0, Math.min(offsetX, maxOffsetX));
        offsetY = Math.max(0, Math.min(offsetY, maxOffsetY));

        lastX = event.clientX;
        lastY = event.clientY;

        loadChunks();
        return; // Skip pixel info when panning
    }

    const rect = canvas.getBoundingClientRect();
    const screenX = event.clientX - rect.left;
    const screenY = event.clientY - rect.top;

    // Convert screen coordinates to world coordinates
    const worldX = (screenX / (pixelSize * zoom)) + offsetX;
    const worldY = (screenY / (pixelSize * zoom)) + offsetY;

    // Convert world coordinates to grid coordinates
    const x = Math.floor(worldX);
    const y = Math.floor(worldY);

    if (!showPixelInfoCheckbox.checked) {
        pixelInfo.style.display = 'none';
        return;
    }

    // Ensure pixel is within map boundaries before requesting info
    if (x >= 0 && x < MAP_WIDTH && y >= 0 && y < MAP_HEIGHT) {
        socket.emit('getPixelInfo', { room: currentRoom, x, y });
    } else {
        pixelInfo.style.display = 'none';
        return;
    }

    // Position the pixelInfo tooltip near the cursor
    pixelInfo.style.left = `${event.clientX + 10}px`;
    pixelInfo.style.top = `${event.clientY + 10}px`;
});

// Handle zooming with the mouse wheel
canvas.addEventListener('wheel', (event) => {
    event.preventDefault();
    const rect = canvas.getBoundingClientRect();
    const mouseX = event.clientX - rect.left;
    const mouseY = event.clientY - rect.top;

    // Convert screen coordinates to world coordinates before zoom
    const worldX = (mouseX / (pixelSize * zoom)) + offsetX;
    const worldY = (mouseY / (pixelSize * zoom)) + offsetY;

    // Determine zoom factor
    const zoomFactor = event.deltaY > 0 ? 0.9 : 1.1;
    const newZoom = zoom * zoomFactor;
    zoom = Math.max(0.1, Math.min(newZoom, 100)); // Clamp zoom

    // Adjust offset to keep the zoom centered on the mouse position
    offsetX = worldX - (mouseX / (pixelSize * zoom));
    offsetY = worldY - (mouseY / (pixelSize * zoom));

    // Clamp offsets to map boundaries after zoom
    const maxOffsetX = MAP_WIDTH - canvas.width / (pixelSize * zoom);
    const maxOffsetY = MAP_HEIGHT - canvas.height / (pixelSize * zoom);
    offsetX = Math.max(0, Math.min(offsetX, maxOffsetX));
    offsetY = Math.max(0, Math.min(offsetY, maxOffsetY));

    loadChunks();
    saveToLocalStorage(zoomCookieName, zoom);
});

function capitalizeString(str) {
if (str.length === 0) return str;
return str.charAt(0).toUpperCase() + str.slice(1);
}

// Handle incoming pixel info
socket.on('pixelInfo', (info) => {
    if (info && info.room === currentRoom) {
        pixelInfo.style.display = 'block';
        const timeString = new Date(info.time).toLocaleString(undefined, {
            year: 'numeric',
            month: 'numeric',
            day: 'numeric',
            hour: '2-digit',
            minute: '2-digit',
            hour12: true
        });
        if (info.color === '#FFFFFF') {
            pixelInfo.style.display = 'none';
        }
        pixelInfo.textContent = `${info.username}: ${capitalizeString(info.color)}\n[${info.x}, ${info.y}]\n(${timeString})`;
    } else {
        pixelInfo.style.display = 'none';
    }
});

// Handle setting the username
setUsernameBtn.addEventListener('click', () => {
    const inputUsername = usernameInput.value.trim();
    if (inputUsername) {
        username = inputUsername;
        socket.emit('setUsername', username);
        usernameInput.value = '';
        document.getElementById('currentUsername').textContent = username;
        addNotification(`Username set to ${username}`);
        saveToLocalStorage(usernameCookieName, username);
    }
});

// Optionally, set username on Enter key
usernameInput.addEventListener('keypress', (event) => {
    if (event.key === 'Enter') {
        setUsernameBtn.click();
    }
});

// Handle sending chat messages
chatInput.addEventListener('keypress', (event) => {
    if (event.key === 'Enter' && chatInput.value.trim() !== '') {
        socket.emit('chatMessage', { room: currentRoom, username, message: chatInput.value.trim() });
        chatInput.value = '';
    }
});

// Receive and display chat messages
socket.on('chatMessage', (data) => {
    if (data.room !== currentRoom) return;
    const messageEl = document.createElement('div');
    messageEl.innerHTML = `<strong>${data.username}:</strong> ${data.message}`;
    chatMessages.appendChild(messageEl);
    chatMessages.scrollTop = chatMessages.scrollHeight;
});

// Handle creating a new room
createRoomBtn.addEventListener('click', () => {
    const roomName = newRoomName.value.trim();
    if (roomName !== '') {
        socket.emit('joinRoom', roomName);
        newRoomName.value = '';
    }
});

// Receive confirmation of room join
socket.on('joinedRoom', (roomName) => {
    addNotification(`Joined room: ${roomName}`);
    // Clear existing chunks and load new ones
    chunks.clear();
    loadChunks();
    currentRoom = roomName;
    saveToLocalStorage(roomCookieName, currentRoom);
    // Clear chat messages
    chatMessages.innerHTML = '';
    // Update current room display if necessary
});

// Receive room list
socket.on('roomList', (rooms) => {
    roomList.innerHTML = '';
    rooms.forEach(room => {
        const roomEl = document.createElement('div');
        roomEl.attributes.class = 'room-btn';
        roomEl.textContent = room;
        roomEl.addEventListener('click', () => {
            socket.emit('joinRoom', room);
        });
        roomList.appendChild(roomEl);
    });
});

// Populate room list on initial load
socket.emit('requestRoomList');

// Handle adding friends
addFriendBtn.addEventListener('click', () => {
    const friendUsername = friendUsernameInput.value.trim();
    if (friendUsername !== '') {
        socket.emit('addFriend', friendUsername);
        friendUsernameInput.value = '';
    }
    saveToLocalStorage(friendsCookieName, Array.from(friends));
});

// Receive confirmation of friend addition
socket.on('friendAdded', (friendUsername) => {
    const friendEl = document.createElement('div');
    const statusEl = document.createElement('span');
    statusEl.classList.add('friend-status');
    statusEl.style.width = '10px';
    statusEl.style.height = '10px';
    statusEl.style.borderRadius = '50%';
    statusEl.style.backgroundColor = 'red'; // Offline by default
    statusEl.style.marginRight = '5px';
    friendEl.appendChild(statusEl);
    const nameEl = document.createElement('span');
    nameEl.textContent = friendUsername;
    friendEl.appendChild(nameEl);
    friendList.appendChild(friendEl);
});

// Handle receiving friend online status
socket.on('friendOnline', (friendUsername) => {
    const friends = Array.from(friendList.children);
    friends.forEach(friendEl => {
        if (friendEl.textContent.includes(friendUsername)) {
            const statusEl = friendEl.querySelector('.friend-status');
            statusEl.style.backgroundColor = 'green';
        }
    });
});

// Handle receiving friend offline status
socket.on('friendOffline', (friendUsername) => {
    const friends = Array.from(friendList.children);
    friends.forEach(friendEl => {
        if (friendEl.textContent.includes(friendUsername)) {
            const statusEl = friendEl.querySelector('.friend-status');
            statusEl.style.backgroundColor = 'red';
        }
    });
});

// Open DM when clicking on a friend
friendList.addEventListener('click', (event) => {
    const friendUsername = event.target.textContent;
    if (friendUsername) {
        openDM(friendUsername);
    }
});

// Function to open DM chat with a friend
function openDM(friendUsername) {
    dmChat.style.display = 'flex';
    window.currentDM = friendUsername;
    dmMessages.innerHTML = '';
    addNotification(`Chatting with ${friendUsername}`);
}

// Send DM
sendDmBtn.addEventListener('click', () => {
    const message = dmInput.value.trim();
    if (message && window.currentDM) {
        socket.emit('dm', { toUsername: window.currentDM, message });
        // Display sent message in DM chat
        const msgEl = document.createElement('div');
        msgEl.innerHTML = `<strong>You:</strong> ${message}`;
        dmMessages.appendChild(msgEl);
        dmInput.value = '';
    }
});

// Receive DMs
socket.on('dm', (data) => {
    const { from, message } = data;
    // Display received message
    const msgEl = document.createElement('div');
    msgEl.innerHTML = `<strong>${from}:</strong> ${message}`;
    dmMessages.appendChild(msgEl);
    dmChat.style.display = 'flex'; // Ensure DM chat is visible
    addNotification(`New DM from ${from}`);
});

// Handle avatar upload
avatarUpload.addEventListener('change', (event) => {
    const file = event.target.files[0];
    if (file) {
        const reader = new FileReader();
        reader.onload = function(e) {
            const avatarData = e.target.result;
            socket.emit('updateAvatar', avatarData);
            currentAvatarImg.src = avatarData;
        };
        reader.readAsDataURL(file);
    }
});

// Receive other users' avatars
socket.on('userAvatar', (data) => {
    const { socketId, avatar } = data;
});

// Receive other users' cursor movements
socket.on('cursorMove', (data) => {
    const { socketId, room, x, y } = data;
    if (room !== currentRoom) return;

    if (!cursors[socketId]) {
        // Create a cursor element
        const cursorEl = document.createElement('div');
        cursorEl.classList.add('cursor');
        document.getElementById('canvasContainer').appendChild(cursorEl);
        cursors[socketId] = cursorEl;
    }
    // Update cursor position
    cursors[socketId].style.left = `${x}px`;
    cursors[socketId].style.top = `${y}px`;
});

// Remove cursor when user disconnects
socket.on('cursorDisconnect', (data) => {
    const { socketId, room } = data;
    if (room !== currentRoom) return;
    if (cursors[socketId]) {
        cursors[socketId].remove();
        delete cursors[socketId];
    }
});

// Handle incoming pixel info
socket.on('pixelInfo', (info) => {
    if (info && info.room === currentRoom) {
        pixelInfo.style.display = 'block';
        const timeString = new Date(info.time).toLocaleString();
        pixelInfo.textContent = `${info.username}: ${info.color}\n[${info.x}, ${info.y}]\n(${timeString})`;
    } else {
        pixelInfo.style.display = 'none';
    }
});

// Handle user online/offline status
socket.on('userOnline', (username) => {
    // Update friend status if necessary
    const friendsArray = Array.from(friendList.children);
    friendsArray.forEach(friendEl => {
        if (friendEl.textContent.includes(username)) {
            const statusEl = friendEl.querySelector('.friend-status');
            statusEl.style.backgroundColor = 'green';
        }
    });
});

socket.on('userOffline', (username) => {
    // Update friend status if necessary
    const friendsArray = Array.from(friendList.children);
    friendsArray.forEach(friendEl => {
        if (friendEl.textContent.includes(username)) {
            const statusEl = friendEl.querySelector('.friend-status');
            statusEl.style.backgroundColor = 'red';
        }
    });
});

function addNotification(message) {
    const existingNotif = Array.from(notificationArea.children).find(
        notif => notif.textContent === message
    );

    if (!existingNotif) {
        const notifEl = document.createElement('div');
        notifEl.classList.add('notification');
        notifEl.textContent = message;
        notificationArea.appendChild(notifEl);

        setTimeout(() => {
            notifEl.remove();
        }, 5000);
    }
}

// Receive notifications
socket.on('notification', (data) => {
    const { type, message } = data;
    addNotification(message);
});

// Ensure initial canvas renders any existing pixels
function initialize() {
    resizeCanvas();
}

initialize();

// Minimap Interactivity: Click to navigate
minimap.addEventListener('click', (event) => {
    const rect = minimap.getBoundingClientRect();
    const clickX = event.clientX - rect.left;
    const clickY = event.clientY - rect.top;

    const mapClickX = clickX / minimap.width * MAP_WIDTH;
    const mapClickY = clickY / minimap.height * MAP_HEIGHT;

    // Center the main canvas on the clicked point
    offsetX = mapClickX - (canvas.width / (2 * pixelSize * zoom));
    offsetY = mapClickY - (canvas.height / (2 * pixelSize * zoom));

    // Clamp offsets
    const maxOffsetX = MAP_WIDTH - canvas.width / (pixelSize * zoom);
    const maxOffsetY = MAP_HEIGHT - canvas.height / (pixelSize * zoom);
    offsetX = Math.max(0, Math.min(offsetX, maxOffsetX));
    offsetY = Math.max(0, Math.min(offsetY, maxOffsetY));

    loadChunks();
});

// Disconnect Handling
socket.on('disconnect', () => {
    onlineUsersSpan.textContent = 0;
    addNotification('You have been disconnected.');
    // save all and reload page
    saveAllData();
    location.reload();
});
socket.emit('requestRoomList');


// Saving and loading

// Save data to localStorage
function saveToLocalStorage(key, value) {
    localStorage.setItem(key, JSON.stringify(value));
}

// Load data from localStorage
function loadFromLocalStorage(key, defaultValue) {
    const value = localStorage.getItem(key);
    return value ? JSON.parse(value) : defaultValue;
}

function saveAllData() {
    saveToLocalStorage(usernameCookieName, username);
    saveToLocalStorage(avatarCookieName, currentAvatarImg.src);
    saveToLocalStorage(friendsCookieName, Array.from(friends));
    saveToLocalStorage(roomCookieName, currentRoom);
    saveToLocalStorage(positionCookieName, { x: offsetX, y: offsetY });
    saveToLocalStorage(zoomCookieName, zoom);
    saveToLocalStorage(dmsCookieName, dmMessages.innerHTML);
    saveToLocalStorage(showPixelInfoCookieName, showPixelInfoCheckbox.checked);
    saveToLocalStorage(showGridCookieName, showGridCheckbox.checked);
}

// Load all data
function loadAllData() {
    console.log("Loading all data...");
    username = loadFromLocalStorage(usernameCookieName, 'Anonymous');
    console.log("Username: " + username);
    usernameInput.value = username;
    setUsernameBtn.click();

    const savedAvatar = loadFromLocalStorage(avatarCookieName, '');
    if (savedAvatar) currentAvatarImg.src = savedAvatar;

    friends = new Set(loadFromLocalStorage(friendsCookieName, []));
    friendList.innerHTML = '';
    friends.forEach(friend => {
        const friendEl = document.createElement('div');
        const statusEl = document.createElement('span');
        statusEl.classList.add('friend-status');
        statusEl.style.width = '10px';
        statusEl.style.height = '10px';
        statusEl.style.borderRadius = '50%';
        statusEl.style.backgroundColor = 'red'; // Offline by default
        statusEl.style.marginRight = '5px';
        friendEl.appendChild(statusEl);
        const nameEl = document.createElement('span');
        nameEl.textContent = friend;
        friendEl.appendChild(nameEl);
        friendList.appendChild(friendEl);
    });

    currentRoom = loadFromLocalStorage(roomCookieName, null);
    if (currentRoom) {
        joinRoom(currentRoom);
    }
    else {
        joinRoom('default');
    }

    const position = loadFromLocalStorage(positionCookieName, { x: MAP_WIDTH / 2, y: MAP_HEIGHT / 2 });
    offsetX = position.x;
    offsetY = position.y;

    zoom = loadFromLocalStorage(zoomCookieName, 1);

    dmMessages.innerHTML = loadFromLocalStorage(dmsCookieName, '');

    showPixelInfoCheckbox.checked = loadFromLocalStorage(showPixelInfoCookieName, true);
    showGridCheckbox.checked = loadFromLocalStorage(showGridCookieName, true);
}

avatarUpload.addEventListener('change', (e) => {
    const file = e.target.files[0];
    const reader = new FileReader();
    reader.onload = (event) => {
        currentAvatarImg.src = event.target.result;
        saveToLocalStorage(avatarCookieName, currentAvatarImg.src);
    };
    reader.readAsDataURL(file);
});

canvas.addEventListener('mouseup', () => {
    saveToLocalStorage(positionCookieName, { x: offsetX, y: offsetY });
});

sendDmBtn.addEventListener('click', () => {
    // Assuming you append the message to dmMessages when sent
    saveToLocalStorage(dmsCookieName, dmMessages.innerHTML);
});

function joinRoom(roomName) {
    socket.emit('joinRoom', roomName);
    newRoomName.value = '';
}

loadAllData();

window.addEventListener('beforeunload', () => {
    saveAllData();
});

setTimeout(() => {
    loadChunks();
}, 500);

setInterval(() => {
    loadChunks();
}, 1000);

// Add this script to your client-side JavaScript file

// Function to handle image selection and placement
function handleImagePlacement() {
    const input = document.createElement('input');
    input.type = 'file';
    input.accept = 'image/*';
    input.onchange = e => {
        const file = e.target.files[0];
        const reader = new FileReader();
        reader.onload = readerEvent => {
            const image = new Image();
            image.onload = () => showPositionInputs(image);
            image.src = readerEvent.target.result;
        }
        reader.readAsDataURL(file);
    }
    input.click();
}

// Function to show position inputs and trigger image processing
function showPositionInputs(image) {
    document.getElementById('placementConfirm').onclick = () => {
        const x = parseInt(document.getElementById('xPosition').value, 10);
        const y = parseInt(document.getElementById('yPosition').value, 10);
        processAndPlaceImage(image, x, y);
    };
}

// Function to process the image and place it on the canvas
function processAndPlaceImage(image, startX, startY) {
    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d');
    
    // Set maximum dimensions for the placed image
    const maxWidth = 100;
    const maxHeight = 100;
    
    // Calculate scaling factor
    const scale = Math.min(maxWidth / image.width, maxHeight / image.height);
    
    // Set canvas size to the scaled image size
    canvas.width = Math.floor(image.width * scale);
    canvas.height = Math.floor(image.height * scale);
    
    // Draw the image onto the canvas (this will handle the scaling)
    ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
    
    // Get the image data
    const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
    
    // Process the image data into pixels
    const pixels = [];
    for (let y = 0; y < canvas.height; y++) {
        for (let x = 0; x < canvas.width; x++) {
            const i = (y * canvas.width + x) * 4;
            const r = imageData.data[i];
            const g = imageData.data[i + 1];
            const b = imageData.data[i + 2];
            const a = imageData.data[i + 3];
            
            // Skip fully transparent pixels
            if (a === 0) continue;
            
            // Convert RGB to hex
            const color = `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
            
            // Add pixel to the list, adjusting for the start position
            pixels.push({ x: x + startX, y: y + startY, color });
        }
    }
    
    // Send the pixels to the server in batches
    const batchSize = 50;
    for (let i = 0; i < pixels.length; i += batchSize) {
        const batch = pixels.slice(i, i + batchSize);
        socket.emit('placeMultiplePixels', {
            room: currentRoom, // Assume currentRoom is defined elsewhere
            pixels: batch
        });
    }
}

const express = require('express');
const app = express();
const http = require('http').createServer(app);
const io = require('socket.io')(http);
const path = require('path');
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const bcrypt = require('bcrypt');
const session = require('express-session');
const { v4: uuidv4 } = require('uuid');
const fs = require('fs');

// Configuration
const PORT = process.env.PORT || 3000;
const CHUNK_SIZE = 100;
const MAP_WIDTH = 10000;
const MAP_HEIGHT = 10000;
const SAVE_FILE = 'save.txt';
const BACKUP_DIR = 'backups'; // Directory to store backups
const logPackets = true;

const saveBackups = false;

// Ensure backup directory exists
if (!fs.existsSync(BACKUP_DIR)) {
    fs.mkdirSync(BACKUP_DIR);
}

// In-memory data stores
const grids = {};
const users = new Map();
const usernames = new Map();

// Utility Functions

/**
 * Formats the current time to 'YYYY-MM-DD HH:MM' with minute precision.
 * @returns {string} - Formatted time string.
 */
function getCurrentFormattedTime() {
    const now = new Date();
    const year = now.getFullYear();
    const month = (`0${now.getMonth() + 1}`).slice(-2);
    const day = (`0${now.getDate()}`).slice(-2);
    const hours = (`0${now.getHours()}`).slice(-2);
    const minutes = (`0${now.getMinutes()}`).slice(-2);
    return `${year}-${month}-${day} ${hours}:${minutes}`;
}

/**
 * Validates if the given x and y positions are within the map boundaries.
 * @param {number} x - X-coordinate.
 * @param {number} y - Y-coordinate.
 * @returns {boolean} - True if valid, else false.
 */
function isValidPosition(x, y) {
    return x >= 0 && x < MAP_WIDTH && y >= 0 && y < MAP_HEIGHT;
}

// Compression Functions

/**
 * Compresses time, color, and username data by associating each group with their pixel positions.
 * Format: time|color|username|x1,y1;x2,y2;...
 * @param {string} time - The time identifier (minute precision).
 * @param {string} color - The color identifier.
 * @param {string} username - The username of the user.
 * @param {Array} pixels - Array of pixel objects with x and y.
 * @returns {string} - Compressed time, color, and username data string.
 */
function compressTimeColorUserData(time, color, username, pixels) {
    // Join each pixel's position with ';' as a separator
    const positions = pixels.map(pixel => `${pixel.x},${pixel.y}`).join(';');
    return `${time}|${color}|${username}|${positions}`;
}

/**
 * Decompresses time, color, and username data into its constituent pixels.
 * @param {string} data - Compressed time, color, and username data string.
 * @returns {Object} - Object containing time, color, username, and an array of pixel positions.
 */
function decompressTimeColorUserData(data) {
    const [time, color, username, positionsData] = data.split('|');
    const pixels = positionsData.split(';').map(posStr => {
        const [x, y] = posStr.split(',').map(Number);
        return { x, y };
    });
    return { time, color, username, pixels };
}

// Save and Load functions

/**
 * Saves the current state of all grids to the SAVE_FILE in a compressed format.
 */
function saveGrids() {
    // Create a backup before saving
    if (saveBackups && fs.existsSync(SAVE_FILE)) {
        const backupFile = path.join(BACKUP_DIR, `save_backup_${Date.now()}.txt`);
        const data = fs.readFileSync(SAVE_FILE);
        fs.writeFileSync(backupFile, data);
        console.log(`Backup created: ${backupFile}`);
    }

    const compressedData = Object.entries(grids).map(([room, grid]) => {
        // Object to hold (time + color + username) grouped pixel data
        const timeColorUserMap = {};

        grid.forEach((chunk, chunkKey) => {
            const [chunkX, chunkY] = chunkKey.split(',').map(Number);
            chunk.forEach((pixelData, pixelKey) => {
                const [xOffset, yOffset] = pixelKey.split(',').map(Number);
                const x = chunkX * CHUNK_SIZE + xOffset;
                const y = chunkY * CHUNK_SIZE + yOffset;
                const { color, username, time } = pixelData;

                const key = `${time}|${color}|${username}`;
                if (!timeColorUserMap[key]) {
                    timeColorUserMap[key] = [];
                }

                timeColorUserMap[key].push({ x, y });
            });
        });

        // Compress time, color, and username data
        const timeColorUserData = Object.entries(timeColorUserMap).map(([key, pixels]) => {
            const [time, color, username] = key.split('|');
            return compressTimeColorUserData(time, color, username, pixels);
        }).join('|');

        return `${room}|${timeColorUserData}`;
    }).join('\n');

    fs.writeFileSync(SAVE_FILE, compressedData);
    console.log('Grids successfully saved.');
}

/**
 * Loads the grids from the SAVE_FILE, reconstructing the in-memory data structures.
 */
function loadGrids() {
    if (!fs.existsSync(SAVE_FILE)) return;

    try {
        const data = fs.readFileSync(SAVE_FILE, 'utf8');

        data.split('\n').forEach(roomData => {
            if (!roomData) return; // Skip empty lines
            const firstPipe = roomData.indexOf('|');
            if (firstPipe === -1) {
                console.warn(`Invalid room data format: ${roomData}`);
                return;
            }
            const room = roomData.substring(0, firstPipe);
            const timeColorUserDataStr = roomData.substring(firstPipe + 1);
            grids[room] = new Map();

            // Split into groups by '|', every 4 tokens represent time, color, username, positions
            const tokens = timeColorUserDataStr.split('|');
            for (let i = 0; i < tokens.length;) {
                if (i + 3 >= tokens.length) {
                    console.warn(`Incomplete pixel group in room '${room}'.`);
                    break;
                }

                const time = tokens[i++];
                const color = tokens[i++];
                const username = tokens[i++];
                const positionsStr = tokens[i++];

                try {
                    const { pixels } = decompressTimeColorUserData(`${time}|${color}|${username}|${positionsStr}`);

                    pixels.forEach(pixel => {
                        const { x, y } = pixel;
                        const chunkX = Math.floor(x / CHUNK_SIZE);
                        const chunkY = Math.floor(y / CHUNK_SIZE);
                        const chunkKey = `${chunkX},${chunkY}`;
                        const pixelKey = `${x % CHUNK_SIZE},${y % CHUNK_SIZE}`;

                        if (!grids[room].has(chunkKey)) {
                            grids[room].set(chunkKey, new Map());
                        }

                        grids[room].get(chunkKey).set(pixelKey, { color, username, time });
                    });
                } catch (error) {
                    console.error(`Error decompressing pixel group: ${time}|${color}|${username}|${positionsStr}`);
                }
            }
        });

        console.log('Grids successfully loaded.');
    } catch (error) {
        console.error('Error loading grids:', error);
    }
}

// Load grids at startup
loadGrids();

// Middleware setup
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(express.static(path.join(__dirname, 'public')));
app.use(session({ 
    secret: process.env.SESSION_SECRET || 'gfdghvhgucjxigvjdfiohgdfihgfisdhgirhgirtfdgv', 
    resave: false, 
    saveUninitialized: false 
}));
app.use(passport.initialize());
app.use(passport.session());

// Passport configuration
passport.use(new LocalStrategy(
    function (username, password, done) {
        const user = Array.from(users.values()).find(u => u.username === username);
        if (!user) return done(null, false, { message: 'Incorrect username.' });
        bcrypt.compare(password, user.password, (err, res) => {
            if (err) return done(err);
            if (res) return done(null, user);
            else return done(null, false, { message: 'Incorrect password.' });
        });
    }
));

passport.serializeUser((user, done) => done(null, user.socketId));
passport.deserializeUser((socketId, done) => {
    const user = users.get(socketId);
    if (user) {
        done(null, user);
    } else {
        done(null, false);
    }
});

// Routes

/**
 * User Registration Route
 */
app.post('/register', async (req, res) => {
    const { username, password } = req.body;
    if (!username || !password) {
        return res.status(400).send('Username and password are required.');
    }
    if (Array.from(users.values()).some(user => user.username === username)) {
        return res.status(400).send('Username already exists.');
    }
    const hashedPassword = await bcrypt.hash(password, 10);
    const tempUser = { 
        id: uuidv4(), 
        username, 
        password: hashedPassword, 
        friends: new Set(), 
        avatar: null 
    };
    users.set(tempUser.id, tempUser); // Temporarily store the user
    res.status(201).send('User registered. Please login.');
});

/**
 * User Login Route
 */
app.post('/login', passport.authenticate('local'), (req, res) => {
    res.send('Logged in');
});

/**
 * Root Route - Serves the Client Interface
 */
app.get('/', (req, res) => {
    res.sendFile(path.join(__dirname, 'public', 'index.html'));
});

// Socket.io connection handling
io.on('connection', (socket) => {
    console.log('A user connected:', socket.id);

    // Initialize user data
    users.set(socket.id, { 
        username: 'Anonymous', 
        friends: new Set(), 
        avatar: null, 
        socketId: socket.id 
    });

    if (logPackets) {
        // Log incoming packets
        socket.onAny((eventName, ...args) => {
            console.log(`Received from ${socket.id}: ${eventName}`, JSON.stringify(args).length + " bytes");
        });

        // Log outgoing packets
        const originalEmit = socket.emit;
        socket.emit = function(eventName, ...args) {
            console.log(`Sent to ${socket.id}: ${eventName}`, JSON.stringify(args).length + " bytes");
            originalEmit.apply(this, [eventName, ...args]);
        };
    }

    // Notify about the number of online users
    io.emit('onlineUsers', users.size);

    /**
     * Set Username Event
     */
    socket.on('setUsername', (username) => {
        if (!username || typeof username !== 'string') {
            socket.emit('notification', { type: 'error', message: 'Invalid username.' });
            return;
        }
        const user = users.get(socket.id);
        if (!user) {
            socket.emit('notification', { type: 'error', message: 'User not found.' });
            return;
        }
        if (Array.from(users.values()).some(u => u.username === username)) {
            socket.emit('notification', { type: 'error', message: 'Username already taken.' });
            return;
        }
        if (user.username !== 'Anonymous') {
            usernames.delete(user.username);
            io.emit('userOffline', user.username);
        }
        user.username = username;
        users.set(socket.id, user);
        usernames.set(username, socket.id);
        io.emit('userOnline', username);
        socket.emit('notification', { type: 'success', message: `Username set to ${username}` });
        console.log(`User ${socket.id} set username to ${username}`);
    });

    /**
     * Join Room Event
     */
    socket.on('joinRoom', (roomName) => {
        if (!roomName || typeof roomName !== 'string') return;
        const previousRooms = Array.from(socket.rooms).filter(r => r !== socket.id);
        previousRooms.forEach(room => {
            socket.leave(room);
        });
        socket.join(roomName);
        if (!grids[roomName]) {
            grids[roomName] = new Map();
        }
        socket.emit('joinedRoom', roomName);
        console.log(`User ${socket.id} joined room ${roomName}`);
        const activeRooms = Array.from(io.sockets.adapter.rooms.keys()).filter(r => !users.has(r));
        io.emit('roomList', activeRooms);
    });

    /**
     * Request Room List Event
     */
    socket.on('requestRoomList', () => {
        const activeRooms = Array.from(io.sockets.adapter.rooms.keys()).filter(r => !users.has(r));
        socket.emit('roomList', activeRooms);
    });

    /**
     * Get Chunks Event
     */
    socket.on('getChunks', (data) => {
        const { room, chunks } = data;
        if (!room || !Array.isArray(chunks)) return;
        if (!grids[room]) {
            grids[room] = new Map();
        }
        const chunkData = {};
        chunks.forEach(chunk => {
            if (!grids[room].has(chunk)) {
                chunkData[chunk] = {};
                return;
            }
            const pixels = grids[room].get(chunk);
            const pixelsObj = {};
            pixels.forEach((pixelData, key) => {
                pixelsObj[key] = pixelData;
            });
            chunkData[chunk] = pixelsObj;
        });
        socket.emit('chunkData', { room, chunkData });
    });

    /**
     * Place Pixel Event
     */
    socket.on('placePixel', (data) => {
        const { room, x, y, color } = data;
        const user = users.get(socket.id);
        if (!user || !room) return;
        if (!isValidPosition(x, y)) {
            socket.emit('notification', { type: 'error', message: 'Invalid pixel position.' });
            return;
        }
        if (!color || typeof color !== 'string') {
            socket.emit('notification', { type: 'error', message: 'Invalid color.' });
            return;
        }
        if (!grids[room]) return;

        const time = getCurrentFormattedTime();

        const key = `${Math.floor(x / CHUNK_SIZE)},${Math.floor(y / CHUNK_SIZE)}`;
        if (!grids[room].has(key)) {
            grids[room].set(key, new Map());
        }

        const chunkMap = grids[room].get(key);
        const pixelKey = `${x % CHUNK_SIZE},${y % CHUNK_SIZE}`;

        if (color.toUpperCase() === '#FFFFFF') {
            // Remove pixel if color is white (assuming white is the default background)
            chunkMap.delete(pixelKey);
            io.to(room).emit('pixelUpdate', { 
                room, 
                chunkKey: key, 
                x: x % CHUNK_SIZE, 
                y: y % CHUNK_SIZE, 
                color: '#FFFFFF', 
                removed: true 
            });
        } else {
            const pixelData = { color, username: user.username, time };
            chunkMap.set(pixelKey, pixelData);
            io.to(room).emit('pixelUpdate', { 
                room, 
                chunkKey: key, 
                x: x % CHUNK_SIZE, 
                y: y % CHUNK_SIZE, 
                color 
            });
        }
    });

    /**
     * Get Pixel Info Event
     */
    socket.on('getPixelInfo', (data) => {
        const { room, x, y } = data;
        if (!room || typeof x !== 'number' || typeof y !== 'number') {
            socket.emit('pixelInfo', null);
            return;
        }
        if (!grids[room]) {
            socket.emit('pixelInfo', null);
            return;
        }
        const chunkKey = `${Math.floor(x / CHUNK_SIZE)},${Math.floor(y / CHUNK_SIZE)}`;
        if (!grids[room].has(chunkKey)) {
            socket.emit('pixelInfo', null);
            return;
        }
        const chunkMap = grids[room].get(chunkKey);
        const pixelKey = `${x % CHUNK_SIZE},${y % CHUNK_SIZE}`;
        if (!chunkMap.has(pixelKey)) {
            socket.emit('pixelInfo', null);
            return;
        }
        const pixelData = chunkMap.get(pixelKey);
        socket.emit('pixelInfo', { room, x, y, ...pixelData });
    });

    /**
     * Chat Message Event
     */
    socket.on('chatMessage', (data) => {
        const { room, message } = data;
        const user = users.get(socket.id);
        if (!user || !room || !message || typeof message !== 'string') return;
        io.to(room).emit('chatMessage', { room, username: user.username, message });
    });

    /**
     * Add Friend Event
     */
    socket.on('addFriend', (friendUsername) => {
        if (!friendUsername || typeof friendUsername !== 'string') {
            socket.emit('notification', { type: 'error', message: 'Invalid friend username.' });
            return;
        }
        const user = users.get(socket.id);
        if (!user) return;
        if (user.friends.has(friendUsername)) {
            socket.emit('notification', { type: 'error', message: `${friendUsername} is already your friend.` });
            return;
        }
        if (!usernames.has(friendUsername)) {
            socket.emit('notification', { type: 'error', message: `${friendUsername} does not exist or is not online.` });
            return;
        }
        user.friends.add(friendUsername);
        users.set(socket.id, user);
        const friendSocketId = usernames.get(friendUsername);
        const friend = users.get(friendSocketId);
        if (friend) {
            friend.friends.add(user.username);
            users.set(friendSocketId, friend);
            io.to(friendSocketId).emit('friendAdded', user.username);
            socket.emit('friendAdded', friend.username);
            socket.emit('notification', { type: 'success', message: `Added ${friendUsername} as a friend.` });
        }
    });

    /**
     * Direct Message (DM) Event
     */
    socket.on('dm', (data) => {
        const { toUsername, message } = data;
        if (!toUsername || typeof toUsername !== 'string' || !message || typeof message !== 'string') {
            socket.emit('notification', { type: 'error', message: 'Invalid DM data.' });
            return;
        }
        const recipientSocketId = usernames.get(toUsername);
        if (recipientSocketId && users.has(recipientSocketId)) {
            const sender = users.get(socket.id);
            io.to(recipientSocketId).emit('dm', { from: sender.username, message });
            io.to(recipientSocketId).emit('notification', { type: 'dm', message: `New DM from ${sender.username}` });
            socket.emit('notification', { type: 'success', message: `DM sent to ${toUsername}` });
        } else {
            socket.emit('notification', { type: 'error', message: `${toUsername} is not online or does not exist.` });
        }
    });

    /**
     * Update Avatar Event
     */
    socket.on('updateAvatar', (avatarData) => {
        if (!avatarData || typeof avatarData !== 'string') {
            socket.emit('notification', { type: 'error', message: 'Invalid avatar data.' });
            return;
        }
        const user = users.get(socket.id);
        if (!user) return;
        user.avatar = avatarData;
        users.set(socket.id, user);
        socket.broadcast.emit('userAvatar', { socketId: socket.id, avatar: avatarData });
    });

    /**
     * User Disconnect Event
     */
    socket.on('disconnect', () => {
        console.log('A user disconnected:', socket.id);
        const user = users.get(socket.id);
        if (user && user.username !== 'Anonymous') {
            io.emit('userOffline', user.username);
            usernames.delete(user.username);
        }
        users.delete(socket.id);
        io.emit('onlineUsers', users.size);
    });
});

// Start the server
http.listen(PORT, "0.0.0.0", () => {
    console.log(`Server running on http://0.0.0.0:${PORT}`);
});

// Save grids on server shutdown
process.on('SIGINT', () => {
    console.log('Saving grids before shutting down...');
    saveGrids();
    console.log('Grids saved. Shutting down.');
    process.exit();
});

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="description" content="k/place is a free, multiplayer pixel art editor.">
    <meta name="keywords" content="k/place, r/place, pixel art, multiplayer, editor, game, online, free">
    <title>k/place</title>
    <!-- Google Fonts for Modern Typography -->
    <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <header>
        <h1 onclick="location.reload();">k/place</h1>
        <div class="user-info">
            <img id="currentAvatarImg" src="/default-avatar.png" alt="Your Avatar">
            <span id="currentUsername">Anonymous</span>
        </div>
    </header>
    <main>
        <!-- Left Side Drawer -->
        <div id="drawer">
            <!-- Tools Section -->
            <section id="toolsSection">
                <h3>Tools</h3>
                <!-- Color Palette -->
                <div class="color-palette" id="colorPalette">
                    <!-- Predefined 16 Colors -->
                    <button class="color-btn selected" style="background-color: #000000;" data-color="#000000"></button>
                    <button class="color-btn" style="background-color: #FFFFFF;" data-color="#FFFFFF"></button>
                    <button class="color-btn" style="background-color: #FF0000;" data-color="#FF0000"></button>
                    <button class="color-btn" style="background-color: #00FF00;" data-color="#00FF00"></button>
                    <button class="color-btn" style="background-color: #0000FF;" data-color="#0000FF"></button>
                    <button class="color-btn" style="background-color: #FFFF00;" data-color="#FFFF00"></button>
                    <button class="color-btn" style="background-color: #00FFFF;" data-color="#00FFFF"></button>
                    <button class="color-btn" style="background-color: #FF00FF;" data-color="#FF00FF"></button>
                    <button class="color-btn" style="background-color: #800000;" data-color="#800000"></button>
                    <button class="color-btn" style="background-color: #808000;" data-color="#808000"></button>
                    <button class="color-btn" style="background-color: #008000;" data-color="#008000"></button>
                    <button class="color-btn" style="background-color: #800080;" data-color="#800080"></button>
                    <button class="color-btn" style="background-color: #008080;" data-color="#008080"></button>
                    <button class="color-btn" style="background-color: #000080;" data-color="#000080"></button>
                    <button class="color-btn" style="background-color: #FFA500;" data-color="#FFA500"></button>
                    <button class="color-btn" style="background-color: #A52A2A;" data-color="#A52A2A"></button>
                </div>
                <!-- Color Picker -->
                <div class="color-picker">
                    <input type="color" id="customColor" value="#000000">
                    <label for="customColor">Custom Color</label>
                </div>
            </section>

            <!-- Username Section -->
            <section id="usernameSection">
                <h3>Username</h3>
                <input type="text" id="username" placeholder="Enter username">
                <button id="setUsername">Set Username</button>
            </section>

            <section id="onlineUsersSection">
                Online users: <span id="onlineUsers">0</span>
            </section>

            <!-- Chat Section -->
            <section id="chatSection">
                <h3>Chat</h3>
                <div id="chatMessages"></div>
                <input type="text" id="chatInput" placeholder="Type a message...">
            </section>

            <!-- Room Section -->
            <section id="roomSection">
                <h3>Rooms</h3>
                <div id="roomList"></div>
                <input type="text" id="newRoomName" placeholder="Room name">
                <button id="createRoom">Join/Create Room</button>
            </section>

            <!-- Friends Section -->
            <section id="friendsSection">
                <h3>Friends</h3>
                <div id="friendList"></div>
                <input type="text" id="friendUsername" placeholder="Friend's username">
                <button id="addFriend">Add Friend</button>
            </section>

            <!-- DM Section -->
            <section id="dmSection">
                <h3>Direct Messages</h3>
                <div id="dmChat">
                    <div id="dmMessages"></div>
                    <input type="text" id="dmInput" placeholder="Type a message...">
                    <button id="sendDm">Send</button>
                </div>
            </section>

            <section id="settingsSection">
                <h3>Settings</h3>
                <input type="checkbox" id="showPixelInfoCheckbox">
                <label for="showPixelInfoCheckbox">Show pixel info</label><br>
                <input type="checkbox" id="showGridCheckbox" checked>
                <label for="showGridCheckbox">Show grid when zoomed</label>
            </section>

            <!-- Avatar Section -->
            <section id="avatarSection">
                <h3>Avatar</h3>
                <input type="file" id="avatarUpload" accept="image/*">
                <img id="currentAvatarImg" src="/default-avatar.png" alt="Your Avatar">
            </section>
        </div>
        
        <!-- Canvas Container -->
        <div id="canvasContainer">
            <canvas id="grid"></canvas>
            <canvas id="minimap"></canvas>
            <div id="pixelInfo"></div>
            <div id="notificationArea"></div>
        </div>
    </main>
    <!-- Include Socket.io client library -->
    <script src="/socket.io/socket.io.js"></script>
    <script src="script.js"></script>
</body>
</html>

add a live update of the cursor positions of the users. broadcast the current position of your cursor each time it stops moving for 1 second, and if the position is not the same as the previous one. by broadcast i mean it sends to the server and then the server sends it to everyone else in the same room. the indicator should render a point, an avatar next to it (the user's avatar in a circle image, ask the client of the user for it and it will respond), their username and position on the map. only people in the same room will be able to see their dot.