#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>  
#include <sys/types.h>  
#include <sys/wait.h>
#include <libdockapp/dockapp.h>
#include <libpafe/libpafe.h>
#define APP_NAME            "wm_pasori"
#include <ao/ao.h>
#define DR_WAV_IMPLEMENTATION
#include "dr_wav.h"


#define APP_WIDTH           58
#define APP_HEIGHT          58
#define SCAN_INTERVAL_MS    700
#define PASORI_TIMEOUT_MS   200
#define SUICA_SERVICE_CODE  0x090f
#define SUICA_BLOCK_NUMBER  0
#define SOUND_DIR           "/usr/local/share/wm_pasori"

typedef enum {
    DISPLAY_START,
    DISPLAY_WAIT,
    DISPLAY_BALANCE,
    DISPLAY_READ_ERROR,
    DISPLAY_NO_DEVICE
} DisplayState;

static pasori *reader = NULL;
static DisplayState display_state = DISPLAY_START;
static unsigned int current_balance = 0;
static int current_error = 0;
static XFontStruct *font_small = NULL;
static XFontStruct *font_large = NULL;
static int last_card_detected = 0;

static void close_pasori(void) {
    if (reader != NULL) {
        pasori_close(reader);
        reader = NULL;
    }
}

static int open_pasori(void) {
    int result;

    if (reader != NULL) {
        return 0;
    }

    reader = pasori_open();

    if (reader == NULL) {
        return -1;
    }

    result = pasori_init(reader);

    if (result != 0) {
        fprintf(stderr, "pasori_init: error %d\n", result);

        pasori_close(reader);
        reader = NULL;

        return -1;
    }

    pasori_set_timeout(reader, PASORI_TIMEOUT_MS);

    fprintf(stderr, "PaSoRi initialized\n");

    return 0;
}

static void play_sound(const char *sound_file) {
    char path[256];
    drwav wav;
    ao_device *dev = NULL;
    ao_sample_format fmt;
    int default_driver;
    static pid_t sound_pid = 0;
    int status;

    if (sound_pid > 0) {
        kill(sound_pid, SIGTERM);
        waitpid(sound_pid, &status, 0);
        sound_pid = 0;
    }

    snprintf(path, sizeof(path), "%s/%s", SOUND_DIR, sound_file);

    sound_pid = fork();
    if (sound_pid != 0) return;

    ao_initialize();
    default_driver = ao_default_driver_id();

    memset(&fmt, 0, sizeof(fmt));
    fmt.bits        = 16;
    fmt.channels    = 2;
    fmt.rate        = 48000;
    fmt.byte_format = AO_FMT_NATIVE;

    if (!drwav_init_file(&wav, path, NULL)) {
        fprintf(stderr, "dr_wav: failed to open %s\n", path);
        _exit(1);
    }
    fmt.bits     = wav.bitsPerSample;
    fmt.channels = wav.channels;
    fmt.rate     = wav.sampleRate;

    dev = ao_open_live(default_driver, &fmt, NULL);
    if (!dev) {
        fprintf(stderr, "ao_open_live failed\n");
        drwav_uninit(&wav);
        _exit(1);
    }

    size_t frames_read;
    int16_t pcm_buf[4096];
    while ((frames_read = drwav_read_pcm_frames_s16(&wav, 4096 / wav.channels, pcm_buf)) > 0) {
        ao_play(dev, (char *)pcm_buf, (int)(frames_read * wav.channels * sizeof(int16_t)));
    }

    ao_close(dev);
    drwav_uninit(&wav);
    ao_shutdown();
    _exit(0);
}

static void draw_centered_text(Drawable drawable, GC gc, XFontStruct *font, int y, const char *text) {
    int width;
    int x;

    if (font == NULL || text == NULL) {
        return;
    }

    width = XTextWidth(
        font,
        text,
        (int)strlen(text)
    );

    x = (APP_WIDTH - width) / 2;

    if (x < 1) {
        x = 1;
    }

    XSetFont(
        DADisplay,
        gc,
        font->fid
    );

    XDrawString(
        DADisplay,
        drawable,
        gc,
        x,
        y,
        text,
        (int)strlen(text)
    );
}

static void redraw_display(void) {
    Pixmap back;
    XGCValues values;
    GC background_gc;
    GC border_dark_gc;
    GC border_light_gc;
    GC normal_gc;
    GC accent_gc;
    GC warning_gc;
    GC error_gc;
    GC dim_gc;
    char balance_text[32];
    char error_text[32];

    back = DAMakePixmap();

    values.foreground = DAGetColor("#101418");
    values.graphics_exposures = False;

    background_gc = XCreateGC(
        DADisplay,
        DAWindow,
        GCForeground | GCGraphicsExposures,
        &values
    );

    XFillRectangle(
        DADisplay,
        back,
        background_gc,
        0,
        0,
        APP_WIDTH,
        APP_HEIGHT
    );

    values.foreground = DAGetColor("#313943");

    border_dark_gc = XCreateGC(
        DADisplay,
        DAWindow,
        GCForeground,
        &values
    );

    /*
    * 内枠
    */ 
    values.foreground = DAGetColor("#56616d");

    border_light_gc = XCreateGC(
        DADisplay,
        DAWindow,
        GCForeground,
        &values
    );

    XDrawRectangle(
        DADisplay,
        back,
        border_dark_gc,
        0,
        0,
        APP_WIDTH - 1,
        APP_HEIGHT - 1
    );

    XDrawRectangle(
        DADisplay,
        back,
        border_light_gc,
        1,
        1,
        APP_WIDTH - 3,
        APP_HEIGHT - 3
    );

    /*
    * 残高表示
    */ 
    values.foreground = DAGetColor("#d0d7de");

    normal_gc = XCreateGC(
        DADisplay,
        DAWindow,
        GCForeground,
        &values
    );

    /*
     * 残高表示
     */
    values.foreground = DAGetColor("#74ff89");

    accent_gc = XCreateGC(
        DADisplay,
        DAWindow,
        GCForeground,
        &values
    );

    /*
     * 読み取り警告
     */
    values.foreground = DAGetColor("#ffd75f");

    warning_gc = XCreateGC(
        DADisplay,
        DAWindow,
        GCForeground,
        &values
    );

    /*
     * デバイスエラー
     */
    values.foreground = DAGetColor("#ff6464");

    error_gc = XCreateGC(
        DADisplay,
        DAWindow,
        GCForeground,
        &values
    );

    values.foreground = DAGetColor("#69737d");

    dim_gc = XCreateGC(
        DADisplay,
        DAWindow,
        GCForeground,
        &values
    );

    switch (display_state) {
    case DISPLAY_BALANCE:
        snprintf(
            balance_text,
            sizeof(balance_text),
            "%u",
            current_balance
        );

        draw_centered_text(
            back,
            accent_gc,
            font_small,
            13,
            "Suica"
        );

        draw_centered_text(
            back,
            accent_gc,
            font_large,
            36,
            balance_text
        );

        draw_centered_text(
            back,
            dim_gc,
            font_small,
            51,
            "YEN"
        );
        break;

    case DISPLAY_WAIT:
        draw_centered_text(
            back,
            normal_gc,
            font_small,
            15,
            "Suica"
        );

        draw_centered_text(
            back,
            normal_gc,
            font_small,
            33,
            "TOUCH"
        );

        draw_centered_text(
            back,
            dim_gc,
            font_small,
            49,
            "----"
        );
        break;

    case DISPLAY_READ_ERROR:
        snprintf(
            error_text,
            sizeof(error_text),
            "ERR %d",
            current_error
        );

        draw_centered_text(
            back,
            warning_gc,
            font_small,
            15,
            "Suica"
        );

        draw_centered_text(
            back,
            warning_gc,
            font_small,
            33,
            "READ"
        );

        draw_centered_text(
            back,
            warning_gc,
            font_small,
            49,
            error_text
        );
        break;

    case DISPLAY_NO_DEVICE:
        draw_centered_text(
            back,
            error_gc,
            font_small,
            15,
            "PaSoRi"
        );

        draw_centered_text(
            back,
            error_gc,
            font_small,
            33,
            "NO DEV"
        );

        draw_centered_text(
            back,
            dim_gc,
            font_small,
            49,
            "USB?"
        );
        break;

    case DISPLAY_START:
    default:
        draw_centered_text(
            back,
            normal_gc,
            font_small,
            18,
            "PaSoRi"
        );

        draw_centered_text(
            back,
            dim_gc,
            font_small,
            39,
            "START"
        );
        break;
    }

    /*
     * DockAppのicon windowへ反映する
     *
     * DASetPixmap()は渡した内容を表示側へ設定する
     */ 
    DASetPixmap(back);

    XFreePixmap(DADisplay, back);

    XFreeGC(DADisplay, background_gc);
    XFreeGC(DADisplay, border_dark_gc);
    XFreeGC(DADisplay, border_light_gc);
    XFreeGC(DADisplay, normal_gc);
    XFreeGC(DADisplay, accent_gc);
    XFreeGC(DADisplay, warning_gc);
    XFreeGC(DADisplay, error_gc);
    XFreeGC(DADisplay, dim_gc);

    XFlush(DADisplay);
}

/*
 * Suica残高を読み取る
 *
 * 戻り値:
 * 0 成功
 * 1 Suicaなし
 * -1 読取り失敗
 * -2 PaSoRiなし・初期化失敗
 */ 
static int read_suica_balance(unsigned int *balance, int *read_error) {
    felica *card;
    uint8_t block_data[16];
    int result;
    size_t i;

    if (balance == NULL) {
        return -2;
    }

    *balance = 0;

    if (read_error != NULL) {
        *read_error = 0;
    }

    if (open_pasori() != 0) {
        return -2;
    }

    card = felica_polling(
        reader,
        FELICA_POLLING_SUICA,
        0,
        0
    );

    if (card == NULL) {
        return 1;
    }

    memset(
        block_data,
        0,
        sizeof(block_data)
    );

    result = felica_read_single(
        card,
        SUICA_SERVICE_CODE,
        0,
        SUICA_BLOCK_NUMBER,
        block_data
    );

    free(card);
    card = NULL;

    if (result != 0) {
        if (read_error != NULL) {
            *read_error = result;
        }

        fprintf(
            stderr,
            "felica_read_single: error %d\n",
            result
        );

        return -1;
    }

    fprintf(stderr, "Suica block:");

    for (i = 0; i < sizeof(block_data); i++) {
        fprintf(stderr, " %02x", block_data[i]);
    }

    fprintf(stderr, "\n");

    /*
     * Suica利用履歴の残高。
     *
     * オフセット10が下位バイト、
     * オフセット11が上位バイト。
     */ 
    *balance =
        (unsigned int)block_data[10] |
        ((unsigned int)block_data[11] << 8);

    return 0;
}

/*
* libdockappのタイマーコールバック。
*/ 
static void timeout_callback(void) {
    unsigned int balance;
    int read_error;
    int result;
    DisplayState new_state;
    static int skip_next = 0;

    if (skip_next) {
        skip_next = 0;
        return;
    }

    result = read_suica_balance(
        &balance,
        &read_error
    );

    switch (result) {
    case 0:
        new_state = DISPLAY_BALANCE;
        current_balance = balance;
        current_error = 0;
        
        if (!last_card_detected) {
            if (balance == 0) {
                play_sound("sound_02.wav");
            } else if (balance <= 1000) {
                play_sound("sound_05.wav");
            } else {
                play_sound("sound_03.wav");
            }
        }
        last_card_detected = 1;
        skip_next = 1;
        break;

    case 1:
        new_state = DISPLAY_WAIT;
        current_error = 0;
        last_card_detected = 0;
        break;

    case -1:
        new_state = DISPLAY_READ_ERROR;
        current_error = read_error;
        if (!last_card_detected) {
            play_sound("sound_06.wav");
        }
        last_card_detected = 1;
        skip_next = 1;
        break;

    default:
        new_state = DISPLAY_NO_DEVICE;
        current_error = 0;
        close_pasori();
        last_card_detected = 0;
        break;
    }

    display_state = new_state;
    redraw_display();
}

/*
 * 終了処理
 */
static void destroy_callback(void) {
    close_pasori();

    if (font_small != NULL) {
        XFreeFont(DADisplay, font_small);
        font_small = NULL;
    }

    if (font_large != NULL) {
        XFreeFont(DADisplay, font_large);
        font_large = NULL;
    }

    fprintf(stderr, "wm_pasori terminated\n");
}

/*
 * メイン
 */
int main(int argc, char **argv) {
    DACallbacks callbacks = {
        destroy_callback, /* destroy */
        NULL, /* buttonPress */
        NULL, /* buttonRelease */
        NULL, /* motion */
        NULL, /* mouse enters window */
        NULL, /* mouse leaves window */
        timeout_callback /* timeout */
    };

    DAParseArguments(
        argc,
        argv,
        NULL,
        0,
        "Window Maker DockApp for displaying Suica balance.\n",
        "wm_pasori 1.0"
    );

    /*
     * FreeBSD libdockapp 0.7.2サンプル
     */ 
    DASetExpectedVersion(20020126);

    DAOpenDisplay(
        NULL,
        argc,
        argv
    );

    DACreateIcon(
        APP_NAME,
        APP_WIDTH,
        APP_HEIGHT,
        argc,
        argv
    );

    font_small = XLoadQueryFont(
        DADisplay,
        "-misc-fixed-medium-r-normal--10-*-*-*-*-*-iso8859-1"
    );

    if (font_small == NULL) {
        font_small = XLoadQueryFont(
            DADisplay,
            "fixed"
        );
    }

    font_large = XLoadQueryFont(
        DADisplay,
        "-misc-fixed-bold-r-normal--18-*-*-*-*-*-iso8859-1"
    );

    if (font_large == NULL) {
        font_large = XLoadQueryFont(
            DADisplay,
            "9x15bold"
        );
    }

    if (font_large == NULL) {
        font_large = XLoadQueryFont(
            DADisplay,
            "fixed"
        );
    }

    if (font_small == NULL || font_large == NULL) {
        fprintf(stderr, "Could not load X11 fonts\n");
        close_pasori();
        return EXIT_FAILURE;
    }

    DASetCallbacks(&callbacks);

    DASetTimeout(SCAN_INTERVAL_MS);

    display_state = DISPLAY_START;
    redraw_display();

    DAShow();

    timeout_callback();

    /*
     * X11イベントとタイマー
     */
    DAEventLoop();

    return EXIT_SUCCESS;
}
