#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <jpeglib.h>

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

static const int SINGLE_MODE = 48;
static int verbose = 0;

static char *device_filename = "/dev/lp0";
static int device = 0;

static unsigned char *buffer = 0;
static size_t buffer_size = 1;
static size_t buffer_pos = 0;

static void require_device() {
    if (!device) {
        if (!strcmp(device_filename, "-")) {
            device = 1;
        } else {
            if (verbose)
                fprintf(stderr, "Opening device: %s\n", device_filename);
            device = open(device_filename, O_RDWR);
            if (device == -1) {
                fprintf(stderr, "Error opening device %s\n", device_filename);
                exit(-1);
            }
        }
    }
}

static void buf_put(unsigned char byte) {
    if (!buffer) {
        buffer = malloc(buffer_size);
    }
    if (buffer_pos >= buffer_size) {
        buffer_size *= 2;
        buffer = realloc(buffer, buffer_size);
    }
    buffer[buffer_pos] = byte;
    ++buffer_pos;
}

static void flush_buffer() {
    if (verbose)
        fprintf(stderr, "Writing data to device\n");
    while (buffer_pos > 0) {
        require_device();
        int rv = write(device, buffer, buffer_pos);
        if (-1 == rv) {
            perror("Error writing to device");
            exit(1);
        }
        buffer_pos -= rv;
    }
}

static void send_image(const char *filename, int small_mode) {
    struct jpeg_decompress_struct cinfo;
    struct jpeg_error_mgr jerr;
    JSAMPROW row_pointer[1];

    FILE *infile = fopen(filename, "rb");
    if (!infile) {
        fprintf(stderr, "Error opening JPEG file %s\n", filename);
        exit(1);
    }
    cinfo.err = jpeg_std_error(&jerr);
    jpeg_create_decompress(&cinfo);
    jpeg_stdio_src(&cinfo, infile);
    jpeg_read_header(&cinfo, TRUE);
    jpeg_start_decompress(&cinfo);
    row_pointer[0] = (unsigned char *)malloc(cinfo.output_width * cinfo.num_components);

    int width = cinfo.output_width;
    int height = cinfo.output_height;
    int spare_bits = width % 8;
    int byte_width = width / 8 + (spare_bits == 0 ? 0 : 1);

    if (small_mode) {
        if (byte_width > 255) {
            fprintf(stderr, "Width (%d) must be <= 255\n", byte_width);
            exit(-1);
        }
        if (height > 48) {
            fprintf(stderr, "Height (%d) must be <= 48\n", height);
            exit(-1);
        }
    } else {
        buf_put(byte_width & 255);
        buf_put(byte_width >> 8);
        buf_put(height & 255);
        buf_put(height >> 8);
    }
    
    while (cinfo.output_scanline < cinfo.image_height) {
        unsigned char current_byte = 0;
        int x = 0;
        int i;
        jpeg_read_scanlines(&cinfo, row_pointer, 1);
        while (x < cinfo.image_width) {
            int value = 0;
            for (i = 0; i < cinfo.num_components; i++) {
                value += row_pointer[0][x * cinfo.num_components + i];
            }
            current_byte = current_byte << 1;
            if (value < 128 * cinfo.num_components) {
                current_byte = current_byte | 1;
            }
            ++x;
            if (x % 8 == 0) {
                buf_put(current_byte);
                current_byte = 0;
            }
        }
        if (spare_bits != 0) {
            current_byte <<= spare_bits;
            buf_put(current_byte);
        }
    }
    jpeg_finish_decompress(&cinfo);
    jpeg_destroy_decompress(&cinfo);
    free(row_pointer[0]);
    fclose(infile);
}

static void print_nv_image(int nv_num) {
    buf_put(0x1c);
    buf_put(0x70);
    buf_put(nv_num);
    buf_put(SINGLE_MODE);
}

static void store_nv_images(int count, char **filenames) {
    buf_put(0x1c);
    buf_put(0x71);
    buf_put(count);
    
    int i;
    for (i = 0; i < count; i++) {
        send_image(filenames[i], FALSE);
    }
}

static unsigned char get_status(int num) {
    buf_put(0x10);
    buf_put(0x04);
    buf_put(num);
    flush_buffer();
    
    unsigned char status;
    if (read(device, &status, 1) != 1) {
        perror("Failed to read status");
        exit(1);
    }
    return status;
}

static void print_status() {
    unsigned char status;
    status = get_status(1);
    printf("Online status: %s\n", (status & 0x08) ? "Offline" : "Online");
    status = get_status(2);
    printf("Cover: %s\n", (status & 0x04) ? "Open" : "Closed");
    printf("Manual feed active: %s\n", (status & 0x08) ? "Yes" : "No");
    printf("Out of paper: %s\n", (status & 0x20) ? "Yes" : "No");
    printf("Error occurred: %s\n", (status & 0x20) ? "Yes" : "No");
}

static void cut_page() {
    buf_put('\n');
    buf_put('\n');
    buf_put('\n');
    buf_put('\n');
    buf_put(0x1d);
    buf_put(0x56);
    buf_put(0x01);
}

static void init_printer() {
    buf_put(0x1b);
    buf_put(0x40);
}

static void print_image(const char *filename) {
    buf_put(0x1d);
    buf_put(0x76);
    buf_put(0x30);
    buf_put(SINGLE_MODE);
    send_image(filename, FALSE);
}

static void set_download_image(const char *filename) {
    buf_put(0x1d);
    buf_put(0x2a);
    send_image(filename, TRUE);
}

static void print_text(const char *text) {
    int i = 0;
    while (text[i]) {
        buf_put(text[i]);
        ++i;
    }
}

static void barcode_hri_pos(unsigned char mode) {
    buf_put(0x1d);
    buf_put(0x48);
    buf_put(mode);
}

static void print_barcode(const char *text) {
    buf_put(0x1d);
    buf_put(0x6b);
    buf_put(67); // JAN13
    buf_put(strlen(text));
    print_text(text);
    buf_put('\n');
}

static void barcode_height(int n) {
    if (n < 1 || n > 255) {
        fprintf(stderr, "barcode height must be in range 1..255\n");
    }
    buf_put(0x1d);
    buf_put(0x66);
    buf_put(n);
}

static void set_margin(int n) {
    buf_put(0x1d);
    buf_put(0x4c);
    buf_put(n & 255);
    buf_put(n >> 8);
}

static void set_emphasis(int n) {
    buf_put(0x1b);
    buf_put(0x45);
    buf_put(n);
}

static void print_usage(FILE *out, const char *program) {
    fprintf(out, "Usage: %s {command}...\n", program);
    fprintf(out, "Commands:\n");
    fprintf(out, "  --init                                 Initialise the printer\n");
    fprintf(out, "  --device {device}                      Specify output device (%s)\n", device_filename);
    fprintf(out, "  --print-image {jpeg}                   Print specified image\n");
    fprintf(out, "  --set-download-image {jpeg}            Set download image (max: 255x48)\n");
    fprintf(out, "  --store-nv-images {jpeg} [{jpeg}...]   Store images in NV (non volatile) area\n");
    fprintf(out, "  --cut                                  Cut the page\n");
    fprintf(out, "  --text {text}                          Print some text\n");
    fprintf(out, "  --nv {num}                             Print NV image {num} (1-255)\n");
    fprintf(out, "  --status                               Print the status of the printer\n");
    fprintf(out, "  --barcode-hri-pos {n}                  n= 0:none, 1:above, 2:below, 3:both\n");
    fprintf(out, "  --barcode-height {n}                   Set height in pixels of bar code\n");
    fprintf(out, "  --print-barcode {JAN13}                Print a bar code\n");
    fprintf(out, "  --margin {n}                           Set the margin\n");
    fprintf(out, "  --emphasis {0/1}                       Turn emphasis off/on\n");
    fprintf(out, "  --verbose                              Be verbose\n");
    fprintf(out, "Example:\n");
    fprintf(out, "%s --text $'This is a test\\nSecond line\\n' --print-image test.jpg\n", program);
    fprintf(out, "To write to standard output, use \"-\" as device\n");
    fprintf(out, "eg: %s --device - --nv 1 | hexdump -C | less\n", program);
}

int main(int argc, char ** argv) {
    int a;
    for (a = 1; a < argc; a++) {
        if (!strcmp(argv[a], "--print-image")) {
            print_image(argv[++a]);
        } else if (!strcmp(argv[a], "--set-download-image")) {
            set_download_image(argv[++a]);
        } else if (!strcmp(argv[a], "--store-nv-images")) {
            store_nv_images(argc - a - 1, argv + a + 1);
            a = argc;
        } else if (!strcmp(argv[a], "--cut")) {
            cut_page();
        } else if (!strcmp(argv[a], "--init")) {
            init_printer();
        } else if (!strcmp(argv[a], "--status")) {
            print_status();
        } else if (!strcmp(argv[a], "--text")) {
            print_text(argv[++a]);
        } else if (!strcmp(argv[a], "--print-barcode")) {
            print_barcode(argv[++a]);
        } else if (!strcmp(argv[a], "--barcode-hri-pos")) {
            barcode_hri_pos(atoi(argv[++a]));
        } else if (!strcmp(argv[a], "--barcode-height")) {
            barcode_height(atoi(argv[++a]));
        } else if (!strcmp(argv[a], "--nv")) {
            print_nv_image(atoi(argv[++a]));
        } else if (!strcmp(argv[a], "--emphasis")) {
            set_emphasis(atoi(argv[++a]));
        } else if (!strcmp(argv[a], "--margin")) {
            set_margin(atoi(argv[++a]));
        } else if (!strcmp(argv[a], "--verbose")) {
            verbose = 1;
        } else if (!strcmp(argv[a], "--help")) {
            print_usage(stdout, argv[0]);
            exit(0);
        } else if (!strcmp(argv[a], "--device")) {
            device_filename = argv[++a];
        } else {
            printf("Unrecognised option: %s\n", argv[a]);
            print_usage(stderr, argv[0]);
            exit(1);
        }
    }

    flush_buffer();

    if (device > 1) {
        if (verbose)
            fprintf(stderr, "Closing device\n");
        close(device);
    }
    
    return 0;
}
