/*Copyright (C) 2018 Javier Serrano Polo <javier@jasp.net>
  You can redistribute this library and/or modify it under the terms of the
   GNU Lesser General Public License as published by the Free Software
   Foundation; either version 2.1 of the License, or (at your option) any later
   version.*/
#include "config.h"

#include "sqcode.h"

#include <stdbool.h>
#include <stdlib.h>

#include "image.h"
#include "img_scanner.h"

typedef enum
{
    SHAPE_DOT,
    SHAPE_CORNER,
    SHAPE_OTHER,
    SHAPE_VOID
} shape_t;

typedef struct {
    float x;
    float y;
} sq_point;

typedef struct {
    shape_t type;
    unsigned x0;
    unsigned y0;
    unsigned width;
    unsigned height;
    sq_point center;
} sq_dot;

struct sq_reader {
    bool enabled;
};

/*Initializes a client reader handle.*/
static void sq_reader_init(sq_reader *reader)
{
    reader->enabled = true;
}

/*Allocates a client reader handle.*/
sq_reader *_zbar_sq_create(void)
{
    sq_reader *reader = malloc(sizeof(sq_reader));
    if (reader)
	sq_reader_init(reader);
    return reader;
}

/*Frees a client reader handle.*/
void _zbar_sq_destroy(sq_reader *reader)
{
    free(reader);
}

/* reset finder state between scans */
void _zbar_sq_reset(sq_reader *reader)
{
    reader->enabled = true;
}

int _zbar_sq_new_config(sq_reader *reader, unsigned config)
{
    reader->enabled = config;
    return 0;
}

static const char base64_table[] = {
    'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
    'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
    'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
    'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'
};

static char *base64_encode_buffer(const char *s, size_t size)
{
    char *e;
    size_t encoded_size = (size + 2) / 3 * 4 + 1;
    char *encoded	= malloc(encoded_size);
    if (!encoded)
	return NULL;
    e = encoded;
    for (;;) {
	unsigned char c = (*s >> 2) & 0x3f;
	*e++		= base64_table[c];
	c		= (*s++ << 4) & 0x30;
	if (!--size) {
	    *e++ = base64_table[c];
	    *e++ = '=';
	    *e++ = '=';
	    break;
	}
	c |= (*s >> 4) & 0x0f;
	*e++ = base64_table[c];
	c    = (*s++ << 2) & 0x3c;
	if (!--size) {
	    *e++ = base64_table[c];
	    *e++ = '=';
	    break;
	}
	c |= (*s >> 6) & 0x03;
	*e++ = base64_table[c];
	c    = *s++ & 0x3f;
	*e++ = base64_table[c];
	if (!--size)
	    break;
    }
    *e = '\0';
    return encoded;
}

static bool sq_extract_text(zbar_image_scanner_t *iscn, const char *buf,
			    size_t len)
{
    size_t b64_len;
    zbar_symbol_t *sym = _zbar_image_scanner_alloc_sym(iscn, ZBAR_SQCODE, 0);
    sym->data	       = base64_encode_buffer(buf, len);
    if (!sym->data) {
	_zbar_image_scanner_recycle_syms(iscn, sym);
	return true;
    }
    b64_len	    = (len + 2) / 3 * 4;
    sym->data_alloc = b64_len + 1;
    sym->datalen    = b64_len;
    _zbar_image_scanner_add_sym(iscn, sym);
    return false;
}

static bool is_black_color(const unsigned char c)
{
    return c <= 0x7f;
}

static bool is_black(zbar_image_t *img, int x, int y)
{
    const unsigned char *data;
    if (x < 0 || (unsigned)x >= img->width || y < 0 ||
	(unsigned)y >= img->height)
	return false;
    data = img->data;
    return is_black_color(data[y * img->width + x]);
}

static void set_dot_center(sq_dot *dot, float x, float y)
{
    dot->center.x = x;
    dot->center.y = y;
}

static void sq_scan_shape(zbar_image_t *img, sq_dot *dot, int start_x,
			  int start_y)
{
    int x, y;
    unsigned x0, y0, width, height, x_sum, y_sum, total_weight;
    const unsigned char *data;
    if (!is_black(img, start_x, start_y)) {
	dot->type   = SHAPE_VOID;
	dot->x0	    = start_x;
	dot->y0	    = start_y;
	dot->width  = 0;
	dot->height = 0;
	set_dot_center(dot, start_x, start_y);
	return;
    }

    x0	   = start_x;
    y0	   = start_y;
    width  = 1;
    height = 1;

new_point:
    for (x = x0 - 1; x < (int)(x0 + width + 1); x++) {
	if (is_black(img, x, y0 - 1)) {
	    y0 = y0 - 1;
	    height++;
	    goto new_point;
	}
	if (is_black(img, x, y0 + height)) {
	    height++;
	    goto new_point;
	}
    }
    for (y = y0; y < (int)(y0 + height); y++) {
	if (is_black(img, x0 - 1, y)) {
	    x0 = x0 - 1;
	    width++;
	    goto new_point;
	}
	if (is_black(img, x0 + width, y)) {
	    width++;
	    goto new_point;
	}
    }

    dot->x0	= x0;
    dot->y0	= y0;
    dot->width	= width;
    dot->height = height;

    /* Is it a corner? */
    if (is_black(img, x0 + 0.25 * width, y0 + 0.25 * height) &&
	!is_black(img, x0 + 0.75 * width, y0 + 0.25 * height) &&
	!is_black(img, x0 + 0.25 * width, y0 + 0.75 * height) &&
	is_black(img, x0 + 0.75 * width, y0 + 0.75 * height)) {
	dot->type = SHAPE_CORNER;
	set_dot_center(dot, x0 + 0.5 * width, y0 + 0.5 * height);
	return;
    }

    /* Set dot center */
    data	 = img->data;
    x_sum	 = 0;
    y_sum	 = 0;
    total_weight = 0;
    for (y = y0; y < (int)(y0 + height); y++) {
	int x;
	for (x = x0; x < (int)(x0 + width); x++) {
	    unsigned char weight;
	    if (!is_black(img, x, y))
		continue;
	    weight = 0xff - data[y * img->width + x];
	    x_sum += weight * x;
	    y_sum += weight * y;
	    total_weight += weight;
	}
    }
    dot->type = SHAPE_DOT;
    set_dot_center(dot, x_sum / (float)total_weight + 0.5,
		   y_sum / (float)total_weight + 0.5);

    /* TODO: Is it other shape? White hole? Really a dot? */
}

static void set_middle_point(sq_point *middle, const sq_point *start,
			     const sq_point *end)
{
    middle->x = (start->x + end->x) / 2;
    middle->y = (start->y + end->y) / 2;
}

bool find_left_dot(zbar_image_t *img, sq_dot *dot, unsigned *found_x,
		   unsigned *found_y)
{
    int y, x;
    for (y = dot->y0; y < (int)(dot->y0 + dot->height); y++) {
	for (x = dot->x0 - 1; x >= (int)(dot->x0 - 2 * dot->width); x--) {
	    if (is_black(img, x, y)) {
		*found_x = x;
		*found_y = y;
		return true;
	    }
	}
    }
    return false;
}

bool find_right_dot(zbar_image_t *img, sq_dot *dot, unsigned *found_x,
		    unsigned *found_y)
{
    int y, x;
    for (y = dot->y0; y < (int)(dot->y0 + dot->height); y++) {
	for (x = dot->x0 + dot->width; x < (int)(dot->x0 + 3 * dot->width);
	     x++) {
	    if (is_black(img, x, y)) {
		*found_x = x;
		*found_y = y;
		return true;
	    }
	}
    }
    return false;
}

bool find_bottom_dot(zbar_image_t *img, sq_dot *dot, unsigned *found_x,
		     unsigned *found_y)
{
    int x, y;

    for (x = dot->x0 + dot->width - 1; x >= (int)dot->x0; x--) {
	for (y = dot->y0 + dot->height; y < (int)(dot->y0 + 3 * dot->height);
	     y++) {
	    if (is_black(img, x, y)) {
		*found_x = x;
		*found_y = y;
		return true;
	    }
	}
    }
    return false;
}

int _zbar_sq_decode(sq_reader *reader, zbar_image_scanner_t *iscn,
		    zbar_image_t *img)
{
    unsigned scan_y, scan_x, y;
    sq_dot start_dot, top_left_dot, top_right_dot, bottom_left_dot,
	bottom_right_dot, bottom_left2_dot;
    bool start_corner, error;
    sq_point *top_border, *left_border, *right_border, *bottom_border;
    size_t border_len, cur_len, offset, bit_side_len, bit_len, byte_len, idx;
    float inc_x, inc_y;
    void *ptr;
    char *buf;

    if (!reader->enabled)
	return 0;

    if (img->format != fourcc('Y', '8', '0', '0')) {
	fputs("Unexpected image format\n", stderr);
	return 1;
    }

    /* Starting pixel */
    for (scan_y = 0; scan_y < img->height; scan_y++) {
	for (scan_x = 0; scan_x < img->width; scan_x++) {
	    if (is_black(img, scan_x, scan_y))
		goto found_start;
	}
    }
    return 1;

found_start:;
    /* Starting dot */
    sq_scan_shape(img, &start_dot, scan_x, scan_y);

    start_corner = start_dot.type == SHAPE_CORNER;

    error = true;

    top_border	  = NULL;
    left_border	  = NULL;
    right_border  = NULL;
    bottom_border = NULL;

    if (start_corner) {
	border_len = 0;
    } else {
	border_len = 1;
	top_border = malloc(sizeof(sq_point));
	if (!top_border)
	    return 1;
	top_border[0] = start_dot.center;
    }

    top_left_dot = start_dot;
    while (find_left_dot(img, &top_left_dot, &scan_x, &scan_y)) {
	sq_scan_shape(img, &top_left_dot, scan_x, scan_y);
	if (top_left_dot.type != SHAPE_DOT)
	    goto free_borders;
	if (border_len) {
	    void *ptr;
	    size_t i;
	    border_len += 2;
	    ptr = realloc(top_border, border_len * sizeof(sq_point));
	    if (!ptr)
		goto free_borders;
	    top_border = ptr;
	    for (i = border_len - 1; i >= 2; i--)
		top_border[i] = top_border[i - 2];
	    top_border[0] = top_left_dot.center;
	    set_middle_point(&top_border[1], &top_border[0], &top_border[2]);
	} else {
	    border_len = 1;
	    top_border = malloc(sizeof(sq_point));
	    if (!top_border)
		return 1;
	    top_border[0] = top_left_dot.center;
	}
    }
    if (top_left_dot.type != SHAPE_DOT)
	goto free_borders;

    top_right_dot = start_dot;
    if (!start_corner) {
	while (find_right_dot(img, &top_right_dot, &scan_x, &scan_y)) {
	    void *ptr;
	    sq_scan_shape(img, &top_right_dot, scan_x, scan_y);
	    if (top_right_dot.type == SHAPE_CORNER)
		break;
	    if (top_right_dot.type != SHAPE_DOT)
		goto free_borders;
	    border_len += 2;
	    ptr = realloc(top_border, border_len * sizeof(sq_point));
	    if (!ptr)
		goto free_borders;
	    top_border		       = ptr;
	    top_border[border_len - 1] = top_right_dot.center;
	    set_middle_point(&top_border[border_len - 2],
			     &top_border[border_len - 3],
			     &top_border[border_len - 1]);
	}
    }
    if (border_len < 3)
	goto free_borders;
    inc_x = top_border[border_len - 1].x - top_border[border_len - 3].x;
    inc_y = top_border[border_len - 1].y - top_border[border_len - 3].y;
    border_len += 3;
    ptr = realloc(top_border, border_len * sizeof(sq_point));
    if (!ptr)
	goto free_borders;
    top_border			 = ptr;
    top_border[border_len - 3].x = top_border[border_len - 4].x + 0.5 * inc_x;
    top_border[border_len - 3].y = top_border[border_len - 4].y + 0.5 * inc_y;
    top_border[border_len - 2].x = top_border[border_len - 4].x + inc_x;
    top_border[border_len - 2].y = top_border[border_len - 4].y + inc_y;
    top_border[border_len - 1].x = top_border[border_len - 4].x + 1.5 * inc_x;
    top_border[border_len - 1].y = top_border[border_len - 4].y + 1.5 * inc_y;

    left_border = malloc(border_len * sizeof(sq_point));
    if (!left_border)
	goto free_borders;
    left_border[0] = top_border[0];

    bottom_left_dot = top_left_dot;
    cur_len	    = 1;
    while (find_bottom_dot(img, &bottom_left_dot, &scan_x, &scan_y)) {
	sq_scan_shape(img, &bottom_left_dot, scan_x, scan_y);
	if (bottom_left_dot.type == SHAPE_CORNER)
	    break;
	if (bottom_left_dot.type != SHAPE_DOT)
	    goto free_borders;
	cur_len += 2;
	if (cur_len > border_len)
	    goto free_borders;
	left_border[cur_len - 1] = bottom_left_dot.center;
	set_middle_point(&left_border[cur_len - 2], &left_border[cur_len - 3],
			 &left_border[cur_len - 1]);
    }
    if (cur_len != border_len - 3 || bottom_left_dot.type != SHAPE_CORNER)
	goto free_borders;
    inc_x = left_border[cur_len - 1].x - left_border[cur_len - 3].x;
    inc_y = left_border[cur_len - 1].y - left_border[cur_len - 3].y;
    left_border[border_len - 3].x = left_border[border_len - 4].x + 0.5 * inc_x;
    left_border[border_len - 3].y = left_border[border_len - 4].y + 0.5 * inc_y;
    left_border[border_len - 2].x = left_border[border_len - 4].x + inc_x;
    left_border[border_len - 2].y = left_border[border_len - 4].y + inc_y;
    left_border[border_len - 1].x = left_border[border_len - 4].x + 1.5 * inc_x;
    left_border[border_len - 1].y = left_border[border_len - 4].y + 1.5 * inc_y;

    right_border = malloc(border_len * sizeof(sq_point));
    if (!right_border)
	goto free_borders;

    bottom_right_dot = top_right_dot;
    cur_len	     = 3;
    while (find_bottom_dot(img, &bottom_right_dot, &scan_x, &scan_y)) {
	sq_scan_shape(img, &bottom_right_dot, scan_x, scan_y);
	if (bottom_right_dot.type != SHAPE_DOT)
	    goto free_borders;
	if (cur_len == 3) {
	    cur_len++;
	    if (cur_len > border_len)
		goto free_borders;
	    right_border[cur_len - 1] = bottom_right_dot.center;
	} else {
	    cur_len += 2;
	    if (cur_len > border_len)
		goto free_borders;
	    right_border[cur_len - 1] = bottom_right_dot.center;
	    set_middle_point(&right_border[cur_len - 2],
			     &right_border[cur_len - 3],
			     &right_border[cur_len - 1]);
	}
    }
    if (cur_len != border_len || border_len < 6)
	return 1;
    inc_x	      = right_border[5].x - right_border[3].x;
    inc_y	      = right_border[5].y - right_border[3].y;
    right_border[2].x = right_border[3].x - 0.5 * inc_x;
    right_border[2].y = right_border[3].y - 0.5 * inc_y;
    right_border[1].x = right_border[3].x - inc_x;
    right_border[1].y = right_border[3].y - inc_y;
    right_border[0].x = right_border[3].x - 1.5 * inc_x;
    right_border[0].y = right_border[3].y - 1.5 * inc_y;

    bottom_border = malloc(border_len * sizeof(sq_point));
    if (!bottom_border)
	goto free_borders;
    bottom_border[border_len - 1] = right_border[border_len - 1];

    bottom_left2_dot = bottom_right_dot;
    offset	     = border_len - 1;
    while (find_left_dot(img, &bottom_left2_dot, &scan_x, &scan_y)) {
	sq_scan_shape(img, &bottom_left2_dot, scan_x, scan_y);
	if (bottom_left2_dot.type == SHAPE_CORNER)
	    break;
	if (bottom_left2_dot.type != SHAPE_DOT)
	    goto free_borders;
	if (offset < 2)
	    goto free_borders;
	offset -= 2;
	bottom_border[offset] = bottom_left2_dot.center;
	set_middle_point(&bottom_border[offset + 1], &bottom_border[offset],
			 &bottom_border[offset + 2]);
    }
    if (offset != 3 || bottom_left2_dot.type != SHAPE_CORNER)
	goto free_borders;
    inc_x	       = bottom_border[5].x - bottom_border[3].x;
    inc_y	       = bottom_border[5].y - bottom_border[3].y;
    bottom_border[2].x = bottom_border[3].x - 0.5 * inc_x;
    bottom_border[2].y = bottom_border[3].y - 0.5 * inc_y;
    bottom_border[1].x = bottom_border[3].x - inc_x;
    bottom_border[1].y = bottom_border[3].y - inc_y;
    bottom_border[0].x = bottom_border[3].x - 1.5 * inc_x;
    bottom_border[0].y = bottom_border[3].y - 1.5 * inc_y;

    /* Size check */
    if (border_len < 8 + 2 * (1 + 2) || border_len > 65535)
	goto free_borders;
    bit_side_len = border_len - 2 * (1 + 2);

    bit_len = bit_side_len * bit_side_len;
    if (bit_len % 8)
	goto free_borders;
    byte_len = bit_len / 8;
    buf	     = calloc(byte_len, sizeof(char));
    if (!buf)
	goto free_borders;

    idx = 0;
    for (y = 3; y <= border_len - 4; y++) {
	unsigned x;
	for (x = 3; x <= border_len - 4; x++) {
	    unsigned char bottom_right_color, mixed_color;
	    float bottom_weight = y / (float)(border_len - 1);
	    float top_weight	= 1 - bottom_weight;
	    float right_weight	= x / (float)(border_len - 1);
	    float left_weight	= 1 - right_weight;

	    sq_point top_left_source = {
		top_border[x].x + left_border[y].x - left_border[0].x,
		top_border[x].y + left_border[y].y - left_border[0].y
	    };

	    sq_point bottom_right_source = {
		bottom_border[x].x + right_border[y].x -
		    right_border[border_len - 1].x,
		bottom_border[x].y + right_border[y].y -
		    right_border[border_len - 1].y
	    };

	    const unsigned char *data = img->data;
	    unsigned sample_x	      = top_left_source.x;
	    unsigned sample_y	      = top_left_source.y;
	    unsigned char top_left_color =
		data[sample_y * img->width + sample_x];
	    sample_x	       = bottom_right_source.x;
	    sample_y	       = bottom_right_source.y;
	    bottom_right_color = data[sample_y * img->width + sample_x];

	    mixed_color =
		((top_weight + left_weight) * top_left_color +
		 (bottom_weight + right_weight) * bottom_right_color) /
		2;

	    if (is_black_color(mixed_color))
		buf[idx / 8] |= 1 << 7 - idx % 8;
	    idx++;
	}
    }
    error = sq_extract_text(iscn, buf, byte_len);
    free(buf);

free_borders:
    free(top_border);
    free(left_border);
    free(right_border);
    free(bottom_border);
    return error ? 1 : 0;
}
