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 = 16;
const MAP_WIDTH = 10000;
const MAP_HEIGHT = 10000;
const SAVE_FILE = 'save.txt';

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

// Compression functions
function compressPixel(x, y, color) {
    return `${x},${y}:${color}`;
}

function decompressPixel(data) {
    const [pos, color] = data.split(':');
    const [x, y] = pos.split(',').map(Number);
    return { x, y, color };
}

function compressRoom(room, pixels) {
    return `${room}|${pixels.join(';')}`;
}

function decompressRoom(data) {
    const [room, pixelsData] = data.split('|');
    const pixels = pixelsData.split(';').map(decompressPixel);
    return { room, pixels };
}

// Save and Load functions
function saveGrids() {
    const compressedData = Object.entries(grids).map(([room, grid]) => {
        const pixels = [];
        grid.forEach((chunk, chunkKey) => {
            const [chunkX, chunkY] = chunkKey.split(',').map(Number);
            chunk.forEach((pixelData, pixelKey) => {
                const [x, y] = pixelKey.split(',').map(Number);
                pixels.push(compressPixel(
                    chunkX * CHUNK_SIZE + x,
                    chunkY * CHUNK_SIZE + y,
                    pixelData.color
                ));
            });
        });
        return compressRoom(room, pixels);
    }).join('\n');

    fs.writeFileSync(SAVE_FILE, compressedData);
}

function loadGrids() {
    if (!fs.existsSync(SAVE_FILE)) return;

    const data = fs.readFileSync(SAVE_FILE, 'utf8');
    data.split('\n').forEach(roomData => {
        const { room, pixels } = decompressRoom(roomData);
        grids[room] = new Map();
        pixels.forEach(pixel => {
            const chunkKey = `${Math.floor(pixel.x / CHUNK_SIZE)},${Math.floor(pixel.y / CHUNK_SIZE)}`;
            const pixelKey = `${pixel.x % CHUNK_SIZE},${pixel.y % CHUNK_SIZE}`;
            if (!grids[room].has(chunkKey)) {
                grids[room].set(chunkKey, new Map());
            }
            grids[room].get(chunkKey).set(pixelKey, { color: pixel.color, username: 'Unknown', time: Date.now() });
        });
    });
}

// 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: 'yourSecret', 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 (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) => done(null, users.get(socketId)));

// Routes
app.post('/register', async (req, res) => {
    const { username, password } = req.body;
    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 };
    res.status(201).send('User registered. Please login.');
});

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

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);

    users.set(socket.id, { username: 'Anonymous', friends: new Set(), avatar: null, socketId: socket.id });
    io.emit('onlineUsers', users.size);

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

    socket.on('joinRoom', (roomName) => {
        if (!roomName) 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);
    });

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

    socket.on('getChunks', (data) => {
        const { room, chunks } = data;
        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 });
    });

    socket.on('placePixel', (data) => {
      const { room, x, y, color, username } = data;
      if (!grids[room]) return;
  
      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 === '#FFFFFF') {
          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, time: Date.now() };
          chunkMap.set(pixelKey, pixelData);
          io.to(room).emit('pixelUpdate', { room, chunkKey: key, x: x % CHUNK_SIZE, y: y % CHUNK_SIZE, color });
      }
  });

    socket.on('getPixelInfo', (data) => {
        const { room, x, y } = data;
        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 });
    });

    socket.on('chatMessage', (data) => {
        const { room, username, message } = data;
        io.to(room).emit('chatMessage', { room, username, message });
    });

    socket.on('addFriend', (friendUsername) => {
        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.` });
            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.` });
        }
    });

    socket.on('dm', (data) => {
        const { toUsername, message } = data;
        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.` });
        }
    });

    socket.on('updateAvatar', (avatarData) => {
        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 });
    });

    socket.on('cursorMove', (data) => {
        const { room, x, y } = data;
        socket.broadcast.to(room).emit('cursorMove', { socketId: socket.id, room, x, y });
    });

    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, () => {
    console.log(`Server running on port ${PORT}`);
});

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

save and load in an even more compressed way by only defining the color once and then specifying the positions after it, they will be colored with the latest defined color. Also store usernames associated with the pixels that way. do not store times.