/*
 * Copyright (C) 2009 Behdad Esfahbod
 *
 * This library is free software; you can redistribute it 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.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this library, in a file named COPYING; if not, write to the
 * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301, USA
 */

#include <fribidi.h>

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

#define TRUE 1
#define FALSE 0

/* Glib array types */
typedef struct {
  int capacity;
  int len;
  int *data;
} int_array_t;

typedef struct {
  int capacity;
  int len;
  char *data;
} char_array_t;

#define LINE_SIZE 2048 /* Size of largest example line in BidiTest */
#define ARRAY_CHUNK_SIZE 32
int_array_t *new_int_array()
{
  int_array_t *arr = (int_array_t*)malloc(sizeof(int_array_t));
  arr->len = 0;
  arr->capacity = ARRAY_CHUNK_SIZE;
  arr->data = (int*)malloc(arr->capacity * sizeof(int));

  return arr;
}

void int_array_add(int_array_t *arr, int val)
{
  if (arr->len == arr->capacity)
    {
      arr->capacity += ARRAY_CHUNK_SIZE;
      arr->data = (int*)realloc(arr->data, arr->capacity*sizeof(int));
    }
  arr->data[arr->len++] = val;
}

int *int_array_free(int_array_t *arr, int free_data)
{
  int *data = arr->data;
  if (free_data) {
    data = NULL;
    free(arr->data);
  }
  free(arr);
  return data;
}

char_array_t *new_char_array()
{
  char_array_t *arr = (char_array_t*)malloc(sizeof(char_array_t));
  arr->len = 0;
  arr->capacity = ARRAY_CHUNK_SIZE;
  arr->data = (char*)malloc(arr->capacity);

  return arr;
}

void char_array_add(char_array_t *arr, char val)
{
  if (arr->len == arr->capacity)
    {
      arr->capacity += ARRAY_CHUNK_SIZE;
      arr->data = (char*)realloc(arr->data, arr->capacity * sizeof(char));
    }
  arr->data[arr->len++] = val;
}

char *char_array_free(char_array_t *arr, int free_data)
{
  char *data = arr->data;
  if (free_data) {
    data = NULL;
    free(arr->data);
  }
  free(arr);
  return data;
}

static void die(const char *fmt, ...)
{
    va_list ap;
    va_start(ap,fmt); 
    
    vfprintf(stderr, fmt, ap);
    exit(-1);
}

static FriBidiCharType
parse_char_type (const char *s, int len)
{
#define MATCH(name, value) \
    if (!strncmp (name, s, len) && name[len] == '\0') return value;

    MATCH ("L",   FRIBIDI_TYPE_LTR);
    MATCH ("R",   FRIBIDI_TYPE_RTL);
    MATCH ("AL",  FRIBIDI_TYPE_AL);
    MATCH ("EN",  FRIBIDI_TYPE_EN);
    MATCH ("AN",  FRIBIDI_TYPE_AN);
    MATCH ("ES",  FRIBIDI_TYPE_ES);
    MATCH ("ET",  FRIBIDI_TYPE_ET);
    MATCH ("CS",  FRIBIDI_TYPE_CS);
    MATCH ("NSM", FRIBIDI_TYPE_NSM);
    MATCH ("BN",  FRIBIDI_TYPE_BN);
    MATCH ("B",   FRIBIDI_TYPE_BS);
    MATCH ("S",   FRIBIDI_TYPE_SS);
    MATCH ("WS",  FRIBIDI_TYPE_WS);
    MATCH ("ON",  FRIBIDI_TYPE_ON);
    MATCH ("LRE", FRIBIDI_TYPE_LRE);
    MATCH ("RLE", FRIBIDI_TYPE_RLE);
    MATCH ("LRO", FRIBIDI_TYPE_LRO);
    MATCH ("RLO", FRIBIDI_TYPE_RLO);
    MATCH ("PDF", FRIBIDI_TYPE_PDF);
    MATCH ("LRI", FRIBIDI_TYPE_LRI);
    MATCH ("RLI", FRIBIDI_TYPE_RLI);
    MATCH ("FSI", FRIBIDI_TYPE_FSI);
    MATCH ("PDI", FRIBIDI_TYPE_PDI);

    die("Oops. I shouldn't reach here!\n");
    return -1;
}

static FriBidiLevel *
parse_levels_line (const char *line,
		   FriBidiLevel *len)
{
    char_array_t *levels;

    if (!strncmp (line, "@Levels:", 8))
	line += 8;

    levels = new_char_array ();

    while (*line)
    {
	FriBidiLevel l;
	char *end;

	errno = 0;
	l = strtol (line, &end, 10);
	if (errno != EINVAL && line != end)
	{
	  char_array_add (levels, l);
	  line = end;
	  continue;
	}

	while (isspace (*line))
	  line++;

	if (*line == 'x')
	{
	  char_array_add (levels, -1);
	  line++;
	  continue;
	}

	if (!*line)
	  break;

	die("Oops. I shouldn't be here!\n");
    }

    *len = levels->len;
    return (FriBidiLevel *) char_array_free(levels, FALSE);
}

static FriBidiStrIndex *
parse_reorder_line (const char *line,
		    FriBidiStrIndex *len)
{
    int_array_t *map;
    FriBidiStrIndex l;
    char *end;

    if (!strncmp (line, "@Reorder:", 9))
	line += 9;

    map = new_int_array ();

    for(; errno = 0, l = strtol (line, &end, 10), line != end && errno != EINVAL; line = end) {
	int_array_add (map, l);
    }

    *len = map->len;
    return (FriBidiStrIndex *) int_array_free (map, FALSE);
}

static FriBidiCharType *
parse_test_line (const char *line,
	         FriBidiStrIndex *len,
		 int *base_dir_flags)
{
    int_array_t *types;
    FriBidiCharType c;
    const char *end;

    types = new_int_array();

    for(;;) {
	while (isspace (*line))
	    line++;
	end = line;
	while (isalpha (*end))
	    end++;
	if (line == end)
	    break;

	c = parse_char_type (line, end - line);
	int_array_add (types, c);

	line = end;
    }

    if (*line == ';')
	line++;
    *base_dir_flags = strtol (line, NULL, 10);

    *len = types->len;
    return (FriBidiCharType *) int_array_free (types, FALSE);
}

int
main (int argc, char **argv)
{
    FILE *channel;
    char line[LINE_SIZE];
    FriBidiStrIndex *expected_ltor = NULL;
    FriBidiStrIndex expected_ltor_len = 0;
    FriBidiStrIndex *ltor = NULL;
    FriBidiStrIndex ltor_len = 0;
    FriBidiCharType *types = NULL;
    FriBidiStrIndex types_len = 0;
    FriBidiLevel *expected_levels = NULL;
    FriBidiLevel expected_levels_len = 0;
    FriBidiLevel *levels = NULL;
    FriBidiStrIndex levels_len = 0;
    int base_dir_flags, base_dir_mode;
    int numerrs = 0;
    int numtests = 0;
    int line_no = 0;
    int debug = FALSE;
    const char *filename;
    int next_arg;

    if (argc < 2) 
	die ("usage: %s [--debug] test-file-name\n", argv[0]);

    next_arg = 1;
    if (!strcmp (argv[next_arg], "--debug")) {
	debug = TRUE;
	next_arg++;
    }

    filename = argv[next_arg++];

    channel = fopen(filename, "r");
    if (!channel) 
	die ("Failed opening %s\n", filename);

    while (!feof(channel)) {
        int len;
        fgets(line, LINE_SIZE, channel);
        len = strlen(line);
        if (len == LINE_SIZE-1)
          die("LINE_SIZE too small at line %d!\n", line_no);

	line_no++;

	if (line[0] == '#')
	    continue;

	if (line[0] == '@')
	{
	    if (!strncmp (line, "@Reorder:", 9)) {
		free (expected_ltor);
		expected_ltor = parse_reorder_line (line, &expected_ltor_len);
		continue;
	    }
	    if (!strncmp (line, "@Levels:", 8)) {
		free (expected_levels);
		expected_levels = parse_levels_line (line, &expected_levels_len);
		continue;
	    }
	    continue;
	}

	/* Test line */
	free (types);
	types = parse_test_line (line, &types_len, &base_dir_flags);

	free (levels);
	levels = malloc (sizeof (FriBidiLevel) * types_len);
	levels_len = types_len;

	free (ltor);
	ltor = malloc (sizeof (FriBidiStrIndex) * types_len);

	/* Test it */
	for (base_dir_mode = 0; base_dir_mode < 3; base_dir_mode++) {
	    FriBidiParType base_dir;
	    int i, j;
	    int matches;

	    if ((base_dir_flags & (1<<base_dir_mode)) == 0)
		continue;

            numtests++;

	    switch (base_dir_mode) {
	    case 0: base_dir = FRIBIDI_PAR_ON;  break;
	    case 1: base_dir = FRIBIDI_PAR_LTR; break;
	    case 2: base_dir = FRIBIDI_PAR_RTL; break;
	    }

	    if (fribidi_get_par_embedding_levels_ex (types,
                                                     NULL, /* Brackets are not used in the BidiTest.txt file */
                                                     types_len,
                                                     &base_dir,
                                                     levels))
            {}

	    for (i = 0; i < types_len; i++)
	        ltor[i] = i;

	    if (fribidi_reorder_line (0 /*FRIBIDI_FLAG_REORDER_NSM*/,
                                      types, types_len,
                                      0, base_dir,
                                      levels,
                                      NULL,
                                      ltor))
            {}

	    j = 0;
	    for (i = 0; i < types_len; i++)
	    	if (!FRIBIDI_IS_EXPLICIT_OR_BN (types[ltor[i]]))
		    ltor[j++] = ltor[i];
	    ltor_len = j;

	    /* Compare */
	    matches = TRUE;
	    if (levels_len != expected_levels_len)
		matches = FALSE;
	    if (matches)
		for (i = 0; i < levels_len; i++)
		    if (levels[i] != expected_levels[i] &&
			expected_levels[i] != (FriBidiLevel) -1) {
			matches = FALSE;
			break;
		    }

	    if (ltor_len != expected_ltor_len)
		matches = FALSE;
	    if (matches)
		for (i = 0; i < ltor_len; i++)
		    if (ltor[i] != expected_ltor[i]) {
			matches = FALSE;
			break;
		    }

	    if (!matches)
	    {
		numerrs++;

		fprintf (stderr, "failure on line %d\n", line_no);
		fprintf (stderr, "input is: %s\n", line);
		fprintf (stderr, "base dir: %s\n",
                         base_dir_mode==0 ? "auto"
                         : base_dir_mode==1 ? "LTR" : "RTL");

		fprintf (stderr, "expected levels:");
		for (i = 0; i < expected_levels_len; i++)
		    if (expected_levels[i] == (FriBidiLevel) -1)
                        fprintf (stderr," x");
		    else
                        fprintf (stderr, " %d", expected_levels[i]);
		fprintf (stderr, "\n");
		fprintf (stderr, "returned levels:");
		for (i = 0; i < levels_len; i++)
                    fprintf (stderr, " %d", levels[i]);
		fprintf (stderr, "\n");

		fprintf (stderr, "expected order:");
		for (i = 0; i < expected_ltor_len; i++)
                    fprintf (stderr, " %d", expected_ltor[i]);
		fprintf (stderr, "\n");
		fprintf (stderr, "returned order:");
		for (i = 0; i < ltor_len; i++)
                    fprintf (stderr, " %d", ltor[i]);
		fprintf (stderr, "\n");

		if (debug)
                  {
		    FriBidiParType base_dir;

		    fribidi_set_debug (1);

		    switch (base_dir_mode) {
		    case 0: base_dir = FRIBIDI_PAR_ON;  break;
		    case 1: base_dir = FRIBIDI_PAR_LTR; break;
		    case 2: base_dir = FRIBIDI_PAR_RTL; break;
		    }

		    if (fribidi_get_par_embedding_levels_ex (types,
                                                             NULL, /* No bracket types */
                                                             types_len,
                                                             &base_dir,
                                                             levels))
                    {}

		    fribidi_set_debug (0);
		}

		fprintf (stderr, "\n");
	    }
	}
    }

    free (ltor);
    free (levels);
    free (expected_ltor);
    free (expected_levels);
    free (types);
    fclose(channel);

    if (numerrs)
        fprintf (stderr, "%d errors out of %d total tests\n", numerrs, numtests);
    else
        printf("No errors found! :-)\n");

    return numerrs;
}
