/* iv/main.c - see README */

#include <libgen.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include "SDL2/SDL.h"
#include "SDL2/SDL_image.h"

#define ZSH(a, b) ((a) * powf(2, b))
#define CLAMP(a, x, b) ((x) < (a) ? (a) : ((x) > (b) ? (b) : (x)))
#define APS 6
#define FPS 60
#define FRAMETIME (1000 / FPS)
#define DZDS 0.25

struct ViewState {
    /* image index */
    int ci;
    /* image dimensions */
    int iw;
    int ih;
    /* zoom level */
    float zoom;
    /* pan position (centre of image) */
    int px;
    int py;
    /* keep track of mouse coordinates */
    int mx;
    int my;
};

/* if cond is true, exit the program as a failure,
with msg as the message, along with the SDL error, if showsdl is true */
void cfail(const int cond, const char* msg, const int showsdl) {
    if (cond) {
        fprintf(stderr, "iv: failed to %s\n", msg);
        if (showsdl) {
            fprintf(stderr, "SDL: %s\n", SDL_GetError());
        }
        exit(EXIT_FAILURE);
    }
}

/* load and return alpha checkers, relative to iv binary */
SDL_Texture* checker(SDL_Renderer* ren, char* progname, int* w, int* h) {
    char* tpath = malloc(strlen(progname) + 32);
    cfail(!tpath, "allocate memory", 0);
    sprintf(tpath, "%s/alpha_checker.bmp", dirname(progname));
    SDL_Surface* acs = SDL_LoadBMP(tpath);
    if (w) {
        *w = acs->w;
    }
    if (h) {
        *h = acs->h;
    }
    cfail(!acs, "load alpha checker texture", 1);
    SDL_Texture* act = SDL_CreateTextureFromSurface(ren, acs);
    cfail(!act, "create surface", 1);
    SDL_FreeSurface(acs);
    return act;
}

/* process command-line options, setting pointed strings */
void handleargs(int argc, char** argv, char** title) {
    int oc;
    while ((oc = getopt(argc, argv, "ht:")) != -1) {
        switch (oc) {
            case 'h':
                printf(
                    "iv - image viewer\n"
                    "\nUsage: iv [-t TITLE] FILES\n"
                    "\n-t TITLE sets a custom window title\n"
                    "\nClick and drag to pan\n"
                    "Scroll to zoom\n"
                    "Right-click or space key to centre image\n"
                    "Middle-click to get coordinates at cursor\n"
                    "Arrow keys to move between images in the list\n"
                    "version 1.8-dev\n"
                );
                exit(EXIT_SUCCESS);
            case 't':
                *title = optarg;
                break;
            case ':':
            case '?':
                cfail(1, "parse options", 0);
        }
    }
}

/* load images from arguments */
SDL_Surface** loadimages(char** argv, const int fc) {
    SDL_Surface** isurf = calloc(fc, sizeof (SDL_Surface*));
    for (int i = 0; i < fc; i++) {
        const char* fname = argv[optind + i];
        SDL_RWops* ir;
        if (!strcmp(fname, "-")) {
            ir = SDL_RWFromFP(stdin, 0);
        } else {
            ir = SDL_RWFromFile(fname, "r");
        }
        cfail(!ir, "open file", 1);
        isurf[i] = IMG_Load_RW(ir, 1);
        cfail(!isurf[i], "load image", 1);
    }
    return isurf;
}

void updatetitle(SDL_Window* window, const char* ct, char** title, const char* fname, const struct ViewState vs) {
    if (!ct) {
        free(*title);
        *title = malloc(strlen(fname) + 32);
        cfail(!*title, "allocate memory", 0);
        sprintf(*title, "iv: %s (%dx%d)", fname, vs.iw, vs.ih);
        SDL_SetWindowTitle(window, *title);
    }
}

void showcoords(const SDL_MouseButtonEvent button, const struct ViewState vs) {
    printf(
        "%.0f, %.0f\n",
        ZSH((float)button.x - (float)vs.px + ZSH(vs.iw, vs.zoom - 1.0), -vs.zoom),
        ZSH((float)button.y - (float)vs.py + ZSH(vs.ih, vs.zoom - 1.0), -vs.zoom)
    );
    fflush(stdout);
}

void drawbg(SDL_Renderer* ren, const int ww, const int wh, const int cw, const int ch, SDL_Texture* act) {
    /* clear to background colour */
    cfail(SDL_RenderClear(ren), "clear screen", 1);
    /* alpha checker background */
    for (int i = 0; i < ww; i += APS * cw) {
        for (int j = 0; j < wh; j += APS * ch) {
            cfail(SDL_RenderCopy(ren, act, NULL, &(SDL_Rect){
                .x = i, .y = j, .w = APS * cw, .h = APS * ch
            }), "draw checker background", 1);
        }
    }
}

void drawfg(SDL_Renderer* ren, const struct ViewState vs, SDL_Texture* imgtex) {
    /* render image */
    cfail(SDL_RenderCopy(ren, imgtex, NULL, &(SDL_Rect){
        .x = vs.px - ZSH(vs.iw, vs.zoom - 1.0), .y = vs.py - ZSH(vs.ih, vs.zoom - 1.0),
        .w = ZSH(vs.iw, vs.zoom), .h = ZSH(vs.ih, vs.zoom)
    }), "display image", 1);
}

int main(int argc, char** argv) {
    /* setup */
    SDL_Init(SDL_INIT_VIDEO);
    atexit(SDL_Quit);
    char* ct = NULL;
    handleargs(argc, argv, &ct);

    /* load images before creating window, to determine window size */
    cfail(optind >= argc, "find filename(s) in options", 0);
    int fc = argc - optind;
    SDL_Surface** isurf = loadimages(argv, fc);
    int iw = isurf[0]->w;
    int ih = isurf[0]->h;

    /* create window and renderer */
    char* title = NULL;
    if (ct) {
        title = ct;
    } else {
        title = malloc(strlen(argv[optind]) + 32);
        cfail(!title, "allocate memory", 0);
        sprintf(title, "iv: %s (%dx%d)", argv[optind], iw, ih);
    }
    SDL_Window* window = SDL_CreateWindow(
        title, SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, iw, ih,
        SDL_WINDOW_RESIZABLE
    );
    cfail(!window, "create window", 1);
    SDL_Renderer* ren = SDL_CreateRenderer(window, -1, 0);
    cfail(!ren, "create renderer", 1);

    /* make images into textures */
    int cw, ch;
    SDL_Texture* act = checker(ren, argv[0], &cw, &ch);
    SDL_Texture** itex = calloc(fc, sizeof (SDL_Texture*));
    for (int i = 0; i < fc; i++) {
        itex[i] = SDL_CreateTextureFromSurface(ren, isurf[i]);
        cfail(!itex[i], "create texture", 1);
        SDL_FreeSurface(isurf[i]);
    }

    /* background (transparent black) */
    cfail(SDL_SetRenderDrawColor(ren, 0, 0, 0, 0), "set colour", 1);

    SDL_Event ev;
    /* time measurements for framerate limitation */
    uint32_t ta = SDL_GetTicks();
    uint32_t tb;
    int r = 1;
    /* only bother to redraw if an event that requires it is found */
    int ef;
    /* window size */
    int ww = iw;
    int wh = ih;
    struct ViewState vs = {
        .ci = 0, .iw = iw, .ih = ih, .zoom = 0,
        .px = iw / 2, .py = ih / 2, .mx = 0, .my = 0
    };

    while (r) {
        ef = 0;
        while (SDL_PollEvent(&ev)) {
            switch (ev.type) {

                case SDL_KEYUP:
                    int iic = 0;
                    switch (ev.key.keysym.sym) {
                        /* bring image back into focus when Space is pressed */
                        case SDLK_SPACE:
                            vs.px = ww / 2;
                            vs.py = wh / 2;
                            ef = 1;
                            break;
                        /* exit when Esc is pressed */
                        case SDLK_ESCAPE:
                            r = 0;
                            break;
                        /* navigate image list with arrow keys */
                        case SDLK_LEFT:
                        case SDLK_UP:
                            if (vs.ci > 0) {
                                vs.ci--;
                                iic = 1;
                            }
                            break;
                        case SDLK_RIGHT:
                        case SDLK_DOWN:
                            if (vs.ci < fc - 1) {
                                vs.ci++;
                                iic = 1;
                            }
                            break;
                    }
                    if (iic) {
                        cfail(SDL_QueryTexture(itex[vs.ci], NULL, NULL, &vs.iw, &vs.ih), "query texture", 1);
                        vs.zoom = 0;
                        vs.px = vs.iw / 2;
                        vs.py = vs.ih / 2;
                        ef = 1;
                        updatetitle(window, ct, &title, argv[optind + vs.ci], vs);
                    }
                    break;

                case SDL_MOUSEBUTTONDOWN:
                    /* output coordinates of cursor in image on middle-click */
                    if (ev.button.button == 2) {
                        showcoords(ev.button, vs);
                    }
                    /* bring image back into focus on right-click */
                    if (ev.button.button == 3) {
                        vs.px = ww / 2;
                        vs.py = wh / 2;
                        ef = 1;
                    }
                    break;

                case SDL_MOUSEMOTION:
                    /* pan-dragging if mouse is down */
                    if (SDL_GetMouseState(NULL, NULL) & SDL_BUTTON(1)) {
                        int sw = ZSH(vs.iw, vs.zoom - 1.0);
                        int sh = ZSH(vs.ih, vs.zoom - 1.0);
                        /* clamp panning to edges of window,
                        unless zoomed image is bigger than window */
                        if (2 * sw > ww) {
                            vs.px += ev.motion.xrel;
                        } else {
                            vs.px = CLAMP(sw, vs.px + ev.motion.xrel, ww - sw);
                        }
                        if (2 * sh > wh) {
                            vs.py += ev.motion.yrel;
                        } else {
                            vs.py = CLAMP(sh, vs.py + ev.motion.yrel, wh - sh);
                        }
                        ef = 1;
                    }
                    vs.mx = ev.motion.x;
                    vs.my = ev.motion.y;
                    break;

                case SDL_MOUSEWHEEL:
                    /* scrolling changes zoom */
                    vs.zoom += DZDS * ev.wheel.y;
                    vs.px = vs.mx - ZSH((vs.mx - vs.px), DZDS * ev.wheel.y);
                    vs.py = vs.my - ZSH((vs.my - vs.py), DZDS * ev.wheel.y);
                    ef = 1;
                    break;

                case SDL_QUIT:
                    /* exit upon a quit signal */
                    r = 0;
                    break;

                case SDL_WINDOWEVENT:
                    switch (ev.window.event) {
                        /* exit when window is closed */
                        case SDL_WINDOWEVENT_CLOSE:
                            r = 0;
                            break;
                        /* redraw required */
                        case SDL_WINDOWEVENT_EXPOSED:
                            ef = 1;
                            break;
                    }
                    break;

            }
        }

        if (ef) {
            SDL_GetWindowSize(window, &ww, &wh);
            drawbg(ren, ww, wh, cw, ch, act);
            drawfg(ren, vs, itex[vs.ci]);
            SDL_RenderPresent(ren);
        }

        /* limit framerate */
        tb = SDL_GetTicks();
        if (tb - ta < FRAMETIME) {
            SDL_Delay(FRAMETIME - (tb - ta));
        }
        ta = tb;
    }

    SDL_DestroyWindow(window);
    return 0;
}
