/*
 * chat.c - Shared memory chat program for local Linux users
 *
 * Multiple users (up to 8) communicate via a shared memory circular queue.
 *
 * Shared memory layout:
 *   uint32_t putq[4]          - 4 putq indices (pure queue positions)
 *   uint8_t  putq_valid[4]    - separate valid flags for each putq
 *   struct user_slot[8]       - per-user chat id + getq index
 *   char queue[QUEUE_SIZE]    - 2^18 byte circular character queue
 *
 * Build: gcc -Wall -o ../bin/chat chat.c
 *
 * Pass 1 fixes:
 *   - Sender header now only printed when the writer changes
 *   - Newline only output when the writer actually types Enter
 * Pass 2 fix:
 *   - Raw mode disables ICRNL so Enter produces \r not \n;
 *     display_output now translates \r to \r\n for proper line advance
 * Pass 3 fixes:
 *   - Backspace (BS 0x08 and DEL 0x7F) now visually erases the
 *     previous character on output via \b \b sequence
 *   - Ctrl-D (EOT 0x04) now exits cleanly; in raw mode with ISIG off,
 *     Ctrl-D is delivered as a literal byte, not as EOF from read()
 * Pass 4 fixes:
 *   - Removed stdout from select() fd_set when circular queue has no
 *     pending data; eliminates 100% CPU spin (select now truly sleeps)
 *   - Removed the count/variable-timeout workaround
 *   - Added file-lock (fcntl F_SETLKW) around putq read-modify-write
 *     to prevent concurrent writers from corrupting the queue index
 * Pass 5 fixes:
 *   - Separated putq valid flags into a dedicated uint8_t[4] array
 *     so putq indices are pure queue positions, never mixed with flags
 *   - Shared memory already used 0660 permissions (group rw, no other);
 *     lock file now also created with 0660 for consistency
 *   - Eliminated race-window display_output calls that bypassed
 *     select()'s stdout-ready check; output now only occurs when
 *     select() has confirmed stdout is writable, fixing 100% CPU
 * Pass 6 feature:
 *   - Detect join/leave by snapshotting user slots each iteration;
 *     prints "user has joined/left the chat" on changes
 *   - Works across any program sharing the same shared memory layout
 */

/* Copyright (c) 2026 Carl L. Wuebker & Claude.ai Opus 4.6

 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:

 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.

 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
 * IN THE SOFTWARE.

 * Note that Claude.ai Opus 4.6, guided by my requests, testing & feedback,
 * wrote the code.  I've minimally tested the code, so there still may be bugs.
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <termios.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/select.h>
#include <sys/types.h>
#include <errno.h>
#include <pwd.h>
#include <fcntl.h>
#include <stdint.h>

#define QUEUE_BITS    18
#define QUEUE_SIZE    (1u << QUEUE_BITS)       /* 262144 */
#define QUEUE_MASK    (QUEUE_SIZE - 1)         /* 0x3FFFF */

#define MAX_USERS     8
#define MAX_PUTQ      4
#define CHATID_LEN    64

#define SHM_KEY       0x43483131               /* "CH11" */
#define LOCK_PATH     "/tmp/.chat11.lock"

/* ── shared memory structures ──────────────────────────────────── */

struct user_slot {
    char     chatid[CHATID_LEN];
    uint32_t getq;
};

struct shm_header {
    uint32_t         putq[MAX_PUTQ];
    uint8_t          putq_valid[MAX_PUTQ];
    struct user_slot users[MAX_USERS];
    char             queue[QUEUE_SIZE];
};

/* ── globals ───────────────────────────────────────────────────── */

static struct shm_header *shm;
static int                my_slot   = -1;
static int                shm_id    = -1;
static int                lock_fd   = -1;
static struct termios     orig_term;
static int                term_set  = 0;
static char               last_sender[CHATID_LEN];
static char               prev_ids[MAX_USERS][CHATID_LEN];  /* presence snapshot */

/* ── file lock helpers ─────────────────────────────────────────── */

static void open_lockfile(void)
{
    lock_fd = open(LOCK_PATH, O_RDWR | O_CREAT, 0660);
    if (lock_fd < 0) {
        perror("open lockfile");
        exit(1);
    }
}

static void acquire_lock(void)
{
    struct flock fl;

    memset(&fl, 0, sizeof(fl));
    fl.l_type   = F_WRLCK;
    fl.l_whence = SEEK_SET;
    fl.l_start  = 0;
    fl.l_len    = 0;          /* lock the entire file */

    while (fcntl(lock_fd, F_SETLKW, &fl) < 0) {
        if (errno == EINTR)
            continue;
        perror("fcntl lock");
        exit(1);
    }
}

static void release_lock(void)
{
    struct flock fl;

    memset(&fl, 0, sizeof(fl));
    fl.l_type   = F_UNLCK;
    fl.l_whence = SEEK_SET;
    fl.l_start  = 0;
    fl.l_len    = 0;

    fcntl(lock_fd, F_SETLK, &fl);
}

/* ── terminal helpers ──────────────────────────────────────────── */

static void restore_terminal(void)
{
    if (term_set)
        tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_term);
}

static void set_raw_mode(void)
{
    struct termios raw;

    if (tcgetattr(STDIN_FILENO, &orig_term) < 0) {
        perror("tcgetattr");
        exit(1);
    }
    term_set = 1;
    atexit(restore_terminal);

    raw = orig_term;
    raw.c_lflag &= ~(ICANON | ECHO | ISIG);
    raw.c_iflag &= ~(IXON | ICRNL | BRKINT);
    raw.c_cc[VMIN]  = 0;
    raw.c_cc[VTIME] = 0;

    if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) < 0) {
        perror("tcsetattr");
        exit(1);
    }
}

/* ── cleanup on exit ───────────────────────────────────────────── */

static void cleanup(void)
{
    int i, active;

    if (my_slot >= 0 && shm) {
        shm->users[my_slot].chatid[0] = '\0';
        shm->users[my_slot].getq      = 0;
    }

    /* if no users remain, remove the shared memory segment */
    if (shm) {
        active = 0;
        for (i = 0; i < MAX_USERS; i++) {
            if (shm->users[i].chatid[0] != '\0')
                active++;
        }
        if (active == 0 && shm_id >= 0)
            shmctl(shm_id, IPC_RMID, NULL);
    }

    if (lock_fd >= 0)
        close(lock_fd);
}

static void signal_handler(int sig)
{
    (void)sig;
    cleanup();
    restore_terminal();
    _exit(0);
}

/* ── index helpers ─────────────────────────────────────────────── */

/* mask to a valid queue position 0..QUEUE_MASK */
static inline uint32_t idx(uint32_t v)
{
    return v & QUEUE_MASK;
}

/* ── check whether the queue has data for this slot ────────────── */

static int queue_has_data(int slot)
{
    uint32_t rd = idx(shm->users[slot].getq);
    uint32_t wr = idx(shm->putq[3]);
    return rd != wr;
}

/* ── putQ: copy bytes into the circular queue ──────────────────── */

static void putQ(const char *data, size_t len)
{
    uint32_t wr = idx(shm->putq[3]);
    size_t   i;

    for (i = 0; i < len; i++) {
        uint32_t next = idx(wr + 1);
        int      j;

        /* advance any lagging reader whose getq equals next */
        for (j = 0; j < MAX_USERS; j++) {
            if (shm->users[j].chatid[0] == '\0')
                continue;
            if (idx(shm->users[j].getq) == next)
                shm->users[j].getq = idx(next + 1);
        }

        shm->queue[wr] = data[i];
        wr = next;
    }

    shm->putq[3] = wr;
    shm->putq_valid[3] = 1;
}

/* ── getQ: read one byte from the circular queue ───────────────── */

static int getQ(int slot, char *out)
{
    uint32_t rd = idx(shm->users[slot].getq);
    uint32_t wr = idx(shm->putq[3]);

    if (rd == wr)
        return 0;   /* caught up */

    *out = shm->queue[rd];
    shm->users[slot].getq = idx(rd + 1);
    return 1;
}

/* ── build chat id ─────────────────────────────────────────────── */

static void build_chatid(char *buf, size_t buflen)
{
    struct passwd *pw  = getpwuid(getuid());
    const char    *usr = pw ? pw->pw_name : "unknown";
    int            i, dup;

    /* first try: bare username */
    snprintf(buf, buflen, "%s", usr);

    /* check if another running chat already has this id */
    if (!shm)
        return;

    dup = 0;
    for (i = 0; i < MAX_USERS; i++) {
        if (shm->users[i].chatid[0] != '\0' &&
            strcmp(shm->users[i].chatid, buf) == 0) {
            dup = 1;
            break;
        }
    }

    if (dup)
        snprintf(buf, buflen, "%s_%d", usr, (int)getpid());
}

/* ── attach / create shared memory ─────────────────────────────── */

static void attach_shm(void)
{
    size_t sz = sizeof(struct shm_header);
    int    created = 0;

    /* try to open existing */
    shm_id = shmget(SHM_KEY, sz, 0660);
    if (shm_id < 0) {
        /* create new */
        shm_id = shmget(SHM_KEY, sz, IPC_CREAT | IPC_EXCL | 0660);
        if (shm_id < 0) {
            /* race: someone else created it between our two calls */
            shm_id = shmget(SHM_KEY, sz, 0660);
            if (shm_id < 0) {
                perror("shmget");
                exit(1);
            }
        } else {
            created = 1;
        }
    }

    shm = shmat(shm_id, NULL, 0);
    if (shm == (void *)-1) {
        perror("shmat");
        exit(1);
    }

    if (created)
        memset(shm, 0, sz);
}

/* ── find a free user slot ─────────────────────────────────────── */

static int find_slot(const char *chatid)
{
    int i;

    for (i = 0; i < MAX_USERS; i++) {
        if (shm->users[i].chatid[0] == '\0') {
            strncpy(shm->users[i].chatid, chatid, CHATID_LEN - 1);
            shm->users[i].chatid[CHATID_LEN - 1] = '\0';
            return i;
        }
    }
    return -1;
}

/* ── initialise new joiner's getq from oldest valid putq ───────── */

static void init_getq(int slot)
{
    int i;

    for (i = 0; i < MAX_PUTQ; i++) {
        if (shm->putq_valid[i]) {
            shm->users[slot].getq = idx(shm->putq[i]);
            return;
        }
    }
    /* no valid putq found — start at current write head */
    shm->users[slot].getq = idx(shm->putq[3]);
}

/* ── detect join / leave ───────────────────────────────────────── */

/*
 * Compare current user slots against previous snapshot.
 * Print join/leave messages for any changes, then update snapshot.
 * Works regardless of which program (chat10, chat, etc.) is
 * using the shared memory, since it reads the slot table directly.
 */
static void check_presence(void)
{
    char curr_ids[MAX_USERS][CHATID_LEN];
    int  i, j, found;

    /* take a snapshot of current slot occupancy */
    for (i = 0; i < MAX_USERS; i++) {
        if (shm->users[i].chatid[0] != '\0') {
            strncpy(curr_ids[i], shm->users[i].chatid, CHATID_LEN - 1);
            curr_ids[i][CHATID_LEN - 1] = '\0';
        } else {
            curr_ids[i][0] = '\0';
        }
    }

    /* detect leaves: id was in prev but not in curr */
    for (i = 0; i < MAX_USERS; i++) {
        if (prev_ids[i][0] == '\0')
            continue;
        if (i == my_slot)
            continue;   /* don't announce ourselves */
        found = 0;
        for (j = 0; j < MAX_USERS; j++) {
            if (strcmp(prev_ids[i], curr_ids[j]) == 0) {
                found = 1;
                break;
            }
        }
        if (!found)
            printf("\n*** %s has left the chat ***\n", prev_ids[i]);
    }

    /* detect joins: id is in curr but not in prev */
    for (i = 0; i < MAX_USERS; i++) {
        if (curr_ids[i][0] == '\0')
            continue;
        if (i == my_slot)
            continue;   /* don't announce ourselves */
        found = 0;
        for (j = 0; j < MAX_USERS; j++) {
            if (strcmp(curr_ids[i], prev_ids[j]) == 0) {
                found = 1;
                break;
            }
        }
        if (!found)
            printf("\n*** %s has joined the chat ***\n", curr_ids[i]);
    }

    /* update snapshot */
    memcpy(prev_ids, curr_ids, sizeof(prev_ids));
}

/* ── display incoming data ─────────────────────────────────────── */

/*
 * Parse state for the queue stream:
 *   STATE_IDLE  - between messages, expecting start of a chat id
 *   STATE_ID    - reading chat id bytes until NUL terminator
 *   STATE_TEXT  - reading message text bytes until NUL terminator
 */
enum parse_state { STATE_IDLE, STATE_ID, STATE_TEXT };

static enum parse_state pstate = STATE_IDLE;
static char             sender[CHATID_LEN];
static size_t           id_pos;

static void display_output(int slot)
{
    char ch;

    while (getQ(slot, &ch)) {
        switch (pstate) {

        case STATE_IDLE:
            if (ch == '\0')
                break;              /* skip stray NULs between messages */
            /* first byte of a new sender id */
            memset(sender, 0, sizeof(sender));
            id_pos = 0;
            sender[id_pos++] = ch;
            pstate = STATE_ID;
            break;

        case STATE_ID:
            if (ch == '\0') {
                /* sender id complete — print header only if writer changed */
                if (strcmp(sender, last_sender) != 0) {
                    printf("\n%s:\n", sender);
                    strncpy(last_sender, sender, CHATID_LEN - 1);
                    last_sender[CHATID_LEN - 1] = '\0';
                }
                pstate = STATE_TEXT;
            } else {
                if (id_pos < CHATID_LEN - 1)
                    sender[id_pos++] = ch;
            }
            break;

        case STATE_TEXT:
            if (ch == '\0') {
                /* end of message text — back to idle */
                pstate = STATE_IDLE;
            } else if (ch == '\r') {
                /* raw mode: Enter sends \r — emit \r\n for proper line feed */
                fputs("\r\n", stdout);
            } else if (ch == 0x08 || ch == 0x7F) {
                /* backspace (BS) or delete (DEL): erase previous character */
                fputs("\b \b", stdout);
            } else {
                putchar(ch);
            }
            break;
        }
    }
    fflush(stdout);
}

/* ── main ──────────────────────────────────────────────────────── */

int main(void)
{
    char    chatid[CHATID_LEN];
    char    inbuf[65536];
    fd_set  rfds, wfds;
    struct  timeval tv;
    int     ret, nfds, pending;

    attach_shm();
    open_lockfile();
    build_chatid(chatid, sizeof(chatid));

    my_slot = find_slot(chatid);
    if (my_slot < 0) {
        fprintf(stderr, "chat: all %d slots full\n", MAX_USERS);
        shmdt(shm);
        return 1;
    }

    init_getq(my_slot);

    /* take initial snapshot of who is present (before announcing) */
    {
        int i;
        for (i = 0; i < MAX_USERS; i++) {
            if (shm->users[i].chatid[0] != '\0') {
                strncpy(prev_ids[i], shm->users[i].chatid, CHATID_LEN - 1);
                prev_ids[i][CHATID_LEN - 1] = '\0';
            } else {
                prev_ids[i][0] = '\0';
            }
        }
    }

    signal(SIGINT,  signal_handler);
    signal(SIGTERM, signal_handler);

    set_raw_mode();

    printf("chat: joined as \"%s\" (slot %d). Ctrl-C or Ctrl-D to quit.\n",
           chatid, my_slot);
    fflush(stdout);

    last_sender[0] = '\0';

    for (;;) {
        FD_ZERO(&rfds);
        FD_ZERO(&wfds);
        FD_SET(STDIN_FILENO, &rfds);
        nfds = STDIN_FILENO + 1;

        /*
         * Only ask select() about stdout when the queue actually has
         * data for us.  This prevents select() from returning
         * immediately (stdout is almost always writable), which was
         * causing 100% CPU usage.
         */
        pending = queue_has_data(my_slot);
        if (pending) {
            FD_SET(STDOUT_FILENO, &wfds);
            if (STDOUT_FILENO + 1 > nfds)
                nfds = STDOUT_FILENO + 1;
        }

        tv.tv_sec  = 0;
        tv.tv_usec = 50000;   /* 50 ms poll for new queue data */

        ret = select(nfds, &rfds, pending ? &wfds : NULL, NULL, &tv);
        if (ret < 0) {
            if (errno == EINTR)
                continue;
            perror("select");
            break;
        }

        /* ── check for join / leave events ──────────────────── */
        check_presence();

        /* ── input ready: read keystrokes ────────────────────── */
        if (FD_ISSET(STDIN_FILENO, &rfds)) {
            ssize_t n = read(STDIN_FILENO, inbuf, sizeof(inbuf) - 1);
            if (n <= 0)
                break;   /* EOF / error */

            /* check for Ctrl-C or Ctrl-D (raw mode, ISIG off) */
            for (ssize_t k = 0; k < n; k++) {
                if (inbuf[k] == 3 || inbuf[k] == 4) {   /* ETX or EOT */
                    goto done;
                }
            }

            /* ── locked section: write to the circular queue ── */
            acquire_lock();

            /* shift putq history (indices and valid flags) */
            shm->putq[0] = shm->putq[1];
            shm->putq[1] = shm->putq[2];
            shm->putq[2] = shm->putq[3];
            shm->putq_valid[0] = shm->putq_valid[1];
            shm->putq_valid[1] = shm->putq_valid[2];
            shm->putq_valid[2] = shm->putq_valid[3];

            /* reset putq[3] to current write position */
            shm->putq[3] = shm->putq[2];
            shm->putq_valid[3] = 0;

            /* write sender id (NUL-terminated) */
            putQ(chatid, strlen(chatid) + 1);

            /* write message text (NUL-terminated) */
            inbuf[n] = '\0';
            putQ(inbuf, (size_t)n + 1);

            release_lock();
        }

        /* ── output ready: display incoming ─────────────────── */
        if (pending && FD_ISSET(STDOUT_FILENO, &wfds)) {
            display_output(my_slot);
        }

        /*
         * If select() timed out and new data may have arrived between
         * queue_has_data() and select(), do NOT display here — just
         * loop back so the next iteration adds stdout to the wfds
         * and lets select() confirm it is writable.
         */
    }

done:
    cleanup();
    printf("\nchat: goodbye.\n");
    return 0;
}
