/*
 * Program for playing melodyes on PC-Speaker.
 * For GNU/Linux 64 bit version.
 * Version: 6 (with ALSA support).
 * Copyright (C) 2021-2026  Artyomov Alexander
 * http://self-made-free.ru/
 * aralni@mail.ru
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

#define _GNU_SOURCE
#include <sys/io.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <linux/input.h>
#include <stdint.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <errno.h>
#include <alsa/asoundlib.h>

/* Include our new unique check library (https://codeberg.org/Alexander2024/libuniquecheck) */
#include <uniquecheck.h>

// gcc -O4 -fPIE -pie gorg64_spkplay.c -o gorg64_spkplay -lm -lasound -luniquecheck

int efd;
int evdev = 0;
int alsa_mode = 0;
snd_pcm_t *alsa_handle = NULL;
unsigned int alsa_sample_rate = 48000;

typedef struct {
    unsigned short tone;
    unsigned short duration;
} ttw;

short spkf(float tone)
{
    int64_t tmp;
    if (tone < 1) { return 0; }
    tmp = round(1193182 / tone);
    if (tmp > 0xFFFF) { tmp = 0xFFFF; }
    return (short)tmp;
}

void spkon()
{
    outb(inb(0x61) | 3, 0x61);
}

void spkoff()
{
    outb(inb(0x61) & 0xFC, 0x61);
}

void spk(short t)
{
    outb(0xB6, 0x43);
    outb(t & 0xff, 0x42);
    outb((t >> 8) & 0xff, 0x42);
}

void kspk(short t)
{
    syscall(1002, t);
}

void kspkon()
{
    syscall(1000);
}

void kspkoff()
{
    syscall(1001);
}

void (*gspkon)() = NULL;
void (*gspkoff)() = NULL;
void (*gspk)(short t) = NULL;

// ==================== ALSA implementation ====================

// Generate sine wave samples
static void generate_sine(unsigned char *buffer, int frames, int channels, 
                          int freq, int volume, int sample_rate)
{
    static double phase = 0.0;
    double phase_increment = 2.0 * M_PI * freq / sample_rate;
    int i, ch;
    
    for (i = 0; i < frames; i++) {
        int sample = (int)(128 + sin(phase) * volume * 1.27); // volume 0-100 -> amplitude 0-127
        if (sample < 0) sample = 0;
        if (sample > 255) sample = 255;
        
        for (ch = 0; ch < channels; ch++) {
            buffer[i * channels + ch] = (unsigned char)sample;
        }
        
        phase += phase_increment;
        while (phase >= 2.0 * M_PI) phase -= 2.0 * M_PI;
    }
}

// Generate silence
static void generate_silence(unsigned char *buffer, int frames, int channels)
{
    memset(buffer, 128, frames * channels);
}

static int alsa_init(void)
{
    int err;
    
    if (alsa_handle != NULL) return 0;
    
    err = snd_pcm_open(&alsa_handle, "default", SND_PCM_STREAM_PLAYBACK, 0);
    if (err < 0) {
        fprintf(stderr, "Cannot open ALSA device: %s\n", snd_strerror(err));
        return -1;
    }
    
    err = snd_pcm_set_params(alsa_handle,
                             SND_PCM_FORMAT_U8,
                             SND_PCM_ACCESS_RW_INTERLEAVED,
                             1,                      // mono
                             alsa_sample_rate,       // sample rate
                             1,                      // allow resampling
                             500000);                // latency 0.5 sec
    if (err < 0) {
        fprintf(stderr, "Cannot set ALSA params: %s\n", snd_strerror(err));
        snd_pcm_close(alsa_handle);
        alsa_handle = NULL;
        return -1;
    }
    
    return 0;
}

static void alsa_close(void)
{
    if (alsa_handle != NULL) {
        snd_pcm_drain(alsa_handle);
        snd_pcm_close(alsa_handle);
        alsa_handle = NULL;
    }
}

static void alsa_play_tone(int freq, int duration_ms, int volume)
{
    int err;
    int frames_needed = (duration_ms * alsa_sample_rate) / 1000;
    int frames_written = 0;
    int buffer_frames = 2400; // 50ms buffer
    unsigned char *buffer;
    int frames_remaining;
    
    if (freq < 20) freq = 20;
    if (freq > 20000) freq = 20000;
    if (volume < 0) volume = 0;
    if (volume > 100) volume = 100;
    if (duration_ms <= 0) return;
    
    buffer = (unsigned char*)malloc(buffer_frames * sizeof(unsigned char));
    if (!buffer) return;
    
    while (frames_written < frames_needed) {
        frames_remaining = frames_needed - frames_written;
        int write_frames = (frames_remaining < buffer_frames) ? frames_remaining : buffer_frames;
        
        if (freq == 0 || volume == 0) {
            generate_silence(buffer, write_frames, 1);
        } else {
            generate_sine(buffer, write_frames, 1, freq, volume, alsa_sample_rate);
        }
        
        err = snd_pcm_writei(alsa_handle, buffer, write_frames);
        if (err == -EPIPE) {
            // Underrun, recover
            err = snd_pcm_recover(alsa_handle, err, 0);
            if (err < 0) break;
        } else if (err < 0) {
            break;
        }
        frames_written += err;
    }
    
    free(buffer);
}

static void alsa_off(void)
{
    if (alsa_handle != NULL) {
        snd_pcm_drain(alsa_handle);
    }
}

// =============================================================

int open_evdev(const char *const device_name)
{
    struct stat sb;
    
    if (-1 == stat(device_name, &sb)) return -1;
    if (!S_ISCHR(sb.st_mode)) return -1;
    
    const int fd = open(device_name, O_WRONLY);
    if (fd == -1) return -1;
    
    if (-1 == fstat(fd, &sb)) return -1;
    if (!S_ISCHR(sb.st_mode)) return -1;
    
    return fd;
}

static void espk(const uint16_t freq)
{
    struct input_event e;
    memset(&e, 0, sizeof(e));
    e.type = EV_SND;
    e.code = SND_TONE;
    e.value = spkf(freq);
    
    if (sizeof(e) != write(efd, &e, sizeof(e))) {
        puts("Cannot use the sound API");
        _Exit(1);
    }
}

static void espkoff()
{
    struct input_event e;
    memset(&e, 0, sizeof(e));
    e.type = EV_SND;
    e.code = SND_TONE;
    e.value = 0;
    
    if (sizeof(e) != write(efd, &e, sizeof(e))) {
        _Exit(1);
    }
}

void SIGHUPHandler(int signal) {
    puts("SIGHUP");
    if (alsa_mode) {
        alsa_close();
    } else if (evdev == 1) {
        espkoff();
    } else {
        gspkoff();
    }
    _Exit(0);
}

void SIGINTHandler(int signal) {
    puts("SIGINT");
    if (alsa_mode) {
        alsa_close();
    } else if (evdev == 1) {
        espkoff();
    } else {
        gspkoff();
    }
    _Exit(0);
}

void SIGTERMHandler(int signal) {
    puts("SIGTERM");
    if (alsa_mode) {
        alsa_close();
    } else if (evdev == 1) {
        espkoff();
    } else {
        gspkoff();
    }
    _Exit(0);
}

void InstallSignalHandlers() {
    struct sigaction action, nu;
    memset(&action, 0, sizeof(action));
    memset(&nu, 0, sizeof(nu));
    action.sa_handler = SIGTERMHandler;
    action.sa_flags = SA_RESTART;
    sigaction(SIGTERM, &action, &nu);
    action.sa_handler = SIGINTHandler;
    sigaction(SIGINT, &action, &nu);
    action.sa_handler = SIGHUPHandler;
    sigaction(SIGHUP, &action, &nu);
}

int main(int argc, char *argv[])
{
    int f_uid, f_euid;
    
    f_uid = getuid();
    f_euid = geteuid();
    printf("UID=%d EUID=%d\n", f_uid, f_euid);
    
    if (f_euid == 0) {
        if (setuid(0)) {
            perror("setuid");
            return 1;
        }
    }
    
    f_uid = getuid();
    f_euid = geteuid();
    printf("Now UID=%d EUID=%d\n", f_uid, f_euid);
    
    /* Handle --stop parameter */
    if ((argc > 1) && (strcmp(argv[1], "--stop") == 0)) {
        system("killall gorg64_spkplay");
        _Exit(0);
    }
    
    /* Initialize unique check library */
    uniquecheck_init(argc, argv);
    
    /* Check if another instance is already running (system-wide) */
    int64_t u;
    int64_t uh = 1000;
    for (u = 1; u <= uh; u++) {
        if (!uniquecheck_is_running_system()) { break; }
        if (u == uh) {
            puts("Already running. Exit.");
            _Exit(0);
        }
        printf("Other playing process runned. Attempt number: %ld\n", u);
        usleep(10000000);
    }
    
    InstallSignalHandlers();
    
    /* Initialize PC speaker output method */
    if ((ioperm(0x42, 2, 1) == 0) && (ioperm(0x61, 1, 1) == 0)) {
        gspkon = spkon;
        gspkoff = spkoff;
        gspk = spk;
        puts("Use I/O ports");
    } else {
        if (syscall(1003) == 123) {
            gspkon = kspkon;
            gspkoff = kspkoff;
            gspk = kspk;
            puts("Use kernel patch");
        } else {
            efd = open_evdev("/dev/input/by-path/platform-pcspkr-event-spkr");
            if (efd < 0) {
                puts("PC-Speaker not available, trying ALSA...");
                if (snd_pcm_open(&alsa_handle, "default", SND_PCM_STREAM_PLAYBACK, 0) == 0) {
                    snd_pcm_close(alsa_handle);
                    alsa_handle = NULL;
                    alsa_mode = 1;
                    puts("Use ALSA (software generated tones)");
                } else {
                    puts("Error: No sound output method available");
                    _Exit(1);
                }
            } else {
                evdev = 1;
                gspkoff = espkoff;
                puts("Use evdev");
            }
        }
    }
    
    /* Set high priority */
    setpriority(PRIO_PROCESS, 0, -20);
    
    /* Show usage if no files specified */
    if (argc == 1) {
        puts("Use: gorg64_spkplay file1.speaker file2.speaker ... ");
        if (alsa_mode) {
            if (alsa_init() == 0) {
                alsa_play_tone(1000, 1000, 70);
                alsa_close();
            }
        } else if (evdev == 1) {
            espk(1000);
            usleep(1000000);
            espkoff();
        } else {
            gspkon();
            gspk(1000);
            usleep(1000000);
            gspkoff();
        }
        _Exit(0);
    }
    
    /* Play files */
    void *a;
    ttw *p;
    struct stat sb;
    int fd;
    int64_t ff, f;
    
    for (ff = 1; ff < argc; ff++) {
        puts(argv[ff]);
        fd = open(argv[ff], O_RDONLY);
        if (fd < 0) {
            printf("Cannot open file: %s\n", strerror(errno));
            continue;
        }
        
        if (fstat(fd, &sb) == -1) {
            printf("Cannot stat file: %s\n", strerror(errno));
            close(fd);
            continue;
        }
        
        a = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
        close(fd);
        
        if (a == MAP_FAILED) {
            puts("map failed");
            continue;
        }
        
        if (sb.st_size % 4 != 0) {
            puts("size / 4 <> 0");
            munmap(a, sb.st_size);
            continue;
        }
        
        p = (ttw*)a;
        
        if (alsa_mode) {
            if (alsa_init() == 0) {
                for (f = 0; f < sb.st_size / 4; f++) {
                    if (p[f].duration < 1) continue;
                    if (p[f].tone < 1) {
                        alsa_play_tone(0, p[f].duration, 0);
                    } else {
                        alsa_play_tone(spkf(p[f].tone), p[f].duration, 70);
                    }
                }
                alsa_off();
            }
        } else if (evdev == 1) {
            for (f = 0; f < sb.st_size / 4; f++) {
                if (p[f].duration < 1) continue;
                if (p[f].tone < 1) {
                    espkoff();
                    usleep(p[f].duration * 1000);
                } else {
                    espk(p[f].tone);
                    usleep(p[f].duration * 1000);
                }
            }
            espkoff();
        } else {
            gspkon();
            for (f = 0; f < sb.st_size / 4; f++) {
                if (p[f].duration < 1) continue;
                if (p[f].tone < 1) {
                    gspkoff();
                    usleep(p[f].duration * 1000);
                    gspkon();
                } else {
                    gspk(p[f].tone);
                    usleep(p[f].duration * 1000);
                }
            }
            gspkoff();
        }
        
        munmap(a, sb.st_size);
    }
    
    if (alsa_mode) alsa_close();
    _Exit(0);
}