/*
 * Copyright (c) 2014-2018 Hayaki Saito
 *
 * 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.
 */

#include "config.h"

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

#if HAVE_UNISTD_H
# include <unistd.h>
#endif
#if HAVE_SYS_UNISTD_H
# include <sys/unistd.h>
#endif
#if HAVE_SYS_TYPES_H
#include <sys/types.h>
#endif
#if HAVE_TIME_H
# include <time.h>
#endif
#if HAVE_SYS_TIME_H
# include <sys/time.h>
#endif
#if HAVE_INTTYPES_H
# include <inttypes.h>
#endif
#if HAVE_ERRNO_H
# include <errno.h>
#endif
#if HAVE_SYS_STAT_H
# include <sys/stat.h>
#endif
#if HAVE_FCNTL_H
# include <fcntl.h>
#endif

#include <sixel.h>
#include "tty.h"
#include "encoder.h"
#include "rgblookup.h"


static char *
arg_strdup(
    char const          /* in */ *s,          /* source buffer */
    sixel_allocator_t   /* in */ *allocator)  /* allocator object for
                                                 destination buffer */
{
    char *p;

    p = (char *)sixel_allocator_malloc(allocator, strlen(s) + 1);
    if (p) {
        strcpy(p, s);
    }
    return p;
}


/* An clone function of XColorSpec() of xlib */
static SIXELSTATUS
sixel_parse_x_colorspec(
    unsigned char       /* out */ **bgcolor,     /* destination buffer */
    char const          /* in */  *s,            /* source buffer */
    sixel_allocator_t   /* in */  *allocator)    /* allocator object for
                                                    destination buffer */
{
    SIXELSTATUS status = SIXEL_FALSE;
    char *p;
    unsigned char components[3];
    int component_index = 0;
    unsigned long v;
    char *endptr;
    char *buf = NULL;
    struct color const *pcolor;

    /* from rgb_lookup.h generated by gpref */
    pcolor = lookup_rgb(s, strlen(s));
    if (pcolor) {
        *bgcolor = (unsigned char *)sixel_allocator_malloc(allocator, 3);
        if (*bgcolor == NULL) {
            sixel_helper_set_additional_message(
                "sixel_parse_x_colorspec: sixel_allocator_malloc() failed.");
            status = SIXEL_BAD_ALLOCATION;
            goto end;
        }
        (*bgcolor)[0] = pcolor->r;
        (*bgcolor)[1] = pcolor->g;
        (*bgcolor)[2] = pcolor->b;
    } else if (s[0] == 'r' && s[1] == 'g' && s[2] == 'b' && s[3] == ':') {
        p = buf = arg_strdup(s + 4, allocator);
        if (buf == NULL) {
            sixel_helper_set_additional_message(
                "sixel_parse_x_colorspec: sixel_allocator_malloc() failed.");
            status = SIXEL_BAD_ALLOCATION;
            goto end;
        }
        while (*p) {
            v = 0;
            for (endptr = p; endptr - p <= 12; ++endptr) {
                if (*endptr >= '0' && *endptr <= '9') {
                    v = (v << 4) | (unsigned long)(*endptr - '0');
                } else if (*endptr >= 'a' && *endptr <= 'f') {
                    v = (v << 4) | (unsigned long)(*endptr - 'a' + 10);
                } else if (*endptr >= 'A' && *endptr <= 'F') {
                    v = (v << 4) | (unsigned long)(*endptr - 'A' + 10);
                } else {
                    break;
                }
            }
            if (endptr - p == 0) {
                break;
            }
            if (endptr - p > 4) {
                break;
            }
            v = v << ((4 - (endptr - p)) * 4) >> 8;
            components[component_index++] = (unsigned char)v;
            p = endptr;
            if (component_index == 3) {
                break;
            }
            if (*p == '\0') {
                break;
            }
            if (*p != '/') {
                break;
            }
            ++p;
        }
        if (component_index != 3 || *p != '\0' || *p == '/') {
            status = SIXEL_BAD_ARGUMENT;
            goto end;
        }
        *bgcolor = (unsigned char *)sixel_allocator_malloc(allocator, 3);
        if (*bgcolor == NULL) {
            sixel_helper_set_additional_message(
                "sixel_parse_x_colorspec: sixel_allocator_malloc() failed.");
            status = SIXEL_BAD_ALLOCATION;
            goto end;
        }
        (*bgcolor)[0] = components[0];
        (*bgcolor)[1] = components[1];
        (*bgcolor)[2] = components[2];
    } else if (*s == '#') {
        buf = arg_strdup(s + 1, allocator);
        if (buf == NULL) {
            sixel_helper_set_additional_message(
                "sixel_parse_x_colorspec: sixel_allocator_malloc() failed.");
            status = SIXEL_BAD_ALLOCATION;
            goto end;
        }
        for (p = endptr = buf; endptr - p <= 12; ++endptr) {
            if (*endptr >= '0' && *endptr <= '9') {
                *endptr -= '0';
            } else if (*endptr >= 'a' && *endptr <= 'f') {
                *endptr -= 'a' - 10;
            } else if (*endptr >= 'A' && *endptr <= 'F') {
                *endptr -= 'A' - 10;
            } else if (*endptr == '\0') {
                break;
            } else {
                status = SIXEL_BAD_ARGUMENT;
                goto end;
            }
        }
        if (endptr - p > 12) {
            status = SIXEL_BAD_ARGUMENT;
            goto end;
        }
        *bgcolor = (unsigned char *)sixel_allocator_malloc(allocator, 3);
        if (*bgcolor == NULL) {
            sixel_helper_set_additional_message(
                "sixel_parse_x_colorspec: sixel_allocator_malloc() failed.");
            status = SIXEL_BAD_ALLOCATION;
            goto end;
        }
        switch (endptr - p) {
        case 3:
            (*bgcolor)[0] = (unsigned char)(p[0] << 4);
            (*bgcolor)[1] = (unsigned char)(p[1] << 4);
            (*bgcolor)[2] = (unsigned char)(p[2] << 4);
            break;
        case 6:
            (*bgcolor)[0] = (unsigned char)(p[0] << 4 | p[1]);
            (*bgcolor)[1] = (unsigned char)(p[2] << 4 | p[3]);
            (*bgcolor)[2] = (unsigned char)(p[4] << 4 | p[4]);
            break;
        case 9:
            (*bgcolor)[0] = (unsigned char)(p[0] << 4 | p[1]);
            (*bgcolor)[1] = (unsigned char)(p[3] << 4 | p[4]);
            (*bgcolor)[2] = (unsigned char)(p[6] << 4 | p[7]);
            break;
        case 12:
            (*bgcolor)[0] = (unsigned char)(p[0] << 4 | p[1]);
            (*bgcolor)[1] = (unsigned char)(p[4] << 4 | p[5]);
            (*bgcolor)[2] = (unsigned char)(p[8] << 4 | p[9]);
            break;
        default:
            status = SIXEL_BAD_ARGUMENT;
            goto end;
        }
    } else {
        status = SIXEL_BAD_ARGUMENT;
        goto end;
    }

    status = SIXEL_OK;

end:
    sixel_allocator_free(allocator, buf);

    return status;
}


/* generic writer function for passing to sixel_output_new() */
static int
sixel_write_callback(char *data, int size, void *priv)
{
    int result;

#if defined(__MINGW64__)
    result = write(*(int *)priv, data, (unsigned int)size);
#else
    result = write(*(int *)priv, data, (size_t)size);
#endif

    return result;
}


/* the writer function with hex-encoding for passing to sixel_output_new() */
static int
sixel_hex_write_callback(
    char    /* in */ *data,
    int     /* in */ size,
    void    /* in */ *priv)
{
    char hex[SIXEL_OUTPUT_PACKET_SIZE * 2];
    int i;
    int j;
    int result;

    for (i = j = 0; i < size; ++i, ++j) {
        hex[j] = (data[i] >> 4) & 0xf;
        hex[j] += (hex[j] < 10 ? '0': ('a' - 10));
        hex[++j] = data[i] & 0xf;
        hex[j] += (hex[j] < 10 ? '0': ('a' - 10));
    }

#if defined(__MINGW64__)
    result = write(*(int *)priv, hex, (unsigned int)(size * 2));
#else
    result = write(*(int *)priv, hex, (size_t)(size * 2));
#endif

    return result;
}


/* returns monochrome dithering context object */
static SIXELSTATUS
sixel_prepare_monochrome_palette(
    sixel_dither_t  /* out */ **dither,
     int            /* in */  finvert)
{
    SIXELSTATUS status = SIXEL_FALSE;

    if (finvert) {
        *dither = sixel_dither_get(SIXEL_BUILTIN_MONO_LIGHT);
    } else {
        *dither = sixel_dither_get(SIXEL_BUILTIN_MONO_DARK);
    }
    if (*dither == NULL) {
        sixel_helper_set_additional_message(
            "sixel_prepare_monochrome_palette: sixel_dither_get() failed.");
        status = SIXEL_RUNTIME_ERROR;
        goto end;
    }

    status = SIXEL_OK;

end:
    return status;
}


/* returns dithering context object with specified builtin palette */
static SIXELSTATUS
sixel_prepare_builtin_palette(
    sixel_dither_t /* out */ **dither,
    int            /* in */  builtin_palette)
{
    SIXELSTATUS status = SIXEL_FALSE;

    *dither = sixel_dither_get(builtin_palette);
    if (*dither == NULL) {
        sixel_helper_set_additional_message(
            "sixel_prepare_builtin_palette: sixel_dither_get() failed.");
        status = SIXEL_RUNTIME_ERROR;
        goto end;
    }

    status = SIXEL_OK;

end:
    return status;
}


typedef struct sixel_callback_context_for_mapfile {
    int reqcolors;
    sixel_dither_t *dither;
    sixel_allocator_t *allocator;
} sixel_callback_context_for_mapfile_t;


/* callback function for sixel_helper_load_image_file() */
static SIXELSTATUS
load_image_callback_for_palette(
    sixel_frame_t   /* in */    *frame, /* frame object from image loader */
    void            /* in */    *data)  /* private data */
{
    SIXELSTATUS status = SIXEL_FALSE;
    sixel_callback_context_for_mapfile_t *callback_context;

    /* get callback context object from the private data */
    callback_context = (sixel_callback_context_for_mapfile_t *)data;

    switch (sixel_frame_get_pixelformat(frame)) {
    case SIXEL_PIXELFORMAT_PAL1:
    case SIXEL_PIXELFORMAT_PAL2:
    case SIXEL_PIXELFORMAT_PAL4:
    case SIXEL_PIXELFORMAT_PAL8:
        if (sixel_frame_get_palette(frame) == NULL) {
            status = SIXEL_LOGIC_ERROR;
            goto end;
        }
        /* create new dither object */
        status = sixel_dither_new(
            &callback_context->dither,
            sixel_frame_get_ncolors(frame),
            callback_context->allocator);
        if (SIXEL_FAILED(status)) {
            goto end;
        }

        /* use palette which is extracted from the image */
        sixel_dither_set_palette(callback_context->dither,
                                 sixel_frame_get_palette(frame));
        /* success */
        status = SIXEL_OK;
        break;
    case SIXEL_PIXELFORMAT_G1:
        /* use 1bpp grayscale builtin palette */
        callback_context->dither = sixel_dither_get(SIXEL_BUILTIN_G1);
        /* success */
        status = SIXEL_OK;
        break;
    case SIXEL_PIXELFORMAT_G2:
        /* use 2bpp grayscale builtin palette */
        callback_context->dither = sixel_dither_get(SIXEL_BUILTIN_G1);
        callback_context->dither = sixel_dither_get(SIXEL_BUILTIN_G2);
        /* success */
        status = SIXEL_OK;
        break;
    case SIXEL_PIXELFORMAT_G4:
        /* use 4bpp grayscale builtin palette */
        callback_context->dither = sixel_dither_get(SIXEL_BUILTIN_G4);
        /* success */
        status = SIXEL_OK;
        break;
    case SIXEL_PIXELFORMAT_G8:
        /* use 8bpp grayscale builtin palette */
        callback_context->dither = sixel_dither_get(SIXEL_BUILTIN_G8);
        /* success */
        status = SIXEL_OK;
        break;
    default:
        /* create new dither object */
        status = sixel_dither_new(
            &callback_context->dither,
            callback_context->reqcolors,
            callback_context->allocator);
        if (SIXEL_FAILED(status)) {
            goto end;
        }

        /* create adaptive palette from given frame object */
        status = sixel_dither_initialize(callback_context->dither,
                                         sixel_frame_get_pixels(frame),
                                         sixel_frame_get_width(frame),
                                         sixel_frame_get_height(frame),
                                         sixel_frame_get_pixelformat(frame),
                                         SIXEL_LARGE_NORM,
                                         SIXEL_REP_CENTER_BOX,
                                         SIXEL_QUALITY_HIGH);
        if (SIXEL_FAILED(status)) {
            sixel_dither_unref(callback_context->dither);
            goto end;
        }

        /* success */
        status = SIXEL_OK;

        break;
    }

end:
    return status;
}


/* create palette from specified map file */
static SIXELSTATUS
sixel_prepare_specified_palette(
    sixel_dither_t  /* out */   **dither,
    sixel_encoder_t /* in */    *encoder)
{
    SIXELSTATUS status = SIXEL_FALSE;
    sixel_callback_context_for_mapfile_t callback_context;

    callback_context.reqcolors = encoder->reqcolors;
    callback_context.dither = NULL;
    callback_context.allocator = encoder->allocator;

    status = sixel_helper_load_image_file(encoder->mapfile,
                                          1,   /* fstatic */
                                          1,   /* fuse_palette */
                                          256, /* reqcolors */
                                          encoder->bgcolor,
                                          SIXEL_LOOP_DISABLE,
                                          load_image_callback_for_palette,
                                          encoder->finsecure,
                                          encoder->cancel_flag,
                                          &callback_context,
                                          encoder->allocator);
    if (status != SIXEL_OK) {
        return status;
    }

    *dither = callback_context.dither;

    return status;
}


/* create dither object from a frame */
static SIXELSTATUS
sixel_encoder_prepare_palette(
    sixel_encoder_t *encoder,  /* encoder object */
    sixel_frame_t   *frame,    /* input frame object */
    sixel_dither_t  **dither)  /* dither object to be created from the frame */
{
    SIXELSTATUS status = SIXEL_FALSE;
    int histogram_colors;

    switch (encoder->color_option) {
    case SIXEL_COLOR_OPTION_HIGHCOLOR:
        if (encoder->dither_cache) {
            *dither = encoder->dither_cache;
            status = SIXEL_OK;
        } else {
            status = sixel_dither_new(dither, (-1), encoder->allocator);
        }
        goto end;
    case SIXEL_COLOR_OPTION_MONOCHROME:
        if (encoder->dither_cache) {
            *dither = encoder->dither_cache;
            status = SIXEL_OK;
        } else {
            status = sixel_prepare_monochrome_palette(dither, encoder->finvert);
        }
        goto end;
    case SIXEL_COLOR_OPTION_MAPFILE:
        if (encoder->dither_cache) {
            *dither = encoder->dither_cache;
            status = SIXEL_OK;
        } else {
            status = sixel_prepare_specified_palette(dither, encoder);
        }
        goto end;
    case SIXEL_COLOR_OPTION_BUILTIN:
        if (encoder->dither_cache) {
            *dither = encoder->dither_cache;
            status = SIXEL_OK;
        } else {
            status = sixel_prepare_builtin_palette(dither, encoder->builtin_palette);
        }
        goto end;
    case SIXEL_COLOR_OPTION_DEFAULT:
    default:
        break;
    }

    if (sixel_frame_get_pixelformat(frame) & SIXEL_FORMATTYPE_PALETTE) {
        if (!sixel_frame_get_palette(frame)) {
            status = SIXEL_LOGIC_ERROR;
            goto end;
        }
        status = sixel_dither_new(dither, sixel_frame_get_ncolors(frame),
                                  encoder->allocator);
        if (SIXEL_FAILED(status)) {
            goto end;
        }
        sixel_dither_set_palette(*dither, sixel_frame_get_palette(frame));
        sixel_dither_set_pixelformat(*dither, sixel_frame_get_pixelformat(frame));
        if (sixel_frame_get_transparent(frame) != (-1)) {
            sixel_dither_set_transparent(*dither, sixel_frame_get_transparent(frame));
        }
        if (*dither && encoder->dither_cache) {
            sixel_dither_unref(encoder->dither_cache);
        }
        goto end;
    }

    if (sixel_frame_get_pixelformat(frame) & SIXEL_FORMATTYPE_GRAYSCALE) {
        switch (sixel_frame_get_pixelformat(frame)) {
        case SIXEL_PIXELFORMAT_G1:
            *dither = sixel_dither_get(SIXEL_BUILTIN_G1);
            break;
        case SIXEL_PIXELFORMAT_G2:
            *dither = sixel_dither_get(SIXEL_BUILTIN_G2);
            break;
        case SIXEL_PIXELFORMAT_G4:
            *dither = sixel_dither_get(SIXEL_BUILTIN_G4);
            break;
        case SIXEL_PIXELFORMAT_G8:
            *dither = sixel_dither_get(SIXEL_BUILTIN_G8);
            break;
        default:
            *dither = NULL;
            status = SIXEL_LOGIC_ERROR;
            goto end;
        }
        if (*dither && encoder->dither_cache) {
            sixel_dither_unref(encoder->dither_cache);
        }
        sixel_dither_set_pixelformat(*dither, sixel_frame_get_pixelformat(frame));
        status = SIXEL_OK;
        goto end;
    }

    if (encoder->dither_cache) {
        sixel_dither_unref(encoder->dither_cache);
    }
    status = sixel_dither_new(dither, encoder->reqcolors, encoder->allocator);
    if (SIXEL_FAILED(status)) {
        goto end;
    }
    status = sixel_dither_initialize(*dither,
                                     sixel_frame_get_pixels(frame),
                                     sixel_frame_get_width(frame),
                                     sixel_frame_get_height(frame),
                                     sixel_frame_get_pixelformat(frame),
                                     encoder->method_for_largest,
                                     encoder->method_for_rep,
                                     encoder->quality_mode);
    if (SIXEL_FAILED(status)) {
        sixel_dither_unref(*dither);
        goto end;
    }

    histogram_colors = sixel_dither_get_num_of_histogram_colors(*dither);
    if (histogram_colors <= encoder->reqcolors) {
        encoder->method_for_diffuse = SIXEL_DIFFUSE_NONE;
    }
    sixel_dither_set_pixelformat(*dither, sixel_frame_get_pixelformat(frame));

    status = SIXEL_OK;

end:
    return status;
}


/* resize a frame with settings of specified encoder object */
static SIXELSTATUS
sixel_encoder_do_resize(
    sixel_encoder_t /* in */    *encoder,   /* encoder object */
    sixel_frame_t   /* in */    *frame)     /* frame object to be resized */
{
    SIXELSTATUS status = SIXEL_FALSE;
    int src_width;
    int src_height;
    int dst_width;
    int dst_height;

    /* get frame width and height */
    src_width = sixel_frame_get_width(frame);
    src_height = sixel_frame_get_height(frame);

    /* settings around scaling */
    dst_width = encoder->pixelwidth;    /* may be -1 (default) */
    dst_height = encoder->pixelheight;  /* may be -1 (default) */

    /* if the encoder has percentwidth or percentheight property,
       convert them to pixelwidth / pixelheight */
    if (encoder->percentwidth > 0) {
        dst_width = src_width * encoder->percentwidth / 100;
    }
    if (encoder->percentheight > 0) {
        dst_height = src_height * encoder->percentheight / 100;
    }

    /* if only either width or height is set, set also the other
       to retain frame aspect ratio */
    if (encoder->pixelwidth > 0 && dst_height <= 0) {
        dst_height = src_height * encoder->pixelwidth / src_width;
    }
    if (encoder->pixelheight > 0 && dst_width <= 0) {
        dst_width = src_width * encoder->pixelheight / src_height;
    }

    /* do resize */
    if (dst_width > 0 && dst_height > 0) {
        status = sixel_frame_resize(frame, dst_width, dst_height,
                                    encoder->method_for_resampling);
        if (SIXEL_FAILED(status)) {
            goto end;
        }
    }

    /* success */
    status = SIXEL_OK;

end:
    return status;
}


/* clip a frame with settings of specified encoder object */
static SIXELSTATUS
sixel_encoder_do_clip(
    sixel_encoder_t /* in */    *encoder,   /* encoder object */
    sixel_frame_t   /* in */    *frame)     /* frame object to be resized */
{
    SIXELSTATUS status = SIXEL_FALSE;
    int src_width;
    int src_height;
    int clip_x;
    int clip_y;
    int clip_w;
    int clip_h;

    /* get frame width and height */
    src_width = sixel_frame_get_width(frame);
    src_height = sixel_frame_get_height(frame);

    /* settings around clipping */
    clip_x = encoder->clipx;
    clip_y = encoder->clipy;
    clip_w = encoder->clipwidth;
    clip_h = encoder->clipheight;

    /* adjust clipping width with comparing it to frame width */
    if (clip_w + clip_x > src_width) {
        if (clip_x > src_width) {
            clip_w = 0;
        } else {
            clip_w = src_width - clip_x;
        }
    }

    /* adjust clipping height with comparing it to frame height */
    if (clip_h + clip_y > src_height) {
        if (clip_y > src_height) {
            clip_h = 0;
        } else {
            clip_h = src_height - clip_y;
        }
    }

    /* do clipping */
    if (clip_w > 0 && clip_h > 0) {
        status = sixel_frame_clip(frame, clip_x, clip_y, clip_w, clip_h);
        if (SIXEL_FAILED(status)) {
            goto end;
        }
    }

    /* success */
    status = SIXEL_OK;

end:
    return status;
}


static void
sixel_debug_print_palette(
    sixel_dither_t /* in */ *dither /* dithering object */
)
{
    unsigned char *palette;
    int i;

    palette = sixel_dither_get_palette(dither);
    fprintf(stderr, "palette:\n");
    for (i = 0; i < sixel_dither_get_num_of_palette_colors(dither); ++i) {
        fprintf(stderr, "%d: #%02x%02x%02x\n", i,
                palette[i * 3 + 1],
                palette[i * 3 + 2],
                palette[i * 3 + 3]);
    }
}


static SIXELSTATUS
sixel_encoder_output_without_macro(
    sixel_frame_t       /* in */ *frame,
    sixel_dither_t      /* in */ *dither,
    sixel_output_t      /* in */ *output,
    sixel_encoder_t     /* in */ *encoder)
{
    SIXELSTATUS status = SIXEL_OK;
    static unsigned char *p;
    int depth;
    char message[256];
    int nwrite;
#if HAVE_NANOSLEEP
    int dulation;
    int delay;
    int lag = 0;
    struct timespec tv;
# if HAVE_CLOCK
    clock_t start;
# endif
#endif
    unsigned char *pixbuf;
    int width;
    int height;
    int pixelformat;
    size_t size;

    if (encoder == NULL) {
        sixel_helper_set_additional_message(
            "sixel_encoder_output_without_macro: encoder object is null.");
        status = SIXEL_BAD_ARGUMENT;
        goto end;
    }
    
    if (encoder->color_option == SIXEL_COLOR_OPTION_DEFAULT) {
        sixel_dither_set_optimize_palette(dither, 1);
    }

    pixelformat = sixel_frame_get_pixelformat(frame);
    depth = sixel_helper_compute_depth(pixelformat);
    if (depth < 0) {
        status = SIXEL_LOGIC_ERROR;
        nwrite = sprintf(message,
                         "sixel_encoder_output_without_macro: "
                         "sixel_helper_compute_depth(%08x) failed.",
                         pixelformat);
        if (nwrite > 0) {
            sixel_helper_set_additional_message(message);
        }
        goto end;
    }

    width = sixel_frame_get_width(frame);
    height = sixel_frame_get_height(frame);
    size = (size_t)(width * height * depth);
    p = (unsigned char *)sixel_allocator_malloc(encoder->allocator, size);
    if (p == NULL) {
        sixel_helper_set_additional_message(
            "sixel_encoder_output_without_macro: sixel_allocator_malloc() failed.");
        status = SIXEL_BAD_ALLOCATION;
        goto end;
    }
#if HAVE_NANOSLEEP && HAVE_CLOCK
    start = clock();
#endif
#if HAVE_NANOSLEEP
    delay = sixel_frame_get_delay(frame);
    if (delay > 0 && !encoder->fignore_delay) {
# if HAVE_CLOCK
        dulation = (int)((clock() - start) * 1000 * 1000 / CLOCKS_PER_SEC) - (int)lag;
        lag = 0;
# else
        dulation = 0;
# endif
        if (dulation < 10000 * delay) {
            tv.tv_sec = 0;
            tv.tv_nsec = (long)((10000 * delay - dulation) * 1000);
            nanosleep(&tv, NULL);
        } else {
            lag = (int)(10000 * delay - dulation);
        }
    }
#endif

    pixbuf = sixel_frame_get_pixels(frame);
    memcpy(p, pixbuf, (size_t)(width * height * depth));

    if (encoder->cancel_flag && *encoder->cancel_flag) {
        goto end;
    }

    status = sixel_encode(p, width, height, depth, dither, output);
    if (status != SIXEL_OK) {
        goto end;
    }

end:
    sixel_allocator_free(encoder->allocator, p);

    return status;
}


static SIXELSTATUS
sixel_encoder_output_with_macro(
    sixel_frame_t   /* in */ *frame,
    sixel_dither_t  /* in */ *dither,
    sixel_output_t  /* in */ *output,
    sixel_encoder_t /* in */ *encoder)
{
    SIXELSTATUS status = SIXEL_OK;
    char buffer[256];
    int nwrite;
#if HAVE_NANOSLEEP
    int dulation;
    int lag = 0;
    struct timespec tv;
# if HAVE_CLOCK
    clock_t start;
# endif
#endif
    unsigned char *pixbuf;
    int width;
    int height;
#if HAVE_NANOSLEEP
    int delay;
#endif

#if HAVE_NANOSLEEP && HAVE_CLOCK
    start = clock();
#endif
    if (sixel_frame_get_loop_no(frame) == 0) {
        if (encoder->macro_number >= 0) {
            nwrite = sprintf(buffer, "\033P%d;0;1!z", encoder->macro_number);
        } else {
            nwrite = sprintf(buffer, "\033P%d;0;1!z", sixel_frame_get_frame_no(frame));
        }
        if (nwrite < 0) {
            status = (SIXEL_LIBC_ERROR | (errno & 0xff));
            sixel_helper_set_additional_message(
                "sixel_encoder_output_with_macro: sprintf() failed.");
            goto end;
        }
        nwrite = sixel_write_callback(buffer, (int)strlen(buffer), &encoder->outfd);
        if (nwrite < 0) {
            status = (SIXEL_LIBC_ERROR | (errno & 0xff));
            sixel_helper_set_additional_message(
                "sixel_encoder_output_with_macro: sixel_write_callback() failed.");
            goto end;
        }

        pixbuf = sixel_frame_get_pixels(frame),
        width = sixel_frame_get_width(frame),
        height = sixel_frame_get_height(frame),
        status = sixel_encode(pixbuf, width, height, /* unused */ 3, dither, output);
        if (SIXEL_FAILED(status)) {
            goto end;
        }

        nwrite = sixel_write_callback("\033\\", 2, &encoder->outfd);
        if (nwrite < 0) {
            status = (SIXEL_LIBC_ERROR | (errno & 0xff));
            sixel_helper_set_additional_message(
                "sixel_encoder_output_with_macro: sixel_write_callback() failed.");
            goto end;
        }
    }
    if (encoder->macro_number < 0) {
        nwrite = sprintf(buffer, "\033[%d*z", sixel_frame_get_frame_no(frame));
        if (nwrite < 0) {
            status = (SIXEL_LIBC_ERROR | (errno & 0xff));
            sixel_helper_set_additional_message(
                "sixel_encoder_output_with_macro: sprintf() failed.");
        }
        nwrite = sixel_write_callback(buffer, (int)strlen(buffer), &encoder->outfd);
        if (nwrite < 0) {
            status = (SIXEL_LIBC_ERROR | (errno & 0xff));
            sixel_helper_set_additional_message(
                "sixel_encoder_output_with_macro: sixel_write_callback() failed.");
            goto end;
        }
#if HAVE_NANOSLEEP
        delay = sixel_frame_get_delay(frame);
        if (delay > 0 && !encoder->fignore_delay) {
# if HAVE_CLOCK
            dulation = (int)((clock() - start) * 1000 * 1000 / CLOCKS_PER_SEC) - (int)lag;
            lag = 0;
# else
            dulation = 0;
# endif
            if (dulation < 10000 * delay) {
                tv.tv_sec = 0;
                tv.tv_nsec = (long)((10000 * delay - dulation) * 1000);
                nanosleep(&tv, NULL);
            } else {
                lag = (int)(10000 * delay - dulation);
            }
        }
#endif
    }

end:
    return status;
}


static SIXELSTATUS
sixel_encoder_encode_frame(
    sixel_encoder_t *encoder,
    sixel_frame_t   *frame,
    sixel_output_t  *output)
{
    SIXELSTATUS status = SIXEL_FALSE;
    sixel_dither_t *dither = NULL;
    int height;
    int is_animation = 0;
    int nwrite;

    /* evaluate -w, -h, and -c option: crop/scale input source */
    if (encoder->clipfirst) {
        /* clipping */
        status = sixel_encoder_do_clip(encoder, frame);
        if (SIXEL_FAILED(status)) {
            goto end;
        }

        /* scaling */
        status = sixel_encoder_do_resize(encoder, frame);
        if (SIXEL_FAILED(status)) {
            goto end;
        }
    } else {
        /* scaling */
        status = sixel_encoder_do_resize(encoder, frame);
        if (SIXEL_FAILED(status)) {
            goto end;
        }

        /* clipping */
        status = sixel_encoder_do_clip(encoder, frame);
        if (SIXEL_FAILED(status)) {
            goto end;
        }
    }

    /* prepare dither context */
    status = sixel_encoder_prepare_palette(encoder, frame, &dither);
    if (status != SIXEL_OK) {
        goto end;
    }

    if (encoder->dither_cache != NULL) {
        encoder->dither_cache = dither;
        sixel_dither_ref(dither);
    }

    /* evaluate -v option: print palette */
    if (encoder->verbose) {
        if ((sixel_frame_get_pixelformat(frame) & SIXEL_FORMATTYPE_PALETTE)) {
            sixel_debug_print_palette(dither);
        }
    }

    /* evaluate -d option: set method for diffusion */
    sixel_dither_set_diffusion_type(dither, encoder->method_for_diffuse);

    /* evaluate -C option: set complexion score */
    if (encoder->complexion > 1) {
        sixel_dither_set_complexion_score(dither, encoder->complexion);
    }

    if (output) {
        sixel_output_ref(output);
    } else {
        /* create output context */
        if (encoder->fuse_macro || encoder->macro_number >= 0) {
            /* -u or -n option */
            status = sixel_output_new(&output,
                                      sixel_hex_write_callback,
                                      &encoder->outfd,
                                      encoder->allocator);
        } else {
            status = sixel_output_new(&output,
                                      sixel_write_callback,
                                      &encoder->outfd,
                                      encoder->allocator);
        }
        if (SIXEL_FAILED(status)) {
            goto end;
        }
    }

    sixel_output_set_8bit_availability(output, encoder->f8bit);
    sixel_output_set_gri_arg_limit(output, encoder->has_gri_arg_limit);
    sixel_output_set_palette_type(output, encoder->palette_type);
    sixel_output_set_penetrate_multiplexer(
        output, encoder->penetrate_multiplexer);
    sixel_output_set_encode_policy(output, encoder->encode_policy);

    if (sixel_frame_get_multiframe(frame) && !encoder->fstatic) {
        if (sixel_frame_get_loop_no(frame) != 0 || sixel_frame_get_frame_no(frame) != 0) {
            is_animation = 1;
        }
        height = sixel_frame_get_height(frame);
        (void) sixel_tty_scroll(sixel_write_callback, encoder->outfd, height, is_animation);
    }

    if (encoder->cancel_flag && *encoder->cancel_flag) {
        status = SIXEL_INTERRUPTED;
        goto end;
    }

    /* output sixel: junction of multi-frame processing strategy */
    if (encoder->fuse_macro) {  /* -u option */
        /* use macro */
        status = sixel_encoder_output_with_macro(frame, dither, output, encoder);
    } else if (encoder->macro_number >= 0) { /* -n option */
        /* use macro */
        status = sixel_encoder_output_with_macro(frame, dither, output, encoder);
    } else {
        /* do not use macro */
        status = sixel_encoder_output_without_macro(frame, dither, output, encoder);
    }

    if (encoder->cancel_flag && *encoder->cancel_flag) {
        nwrite = sixel_write_callback("\x18\033\\", 3, &encoder->outfd);
        if (nwrite < 0) {
            status = (SIXEL_LIBC_ERROR | (errno & 0xff));
            sixel_helper_set_additional_message(
                "load_image_callback: sixel_write_callback() failed.");
            goto end;
        }
        status = SIXEL_INTERRUPTED;
    }

    if (SIXEL_FAILED(status)) {
        goto end;
    }

end:
    if (output) {
        sixel_output_unref(output);
    }
    if (dither) {
        sixel_dither_unref(dither);
    }

    return status;
}


/* create encoder object */
SIXELAPI SIXELSTATUS
sixel_encoder_new(
    sixel_encoder_t     /* out */ **ppencoder, /* encoder object to be created */
    sixel_allocator_t   /* in */  *allocator)  /* allocator, null if you use
                                                  default allocator */
{
    SIXELSTATUS status = SIXEL_FALSE;
    char const *env_default_bgcolor;
    char const *env_default_ncolors;
    int ncolors;

    if (allocator == NULL) {
        status = sixel_allocator_new(&allocator, NULL, NULL, NULL, NULL);
        if (SIXEL_FAILED(status)) {
            goto end;
        }
    } else {
        sixel_allocator_ref(allocator);
    }

    *ppencoder
        = (sixel_encoder_t *)sixel_allocator_malloc(allocator,
                                                    sizeof(sixel_encoder_t));
    if (*ppencoder == NULL) {
        sixel_helper_set_additional_message(
            "sixel_encoder_new: sixel_allocator_malloc() failed.");
        status = SIXEL_BAD_ALLOCATION;
        sixel_allocator_unref(allocator);
        goto end;
    }

    (*ppencoder)->ref                   = 1;
    (*ppencoder)->reqcolors             = (-1);
    (*ppencoder)->mapfile               = NULL;
    (*ppencoder)->color_option          = SIXEL_COLOR_OPTION_DEFAULT;
    (*ppencoder)->builtin_palette       = 0;
    (*ppencoder)->method_for_diffuse    = SIXEL_DIFFUSE_AUTO;
    (*ppencoder)->method_for_largest    = SIXEL_LARGE_AUTO;
    (*ppencoder)->method_for_rep        = SIXEL_REP_AUTO;
    (*ppencoder)->quality_mode          = SIXEL_QUALITY_AUTO;
    (*ppencoder)->method_for_resampling = SIXEL_RES_BILINEAR;
    (*ppencoder)->loop_mode             = SIXEL_LOOP_AUTO;
    (*ppencoder)->palette_type          = SIXEL_PALETTETYPE_AUTO;
    (*ppencoder)->f8bit                 = 0;
    (*ppencoder)->has_gri_arg_limit     = 0;
    (*ppencoder)->finvert               = 0;
    (*ppencoder)->fuse_macro            = 0;
    (*ppencoder)->fignore_delay         = 0;
    (*ppencoder)->complexion            = 1;
    (*ppencoder)->fstatic               = 0;
    (*ppencoder)->pixelwidth            = (-1);
    (*ppencoder)->pixelheight           = (-1);
    (*ppencoder)->percentwidth          = (-1);
    (*ppencoder)->percentheight         = (-1);
    (*ppencoder)->clipx                 = 0;
    (*ppencoder)->clipy                 = 0;
    (*ppencoder)->clipwidth             = 0;
    (*ppencoder)->clipheight            = 0;
    (*ppencoder)->clipfirst             = 0;
    (*ppencoder)->macro_number          = (-1);
    (*ppencoder)->verbose               = 0;
    (*ppencoder)->penetrate_multiplexer = 0;
    (*ppencoder)->encode_policy         = SIXEL_ENCODEPOLICY_AUTO;
    (*ppencoder)->pipe_mode             = 0;
    (*ppencoder)->bgcolor               = NULL;
    (*ppencoder)->outfd                 = STDOUT_FILENO;
    (*ppencoder)->finsecure             = 0;
    (*ppencoder)->cancel_flag           = NULL;
    (*ppencoder)->dither_cache          = NULL;
    (*ppencoder)->allocator             = allocator;

    /* evaluate environment variable ${SIXEL_BGCOLOR} */
    env_default_bgcolor = getenv("SIXEL_BGCOLOR");
    if (env_default_bgcolor) {
        status = sixel_parse_x_colorspec(&(*ppencoder)->bgcolor,
                                         env_default_bgcolor,
                                         allocator);
        if (SIXEL_FAILED(status)) {
            goto error;
        }
    }

    /* evaluate environment variable ${SIXEL_COLORS} */
    env_default_ncolors = getenv("SIXEL_COLORS");
    if (env_default_ncolors) {
        ncolors = atoi(env_default_ncolors); /* may overflow */
        if (ncolors > 1 && ncolors <= 256) {
            (*ppencoder)->reqcolors = ncolors;
        }
    }

    /* success */
    status = SIXEL_OK;

    goto end;

error:
    sixel_allocator_free(allocator, *ppencoder);
    sixel_allocator_unref(allocator);
    *ppencoder = NULL;

end:
    return status;
}


/* create encoder object (deprecated version) */
SIXELAPI /* deprecated */ sixel_encoder_t *
sixel_encoder_create(void)
{
    SIXELSTATUS status = SIXEL_FALSE;
    sixel_encoder_t *encoder = NULL;

    status = sixel_encoder_new(&encoder, NULL);
    if (SIXEL_FAILED(status)) {
        return NULL;
    }

    return encoder;
}


/* destroy encoder object */
static void
sixel_encoder_destroy(sixel_encoder_t *encoder)
{
    sixel_allocator_t *allocator;

    if (encoder) {
        allocator = encoder->allocator;
        sixel_allocator_free(allocator, encoder->mapfile);
        sixel_allocator_free(allocator, encoder->bgcolor);
        sixel_dither_unref(encoder->dither_cache);
        if (encoder->outfd
            && encoder->outfd != STDOUT_FILENO
            && encoder->outfd != STDERR_FILENO) {
            close(encoder->outfd);
        }
        sixel_allocator_free(allocator, encoder);
        sixel_allocator_unref(allocator);
    }
}


/* increase reference count of encoder object (thread-unsafe) */
SIXELAPI void
sixel_encoder_ref(sixel_encoder_t *encoder)
{
    /* TODO: be thread safe */
    ++encoder->ref;
}


/* decrease reference count of encoder object (thread-unsafe) */
SIXELAPI void
sixel_encoder_unref(sixel_encoder_t *encoder)
{
    /* TODO: be thread safe */
    if (encoder != NULL && --encoder->ref == 0) {
        sixel_encoder_destroy(encoder);
    }
}


/* set cancel state flag to encoder object */
SIXELAPI SIXELSTATUS
sixel_encoder_set_cancel_flag(
    sixel_encoder_t /* in */ *encoder,
    int             /* in */ *cancel_flag
)
{
    SIXELSTATUS status = SIXEL_OK;

    encoder->cancel_flag = cancel_flag;

    return status;
}


/* set an option flag to encoder object */
SIXELAPI SIXELSTATUS
sixel_encoder_setopt(
    sixel_encoder_t /* in */ *encoder,
    int             /* in */ arg,
    char const      /* in */ *value)
{
    SIXELSTATUS status = SIXEL_FALSE;
    int number;
    int parsed;
    char unit[32];

    sixel_encoder_ref(encoder);

    switch(arg) {
    case SIXEL_OPTFLAG_OUTFILE:  /* o */
        if (*value == '\0') {
            sixel_helper_set_additional_message(
                "no file name specified.");
            status = SIXEL_BAD_ARGUMENT;
            goto end;
        }
        if (strcmp(value, "-") != 0) {
            if (encoder->outfd && encoder->outfd != STDOUT_FILENO) {
                close(encoder->outfd);
            }
            encoder->outfd = open(value,
                                  O_RDWR|O_CREAT|O_TRUNC,
                                  S_IRUSR|S_IWUSR);
        }
        break;
    case SIXEL_OPTFLAG_7BIT_MODE:  /* 7 */
        encoder->f8bit = 0;
        break;
    case SIXEL_OPTFLAG_8BIT_MODE:  /* 8 */
        encoder->f8bit = 1;
        break;
    case SIXEL_OPTFLAG_HAS_GRI_ARG_LIMIT:  /* R */
        encoder->has_gri_arg_limit = 1;
        break;
    case SIXEL_OPTFLAG_COLORS:  /* p */
        encoder->reqcolors = atoi(value);
        break;
    case SIXEL_OPTFLAG_MAPFILE:  /* m */
        if (encoder->mapfile) {
            sixel_allocator_free(encoder->allocator, encoder->mapfile);
        }
        encoder->mapfile = arg_strdup(value, encoder->allocator);
        if (encoder->mapfile == NULL) {
            sixel_helper_set_additional_message(
                "sixel_encoder_setopt: sixel_allocator_malloc() failed.");
            status = SIXEL_BAD_ALLOCATION;
            goto end;
        }
        encoder->color_option = SIXEL_COLOR_OPTION_MAPFILE;
        break;
    case SIXEL_OPTFLAG_MONOCHROME:  /* e */
        encoder->color_option = SIXEL_COLOR_OPTION_MONOCHROME;
        break;
    case SIXEL_OPTFLAG_HIGH_COLOR:  /* I */
        encoder->color_option = SIXEL_COLOR_OPTION_HIGHCOLOR;
        break;
    case SIXEL_OPTFLAG_BUILTIN_PALETTE:  /* b */
        if (strcmp(value, "xterm16") == 0) {
            encoder->builtin_palette = SIXEL_BUILTIN_XTERM16;
        } else if (strcmp(value, "xterm256") == 0) {
            encoder->builtin_palette = SIXEL_BUILTIN_XTERM256;
        } else if (strcmp(value, "vt340mono") == 0) {
            encoder->builtin_palette = SIXEL_BUILTIN_VT340_MONO;
        } else if (strcmp(value, "vt340color") == 0) {
            encoder->builtin_palette = SIXEL_BUILTIN_VT340_COLOR;
        } else if (strcmp(value, "gray1") == 0) {
            encoder->builtin_palette = SIXEL_BUILTIN_G1;
        } else if (strcmp(value, "gray2") == 0) {
            encoder->builtin_palette = SIXEL_BUILTIN_G2;
        } else if (strcmp(value, "gray4") == 0) {
            encoder->builtin_palette = SIXEL_BUILTIN_G4;
        } else if (strcmp(value, "gray8") == 0) {
            encoder->builtin_palette = SIXEL_BUILTIN_G8;
        } else {
            sixel_helper_set_additional_message(
                    "cannot parse builtin palette option.");
            status = SIXEL_BAD_ARGUMENT;
            goto end;
        }
        encoder->color_option = SIXEL_COLOR_OPTION_BUILTIN;
        break;
    case SIXEL_OPTFLAG_DIFFUSION:  /* d */
        /* parse --diffusion option */
        if (strcmp(value, "auto") == 0) {
            encoder->method_for_diffuse = SIXEL_DIFFUSE_AUTO;
        } else if (strcmp(value, "none") == 0) {
            encoder->method_for_diffuse = SIXEL_DIFFUSE_NONE;
        } else if (strcmp(value, "fs") == 0) {
            encoder->method_for_diffuse = SIXEL_DIFFUSE_FS;
        } else if (strcmp(value, "atkinson") == 0) {
            encoder->method_for_diffuse = SIXEL_DIFFUSE_ATKINSON;
        } else if (strcmp(value, "jajuni") == 0) {
            encoder->method_for_diffuse = SIXEL_DIFFUSE_JAJUNI;
        } else if (strcmp(value, "stucki") == 0) {
            encoder->method_for_diffuse = SIXEL_DIFFUSE_STUCKI;
        } else if (strcmp(value, "burkes") == 0) {
            encoder->method_for_diffuse = SIXEL_DIFFUSE_BURKES;
        } else if (strcmp(value, "a_dither") == 0) {
            encoder->method_for_diffuse = SIXEL_DIFFUSE_A_DITHER;
        } else if (strcmp(value, "x_dither") == 0) {
            encoder->method_for_diffuse = SIXEL_DIFFUSE_X_DITHER;
        } else {
            sixel_helper_set_additional_message(
                "specified diffusion method is not supported.");
            status = SIXEL_BAD_ARGUMENT;
            goto end;
        }
        break;
    case SIXEL_OPTFLAG_FIND_LARGEST:  /* f */
        /* parse --find-largest option */
        if (value) {
            if (strcmp(value, "auto") == 0) {
                encoder->method_for_largest = SIXEL_LARGE_AUTO;
            } else if (strcmp(value, "norm") == 0) {
                encoder->method_for_largest = SIXEL_LARGE_NORM;
            } else if (strcmp(value, "lum") == 0) {
                encoder->method_for_largest = SIXEL_LARGE_LUM;
            } else {
                sixel_helper_set_additional_message(
                    "specified finding method is not supported.");
                status = SIXEL_BAD_ARGUMENT;
                goto end;
            }
        }
        break;
    case SIXEL_OPTFLAG_SELECT_COLOR:  /* s */
        /* parse --select-color option */
        if (strcmp(value, "auto") == 0) {
            encoder->method_for_rep = SIXEL_REP_AUTO;
        } else if (strcmp(value, "center") == 0) {
            encoder->method_for_rep = SIXEL_REP_CENTER_BOX;
        } else if (strcmp(value, "average") == 0) {
            encoder->method_for_rep = SIXEL_REP_AVERAGE_COLORS;
        } else if ((strcmp(value, "histogram") == 0) ||
                   (strcmp(value, "histgram") == 0)) {
            encoder->method_for_rep = SIXEL_REP_AVERAGE_PIXELS;
        } else {
            sixel_helper_set_additional_message(
                "specified finding method is not supported.");
            status = SIXEL_BAD_ARGUMENT;
            goto end;
        }
        break;
    case SIXEL_OPTFLAG_CROP:  /* c */
        number = sscanf(value, "%dx%d+%d+%d",
                        &encoder->clipwidth, &encoder->clipheight,
                        &encoder->clipx, &encoder->clipy);
        if (number != 4) {
            status = SIXEL_BAD_ARGUMENT;
            goto end;
        }
        if (encoder->clipwidth <= 0 || encoder->clipheight <= 0) {
            status = SIXEL_BAD_ARGUMENT;
            goto end;
        }
        if (encoder->clipx < 0 || encoder->clipy < 0) {
            status = SIXEL_BAD_ARGUMENT;
            goto end;
        }
        encoder->clipfirst = 0;
        break;
    case SIXEL_OPTFLAG_WIDTH:  /* w */
        parsed = sscanf(value, "%d%2s", &number, unit);
        if (parsed == 2 && strcmp(unit, "%") == 0) {
            encoder->pixelwidth = (-1);
            encoder->percentwidth = number;
        } else if (parsed == 1 || (parsed == 2 && strcmp(unit, "px") == 0)) {
            encoder->pixelwidth = number;
            encoder->percentwidth = (-1);
        } else if (strcmp(value, "auto") == 0) {
            encoder->pixelwidth = (-1);
            encoder->percentwidth = (-1);
        } else {
            sixel_helper_set_additional_message(
                "cannot parse -w/--width option.");
            status = SIXEL_BAD_ARGUMENT;
            goto end;
        }
        if (encoder->clipwidth) {
            encoder->clipfirst = 1;
        }
        break;
    case SIXEL_OPTFLAG_HEIGHT:  /* h */
        parsed = sscanf(value, "%d%2s", &number, unit);
        if (parsed == 2 && strcmp(unit, "%") == 0) {
            encoder->pixelheight = (-1);
            encoder->percentheight = number;
        } else if (parsed == 1 || (parsed == 2 && strcmp(unit, "px") == 0)) {
            encoder->pixelheight = number;
            encoder->percentheight = (-1);
        } else if (strcmp(value, "auto") == 0) {
            encoder->pixelheight = (-1);
            encoder->percentheight = (-1);
        } else {
            sixel_helper_set_additional_message(
                "cannot parse -h/--height option.");
            status = SIXEL_BAD_ARGUMENT;
            goto end;
        }
        if (encoder->clipheight) {
            encoder->clipfirst = 1;
        }
        break;
    case SIXEL_OPTFLAG_RESAMPLING:  /* r */
        /* parse --resampling option */
        if (strcmp(value, "nearest") == 0) {
            encoder->method_for_resampling = SIXEL_RES_NEAREST;
        } else if (strcmp(value, "gaussian") == 0) {
            encoder->method_for_resampling = SIXEL_RES_GAUSSIAN;
        } else if (strcmp(value, "hanning") == 0) {
            encoder->method_for_resampling = SIXEL_RES_HANNING;
        } else if (strcmp(value, "hamming") == 0) {
            encoder->method_for_resampling = SIXEL_RES_HAMMING;
        } else if (strcmp(value, "bilinear") == 0) {
            encoder->method_for_resampling = SIXEL_RES_BILINEAR;
        } else if (strcmp(value, "welsh") == 0) {
            encoder->method_for_resampling = SIXEL_RES_WELSH;
        } else if (strcmp(value, "bicubic") == 0) {
            encoder->method_for_resampling = SIXEL_RES_BICUBIC;
        } else if (strcmp(value, "lanczos2") == 0) {
            encoder->method_for_resampling = SIXEL_RES_LANCZOS2;
        } else if (strcmp(value, "lanczos3") == 0) {
            encoder->method_for_resampling = SIXEL_RES_LANCZOS3;
        } else if (strcmp(value, "lanczos4") == 0) {
            encoder->method_for_resampling = SIXEL_RES_LANCZOS4;
        } else {
            sixel_helper_set_additional_message(
                "specified desampling method is not supported.");
            status = SIXEL_BAD_ARGUMENT;
            goto end;
        }
        break;
    case SIXEL_OPTFLAG_QUALITY:  /* q */
        /* parse --quality option */
        if (strcmp(value, "auto") == 0) {
            encoder->quality_mode = SIXEL_QUALITY_AUTO;
        } else if (strcmp(value, "high") == 0) {
            encoder->quality_mode = SIXEL_QUALITY_HIGH;
        } else if (strcmp(value, "low") == 0) {
            encoder->quality_mode = SIXEL_QUALITY_LOW;
        } else if (strcmp(value, "full") == 0) {
            encoder->quality_mode = SIXEL_QUALITY_FULL;
        } else {
            sixel_helper_set_additional_message(
                "cannot parse quality option.");
            status = SIXEL_BAD_ARGUMENT;
            goto end;
        }
        break;
    case SIXEL_OPTFLAG_LOOPMODE:  /* l */
        /* parse --loop-control option */
        if (strcmp(value, "auto") == 0) {
            encoder->loop_mode = SIXEL_LOOP_AUTO;
        } else if (strcmp(value, "force") == 0) {
            encoder->loop_mode = SIXEL_LOOP_FORCE;
        } else if (strcmp(value, "disable") == 0) {
            encoder->loop_mode = SIXEL_LOOP_DISABLE;
        } else {
            sixel_helper_set_additional_message(
                "cannot parse loop-control option.");
            status = SIXEL_BAD_ARGUMENT;
            goto end;
        }
        break;
    case SIXEL_OPTFLAG_PALETTE_TYPE:  /* t */
        /* parse --palette-type option */
        if (strcmp(value, "auto") == 0) {
            encoder->palette_type = SIXEL_PALETTETYPE_AUTO;
        } else if (strcmp(value, "hls") == 0) {
            encoder->palette_type = SIXEL_PALETTETYPE_HLS;
        } else if (strcmp(value, "rgb") == 0) {
            encoder->palette_type = SIXEL_PALETTETYPE_RGB;
        } else {
            sixel_helper_set_additional_message(
                "cannot parse palette type option.");
            status = SIXEL_BAD_ARGUMENT;
            goto end;
        }
        break;
    case SIXEL_OPTFLAG_BGCOLOR:  /* B */
        /* parse --bgcolor option */
        if (encoder->bgcolor) {
            sixel_allocator_free(encoder->allocator, encoder->bgcolor);
        }
        status = sixel_parse_x_colorspec(&encoder->bgcolor,
                                         value,
                                         encoder->allocator);
        if (SIXEL_FAILED(status)) {
            sixel_helper_set_additional_message(
                "cannot parse bgcolor option.");
            status = SIXEL_BAD_ARGUMENT;
            goto end;
        }
        break;
    case SIXEL_OPTFLAG_INSECURE:  /* k */
        encoder->finsecure = 1;
        break;
    case SIXEL_OPTFLAG_INVERT:  /* i */
        encoder->finvert = 1;
        break;
    case SIXEL_OPTFLAG_USE_MACRO:  /* u */
        encoder->fuse_macro = 1;
        break;
    case SIXEL_OPTFLAG_MACRO_NUMBER:  /* n */
        encoder->macro_number = atoi(value);
        if (encoder->macro_number < 0) {
            status = SIXEL_BAD_ARGUMENT;
            goto end;
        }
        break;
    case SIXEL_OPTFLAG_IGNORE_DELAY:  /* g */
        encoder->fignore_delay = 1;
        break;
    case SIXEL_OPTFLAG_VERBOSE:  /* v */
        encoder->verbose = 1;
        break;
    case SIXEL_OPTFLAG_STATIC:  /* S */
        encoder->fstatic = 1;
        break;
    case SIXEL_OPTFLAG_PENETRATE:  /* P */
        encoder->penetrate_multiplexer = 1;
        break;
    case SIXEL_OPTFLAG_ENCODE_POLICY:  /* E */
        if (strcmp(value, "auto") == 0) {
            encoder->encode_policy = SIXEL_ENCODEPOLICY_AUTO;
        } else if (strcmp(value, "fast") == 0) {
            encoder->encode_policy = SIXEL_ENCODEPOLICY_FAST;
        } else if (strcmp(value, "size") == 0) {
            encoder->encode_policy = SIXEL_ENCODEPOLICY_SIZE;
        } else {
            sixel_helper_set_additional_message(
                "cannot parse encode policy option.");
            status = SIXEL_BAD_ARGUMENT;
            goto end;
        }
        break;
    case SIXEL_OPTFLAG_COMPLEXION_SCORE:  /* C */
        encoder->complexion = atoi(value);
        if (encoder->complexion < 1) {
            sixel_helper_set_additional_message(
                "complexion parameter must be 1 or more.");
            status = SIXEL_BAD_ARGUMENT;
            goto end;
        }
        break;
    case SIXEL_OPTFLAG_PIPE_MODE:  /* D */
        encoder->pipe_mode = 1;
        break;
    case '?':  /* unknown option */
    default:
        /* exit if unknown options are specified */
        sixel_helper_set_additional_message(
            "unknwon option is specified.");
        status = SIXEL_BAD_ARGUMENT;
        goto end;
    }

    /* detects arguments conflictions */
    if (encoder->reqcolors != (-1)) {
        switch (encoder->color_option) {
        case SIXEL_COLOR_OPTION_MAPFILE:
            sixel_helper_set_additional_message(
                "option -p, --colors conflicts with -m, --mapfile.");
            status = SIXEL_BAD_ARGUMENT;
            goto end;
        case SIXEL_COLOR_OPTION_MONOCHROME:
            sixel_helper_set_additional_message(
                "option -e, --monochrome conflicts with -p, --colors.");
            status = SIXEL_BAD_ARGUMENT;
            goto end;
        case SIXEL_COLOR_OPTION_HIGHCOLOR:
            sixel_helper_set_additional_message(
                "option -p, --colors conflicts with -I, --high-color.");
            status = SIXEL_BAD_ARGUMENT;
            goto end;
        case SIXEL_COLOR_OPTION_BUILTIN:
            sixel_helper_set_additional_message(
                "option -p, --colors conflicts with -b, --builtin-palette.");
            status = SIXEL_BAD_ARGUMENT;
            goto end;
        default:
            break;
        }
    }

    /* 8bit output option(-8) conflicts width GNU Screen integration(-P) */
    if (encoder->f8bit && encoder->penetrate_multiplexer) {
        sixel_helper_set_additional_message(
            "option -8 --8bit-mode conflicts"
            " with -P, --penetrate.");
        status = SIXEL_BAD_ARGUMENT;
        goto end;
    }

    status = SIXEL_OK;

end:
    sixel_encoder_unref(encoder);

    return status;
}


/* called when image loader component load a image frame */
static SIXELSTATUS
load_image_callback(sixel_frame_t *frame, void *data)
{
    return sixel_encoder_encode_frame((sixel_encoder_t *)data, frame, NULL);
}


/* load source data from specified file and encode it to SIXEL format
 * output to encoder->outfd */
SIXELAPI SIXELSTATUS
sixel_encoder_encode(
    sixel_encoder_t *encoder,   /* encoder object */
    char const      *filename)  /* input filename */
{
    SIXELSTATUS status = SIXEL_FALSE;
    int fuse_palette = 1;

    if (encoder == NULL) {
#if HAVE_DIAGNOSTIC_DEPRECATED_DECLARATIONS
#  pragma GCC diagnostic push
#  pragma GCC diagnostic ignored "-Wdeprecated-declarations"
#endif
        encoder = sixel_encoder_create();
#if HAVE_DIAGNOSTIC_DEPRECATED_DECLARATIONS
#  pragma GCC diagnostic pop
#endif
        if (encoder == NULL) {
            sixel_helper_set_additional_message(
                "sixel_encoder_encode: sixel_encoder_create() failed.");
            status = SIXEL_BAD_ALLOCATION;
            goto end;
        }
    } else {
        sixel_encoder_ref(encoder);
    }

    /* if required color is not set, set the max value */
    if (encoder->reqcolors == (-1)) {
        encoder->reqcolors = SIXEL_PALETTE_MAX;
    }

    /* if required color is less then 2, set the min value */
    if (encoder->reqcolors < 2) {
        encoder->reqcolors = SIXEL_PALETTE_MIN;
    }

    /* if color space option is not set, choose RGB color space */
    if (encoder->palette_type == SIXEL_PALETTETYPE_AUTO) {
        encoder->palette_type = SIXEL_PALETTETYPE_RGB;
    }

    /* if color option is not default value, prohibit to read
       the file as a paletted image */
    if (encoder->color_option != SIXEL_COLOR_OPTION_DEFAULT) {
        fuse_palette = 0;
    }

    /* if scaling options are set, prohibit to read the file as
       a paletted image */
    if (encoder->percentwidth > 0 ||
        encoder->percentheight > 0 ||
        encoder->pixelwidth > 0 ||
        encoder->pixelheight > 0) {
        fuse_palette = 0;
    }

reload:
    status = sixel_helper_load_image_file(filename,
                                          encoder->fstatic,
                                          fuse_palette,
                                          encoder->reqcolors,
                                          encoder->bgcolor,
                                          encoder->loop_mode,
                                          load_image_callback,
                                          encoder->finsecure,
                                          encoder->cancel_flag,
                                          (void *)encoder,
                                          encoder->allocator);
    if (status != SIXEL_OK) {
        goto end;
    }

    if (encoder->pipe_mode) {
#if HAVE_CLEARERR
        clearerr(stdin);
#endif  /* HAVE_FSEEK */
        while (encoder->cancel_flag && !*encoder->cancel_flag) {
            status = sixel_tty_wait_stdin(1000000);
            if (SIXEL_FAILED(status)) {
                goto end;
            }
            if (status != SIXEL_OK) {
                break;
            }
        }
        if (!encoder->cancel_flag || !*encoder->cancel_flag) {
            goto reload;
        }
    }

    /* the status may not be SIXEL_OK */

end:
    sixel_encoder_unref(encoder);

    return status;
}


/* encode specified pixel data to SIXEL format
 * output to encoder->outfd */
SIXELAPI SIXELSTATUS
sixel_encoder_encode_bytes(
    sixel_encoder_t     /* in */    *encoder,
    unsigned char       /* in */    *bytes,
    int                 /* in */    width,
    int                 /* in */    height,
    int                 /* in */    pixelformat,
    unsigned char       /* in */    *palette,
    int                 /* in */    ncolors)
{
    SIXELSTATUS status = SIXEL_FALSE;
    sixel_frame_t *frame;

    if (encoder == NULL || bytes == NULL) {
        status = SIXEL_BAD_ARGUMENT;
        goto end;
    }

    status = sixel_frame_new(&frame, encoder->allocator);
    if (SIXEL_FAILED(status)) {
        goto end;
    }

    status = sixel_frame_init(frame, bytes, width, height,
                              pixelformat, palette, ncolors);
    if (SIXEL_FAILED(status)) {
        goto end;
    }

    status = sixel_encoder_encode_frame(encoder, frame, NULL);
    if (SIXEL_FAILED(status)) {
        goto end;
    }

    status = SIXEL_OK;

end:
    return status;
}


#if HAVE_TESTS
static int
test1(void)
{
    int nret = EXIT_FAILURE;
    sixel_encoder_t *encoder = NULL;

#if HAVE_DIAGNOSTIC_DEPRECATED_DECLARATIONS
#  pragma GCC diagnostic push
#  pragma GCC diagnostic ignored "-Wdeprecated-declarations"
#endif
    encoder = sixel_encoder_create();
#if HAVE_DIAGNOSTIC_DEPRECATED_DECLARATIONS
#  pragma GCC diagnostic pop
#endif
    if (encoder == NULL) {
        goto error;
    }
    sixel_encoder_ref(encoder);
    sixel_encoder_unref(encoder);
    nret = EXIT_SUCCESS;

error:
    sixel_encoder_unref(encoder);
    return nret;
}


static int
test2(void)
{
    int nret = EXIT_FAILURE;
    SIXELSTATUS status;
    sixel_encoder_t *encoder = NULL;
    sixel_frame_t *frame = NULL;
    unsigned char *buffer;
    int height = 0;
    int is_animation = 0;

#if HAVE_DIAGNOSTIC_DEPRECATED_DECLARATIONS
#  pragma GCC diagnostic push
#  pragma GCC diagnostic ignored "-Wdeprecated-declarations"
#endif
    encoder = sixel_encoder_create();
#if HAVE_DIAGNOSTIC_DEPRECATED_DECLARATIONS
#  pragma GCC diagnostic pop
#endif
    if (encoder == NULL) {
        goto error;
    }

#if HAVE_DIAGNOSTIC_DEPRECATED_DECLARATIONS
#  pragma GCC diagnostic push
#  pragma GCC diagnostic ignored "-Wdeprecated-declarations"
#endif
    frame = sixel_frame_create();
#if HAVE_DIAGNOSTIC_DEPRECATED_DECLARATIONS
#  pragma GCC diagnostic pop
#endif
    if (encoder == NULL) {
        goto error;
    }

    buffer = (unsigned char *)sixel_allocator_malloc(encoder->allocator, 3);
    if (buffer == NULL) {
        goto error;
    }
    status = sixel_frame_init(frame, buffer, 1, 1,
                              SIXEL_PIXELFORMAT_RGB888,
                              NULL, 0);
    if (SIXEL_FAILED(status)) {
        goto error;
    }

    if (sixel_frame_get_loop_no(frame) != 0 || sixel_frame_get_frame_no(frame) != 0) {
        is_animation = 1;
    }

    height = sixel_frame_get_height(frame);

    status = sixel_tty_scroll(sixel_write_callback, encoder->outfd, height, is_animation);
    if (SIXEL_FAILED(status)) {
        goto error;
    }

    nret = EXIT_SUCCESS;

error:
    sixel_encoder_unref(encoder);
    sixel_frame_unref(frame);
    return nret;
}


static int
test3(void)
{
    int nret = EXIT_FAILURE;
    int result;

    result = sixel_tty_wait_stdin(1000);
    if (result != 0) {
        goto error;
    }

    nret = EXIT_SUCCESS;

error:
    return nret;
}


static int
test4(void)
{
    int nret = EXIT_FAILURE;
    sixel_encoder_t *encoder = NULL;
    SIXELSTATUS status;

#if HAVE_DIAGNOSTIC_DEPRECATED_DECLARATIONS
# pragma GCC diagnostic push
# pragma GCC diagnostic ignored "-Wdeprecated-declarations"
#endif
    encoder = sixel_encoder_create();
#if HAVE_DIAGNOSTIC_DEPRECATED_DECLARATIONS
# pragma GCC diagnostic pop
#endif
    if (encoder == NULL) {
        goto error;
    }

    status = sixel_encoder_setopt(encoder,
                                  SIXEL_OPTFLAG_LOOPMODE,
                                  "force");
    if (SIXEL_FAILED(status)) {
        goto error;
    }

    status = sixel_encoder_setopt(encoder,
                                  SIXEL_OPTFLAG_PIPE_MODE,
                                  "force");
    if (SIXEL_FAILED(status)) {
        goto error;
    }

    nret = EXIT_SUCCESS;

error:
    sixel_encoder_unref(encoder);
    return nret;
}


static int
test5(void)
{
    int nret = EXIT_FAILURE;
    sixel_encoder_t *encoder = NULL;
    sixel_allocator_t *allocator = NULL;
    SIXELSTATUS status;

    status = sixel_allocator_new(&allocator, NULL, NULL, NULL, NULL);
    if (SIXEL_FAILED(status)) {
        goto error;
    }

    status = sixel_encoder_new(&encoder, allocator);
    if (SIXEL_FAILED(status)) {
        goto error;
    }

    sixel_encoder_ref(encoder);
    sixel_encoder_unref(encoder);
    nret = EXIT_SUCCESS;

error:
    sixel_encoder_unref(encoder);
    return nret;
}


SIXELAPI int
sixel_encoder_tests_main(void)
{
    int nret = EXIT_FAILURE;
    size_t i;
    typedef int (* testcase)(void);

    static testcase const testcases[] = {
        test1,
        test2,
        test3,
        test4,
        test5
    };

    for (i = 0; i < sizeof(testcases) / sizeof(testcase); ++i) {
        nret = testcases[i]();
        if (nret != EXIT_SUCCESS) {
            goto error;
        }
    }

    nret = EXIT_SUCCESS;

error:
    return nret;
}
#endif  /* HAVE_TESTS */


/* emacs Local Variables:      */
/* emacs mode: c               */
/* emacs tab-width: 4          */
/* emacs indent-tabs-mode: nil */
/* emacs c-basic-offset: 4     */
/* emacs End:                  */
/* vim: set expandtab ts=4 : */
/* EOF */
