/** Terminal sixel routines.
 * @file */

/*
 * Copyright (c) 2021 libsixel developers. See `AUTHORS`.
 * Copyright (c) 2014-2019 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.
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

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

#include "elinks.h"

#include "document/document.h"
#include "osdep/osdep.h"
#include "terminal/hardio.h"
#include "terminal/screen.h"
#include "terminal/sixel.h"
#include "terminal/terminal.h"
#include "util/memcount.h"

/* encode settings object */
struct sixel_decoder {
	unsigned int ref;
	char *input;
	char *output;
	sixel_allocator_t *allocator;
};

/* encoder object */
struct sixel_encoder {
    unsigned int ref;               /* reference counter */
    sixel_allocator_t *allocator;   /* allocator object */
    int reqcolors;
    int color_option;
    char *mapfile;
    int builtin_palette;
    int method_for_diffuse;
    int method_for_largest;
    int method_for_rep;
    int quality_mode;
    int method_for_resampling;
    int loop_mode;
    int palette_type;
    int f8bit;
    int finvert;
    int fuse_macro;
    int fignore_delay;
    int complexion;
    int fstatic;
    int pixelwidth;
    int pixelheight;
    int percentwidth;
    int percentheight;
    int clipx;
    int clipy;
    int clipwidth;
    int clipheight;
    int clipfirst;
    int macro_number;
    int penetrate_multiplexer;
    int encode_policy;
    int ormode;
    int pipe_mode;
    int verbose;
    int has_gri_arg_limit;
    unsigned char *bgcolor;
    int outfd;
    int finsecure;
    int *cancel_flag;
    void *dither_cache;
};

#ifdef CONFIG_DEBUG
static sixel_allocator_t *el_sixel_allocator;

static void
init_allocator(void)
{
	static int initialized = 0;
	if (!initialized) {
		sixel_allocator_new(&el_sixel_allocator, el_sixel_malloc, el_sixel_calloc, el_sixel_realloc, el_sixel_free);
		initialized = 1;
	}
}
#endif

/* palette type */
#define SIXEL_COLOR_OPTION_DEFAULT          0   /* use default settings */
#define SIXEL_COLOR_OPTION_MONOCHROME       1   /* use monochrome palette */
#define SIXEL_COLOR_OPTION_BUILTIN          2   /* use builtin palette */
#define SIXEL_COLOR_OPTION_MAPFILE          3   /* use mapfile option */
#define SIXEL_COLOR_OPTION_HIGHCOLOR        4   /* use highcolor option */


static int
sixel_write_callback(char *data, int size, void *priv)
{
	struct string *text = priv;

	add_bytes_to_string(text, data, size);
	return size;
}

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;
    enum { message_buffer_size = 256 };
    char message[message_buffer_size];
    int nwrite;
    int dulation;
    int delay;
    int lag = 0;
    struct timespec tv;
    clock_t start;
    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;
    }
    start = clock();
    delay = sixel_frame_get_delay(frame);
    if (delay > 0 && !encoder->fignore_delay) {
        dulation = (int)((clock() - start) * 1000 * 1000 / CLOCKS_PER_SEC) - (int)lag;
        lag = 0;
        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);
        }
    }

    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;
    enum { message_buffer_size = 256 };
    char buffer[message_buffer_size];
    int nwrite;
    int dulation;
    int lag = 0;
    struct timespec tv;
    clock_t start;
    unsigned char *pixbuf;
    int width;
    int height;
    int delay;

    start = clock();
    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;
        }
        delay = sixel_frame_get_delay(frame);
        if (delay > 0 && !encoder->fignore_delay) {
            dulation = (int)((clock() - start) * 1000 * 1000 / CLOCKS_PER_SEC) - (int)lag;
            lag = 0;
            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);
            }
        }
    }

end:
    return status;
}


/* 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;
}

#if 0
/* 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 */
                                          SIXEL_PALETTE_MAX, /* 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;
}
#endif

/* 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 0
        if (encoder->dither_cache) {
            *dither = encoder->dither_cache;
            status = SIXEL_OK;
        } else {
            status = sixel_prepare_specified_palette(dither, encoder);
        }
#endif
        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)) {
        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;
}


/* from libsixel-1.10.3 */
/* 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;
}

/* from libsixel-1.10.3 */
/* 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;
}


/* from libsixel-1.10.3 */
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);
    }

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

    /* 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 {
#if 0
        /* 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;
        }
#endif
    }

    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);
    sixel_output_set_ormode(output, encoder->ormode);

#if 0
    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);
    }
#endif

    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 0
    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;
    }
#endif

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

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

    return status;
}


void
try_to_draw_images(struct terminal *term)
{
	struct image *im;

	if (!term->sixel) {
		return;
	}

	foreach (im, term->images) {
		struct string text;

		if (!init_string(&text)) {
			return;
		}
		add_cursor_move_to_string(&text, im->y + 1, im->x + 1);
		add_string_to_string(&text, &im->pixels);

		if (text.length) {
			if (term->master) want_draw();
			hard_write(term->fdout, text.source, text.length);
			if (term->master) done_draw();
		}
		done_string(&text);
	}
}

void
delete_image(struct image *im)
{
	del_from_list(im);
	done_string(&im->pixels);
	mem_free(im);
}

int
add_image_to_document(struct document *doc, struct string *pixels, int lineno)
{
	unsigned char *indexed_pixels = NULL;
	unsigned char *palette = NULL;
	sixel_decoder_t *decoder = NULL;
	sixel_frame_t *frame = NULL;
	int ncolors;
	int width;
	int height;
	int ile = 0;
	struct image *im = mem_calloc(1, sizeof(*im));
	SIXELSTATUS status;

	if (!im) {
		return 0;
	}
	if (!init_string(&im->pixels)) {
		mem_free(im);
		return 0;
	}
#ifdef CONFIG_DEBUG
	init_allocator();
	status = sixel_decoder_new(&decoder, el_sixel_allocator);
#else
	status = sixel_decoder_new(&decoder, NULL);
#endif
	if (SIXEL_FAILED(status)) {
		goto end;
	}
	status = sixel_decode_raw(
		(unsigned char *)pixels->source,
		pixels->length,
		&indexed_pixels,
		&width,
		&height,
		&palette,
		&ncolors,
		decoder->allocator
	);

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

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

	if (SIXEL_FAILED(status)) {
		goto end;
	}
	im->y = lineno + 1;
	im->x = 0;
	im->width = width;
	im->height = height;
	add_string_to_string(&im->pixels, pixels);

	ile = (height + doc->options.cell_height - 1) / doc->options.cell_height;
	add_to_list(doc->images, im);
end:
	sixel_frame_unref(frame);
	sixel_decoder_unref(decoder);

	return ile;
}


struct image *
copy_frame(struct image *src, int box_width, int box_height, int cell_width, int cell_height, int dx, int dy)
{
	sixel_decoder_t *decoder = NULL;
	sixel_encoder_t *encoder = NULL;
	sixel_output_t *output = NULL;
	sixel_frame_t *frame = NULL;
	unsigned char *indexed_pixels = NULL;
	unsigned char *palette = NULL;
	int ncolors;
	int width;
	int height;
	int x;
	int y;
	struct image *dest = mem_calloc(1, sizeof(*dest));
	SIXELSTATUS status;

	if (!dest) {
		return NULL;
	}

	if (!init_string(&dest->pixels)) {
		mem_free(dest);
		return NULL;
	}
#ifdef CONFIG_DEBUG
	init_allocator();
	status = sixel_decoder_new(&decoder, el_sixel_allocator);
#else
	status = sixel_decoder_new(&decoder, NULL);
#endif

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

	status = sixel_decode_raw(
		(unsigned char *)src->pixels.source,
		src->pixels.length,
		&indexed_pixels,
		&width,
		&height,
		&palette,
		&ncolors,
		decoder->allocator
	);

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

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

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

	if (SIXEL_FAILED(status)) {
		goto end;
	}
	x = src->x - dx;
	y = src->y - dy;
	encoder->clipx = x >= 0 ? 0 : (-x * cell_width);
	encoder->clipy = y >= 0 ? 0 : (-y * cell_height);
	encoder->clipwidth = box_width * cell_width;
	encoder->clipheight = box_height * cell_height;
	status = sixel_output_new(&output, sixel_write_callback, &dest->pixels, NULL);

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

	if (SIXEL_FAILED(status)) {
		goto end;
	}
	dest->x = x < 0 ? 0 : x;
	dest->y = y < 0 ? 1 : y;
	dest->width = src->width;
	dest->height = src->height;
end:
	sixel_frame_unref(frame);
	sixel_output_unref(output);
	sixel_decoder_unref(decoder);
	sixel_encoder_unref(encoder);

	return dest;
}
