/**
 * xrdp: A Remote Desktop Protocol server.
 *
 * Copyright (C) Jay Sorg 2004-2012
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#if defined(HAVE_CONFIG_H)
#include <config_ac.h>
#endif

#include <unistd.h>
#include <ctype.h>
#include <ft2build.h>
#include FT_FREETYPE_H

/* See the FT2 documentation - this builds an error table */
#undef FTERRORS_H_
#define FT_ERRORDEF( e, v, s )  { e, s },
#define FT_ERROR_START_LIST     {
#define FT_ERROR_END_LIST       { 0, NULL } };

static const struct
{
    int          err_code;
    const char  *err_msg;
} ft_errors[] =
#include <freetype/fterrors.h>
#ifdef __cppcheck__
    // avoid syntaxError by providing the array contents
    {};
#endif


#include "arch.h"
#include "defines.h"
#include "log.h"
#include "os_calls.h"
#include "string_calls.h"

#include "fv1.h"

#define DEFAULT_POINT_SIZE 10
#define DEFAULT_MAX_CHAR 0x4dff

/**
 * sans10 compatibility choices
 */
enum sans10_compat
{
    S10_OFF = 0,
    S10_ON,
    S10_AUTO
};

/**
 * Parsed program arguments
 */
struct program_args
{
    const char *input_file;
    const char *output_file;
    char font_name[FV1_MAX_FONT_NAME_SIZE + 1];
    unsigned short point_size;
    /** Last character value in file */
    unsigned int max_ucode;
    /** Are we generating san10 in compatibility mode? */
    enum sans10_compat sans10_compatibility;
};

struct x_dimensions
{
    unsigned short width;
    short offset;
    unsigned short inc_by;
};

/**
 * Table of some character settings in the original sans-10.fv1 font
 */
static const struct x_dimensions
    original_sans10_data[] =
{
    /* 0x20 - 0x3f */
    {1, 0, 4}, {1, 2, 5}, {3, 1, 5}, {9, 1, 11},
    {7, 1, 8}, {11, 0, 12}, {9, 1, 11}, {1, 1, 3},
    {3, 1, 5}, {3, 1, 5}, {7, 0, 7}, {9, 1, 11},
    {2, 1, 4}, {3, 1, 5}, {1, 2, 4}, {4, 0, 4},
    {6, 1, 8}, {5, 2, 8}, {6, 1, 8}, {6, 1, 8},
    {6, 1, 8}, {6, 1, 8}, {6, 1, 8}, {6, 1, 8},
    {6, 1, 8}, {6, 1, 8}, {1, 2, 4}, {2, 1, 4},
    {8, 1, 11}, {8, 1, 11}, {8, 1, 11}, {5, 1, 7},
    /* 0x40 - 0x5f */
    {11, 1, 13}, {9, 0, 9}, {7, 1, 9}, {7, 1, 9},
    {8, 1, 10}, {6, 1, 8}, {5, 1, 7}, {8, 1, 10},
    {8, 1, 10}, {1, 1, 3}, {3, -1, 3}, {7, 1, 8},
    {6, 1, 7}, {9, 1, 11}, {8, 1, 10}, {8, 1, 10},
    {6, 1, 8}, {8, 1, 10}, {7, 1, 8}, {7, 1, 9},
    {7, 0, 7}, {8, 1, 10}, {9, 0, 9}, {11, 0, 11},
    {8, 0, 8}, {7, 0, 7}, {8, 1, 10}, {3, 1, 5},
    {4, 0, 4}, {3, 1, 5}, {8, 1, 11}, {7, 0, 7},
    /* 0x60 - 0x7f */
    {3, 1, 7}, {6, 1, 8}, {6, 1, 8}, {5, 1, 7},
    {6, 1, 8}, {6, 1, 8}, {4, 0, 4}, {6, 1, 8},
    {6, 1, 8}, {1, 1, 3}, {2, 0, 3}, {6, 1, 7},
    {1, 1, 3}, {11, 1, 13}, {6, 1, 8}, {6, 1, 8},
    {6, 1, 8}, {6, 1, 8}, {4, 1, 5}, {5, 1, 7},
    {4, 0, 5}, {6, 1, 8}, {7, 0, 7}, {9, 0, 9},
    {7, 0, 7}, {7, 0, 7}, {5, 1, 7}, {5, 2, 8},
    {1, 2, 4}, {5, 2, 8}, {8, 1, 11}, {7, 1, 8},
    /* 0x80 - 0x9f */
    {7, 1, 8}, {7, 1, 8}, {7, 1, 8}, {7, 1, 8},
    {7, 1, 8}, {7, 1, 8}, {7, 1, 8}, {7, 1, 8},
    {7, 1, 8}, {7, 1, 8}, {7, 1, 8}, {7, 1, 8},
    {7, 1, 8}, {7, 1, 8}, {7, 1, 8}, {7, 1, 8},
    {7, 1, 8}, {7, 1, 8}, {7, 1, 8}, {7, 1, 8},
    {7, 1, 8}, {7, 1, 8}, {7, 1, 8}, {7, 1, 8},
    {7, 1, 8}, {7, 1, 8}, {7, 1, 8}, {7, 1, 8},
    {7, 1, 8}, {7, 1, 8}, {7, 1, 8}, {7, 1, 8},
    /* 0xa0 - 0xbf */
    {1, 0, 4}, {1, 2, 5}, {6, 1, 8}, {7, 0, 8},
    {7, 0, 8}, {7, 1, 8}, {1, 2, 4}, {5, 1, 7},
    {3, 2, 7}, {9, 2, 13}, {5, 1, 6}, {6, 1, 8},
    {8, 1, 11}, {3, 1, 5}, {9, 2, 13}, {4, 1, 7},
    {4, 1, 7}, {9, 1, 11}, {4, 1, 5}, {4, 1, 5},
    {3, 2, 7}, {7, 1, 8}, {6, 1, 8}, {1, 1, 4},
    {3, 2, 7}, {3, 1, 5}, {5, 1, 6}, {6, 1, 8},
    {12, 1, 13}, {11, 1, 13}, {12, 1, 13}, {5, 1, 7},
    /* 0xc0 - 0xdf */
    {9, 0, 9}, {9, 0, 9}, {9, 0, 9}, {9, 0, 9},
    {9, 0, 9}, {9, 0, 9}, {12, 0, 13}, {7, 1, 9},
    {6, 1, 8}, {6, 1, 8}, {6, 1, 8}, {6, 1, 8},
    {2, 0, 3}, {2, 1, 3}, {5, -1, 3}, {3, 0, 3},
    {9, 0, 10}, {8, 1, 10}, {8, 1, 10}, {8, 1, 10},
    {8, 1, 10}, {8, 1, 10}, {8, 1, 10}, {7, 2, 11},
    {8, 1, 10}, {8, 1, 10}, {8, 1, 10}, {8, 1, 10},
    {8, 1, 10}, {7, 0, 7}, {6, 1, 8}, {6, 1, 8},
    /* 0xe0 - 0xff */
    {6, 1, 8}, {6, 1, 8}, {6, 1, 8}, {6, 1, 8},
    {6, 1, 8}, {6, 1, 8}, {11, 1, 13}, {5, 1, 7},
    {6, 1, 8}, {6, 1, 8}, {6, 1, 8}, {6, 1, 8},
    {3, 0, 3}, {3, 1, 3}, {5, -1, 3}, {3, 0, 3},
    {6, 1, 8}, {6, 1, 8}, {6, 1, 8}, {6, 1, 8},
    {6, 1, 8}, {6, 1, 8}, {6, 1, 8}, {8, 1, 11},
    {6, 1, 8}, {6, 1, 8}, {6, 1, 8}, {6, 1, 8},
    {6, 1, 8}, {7, 0, 7}, {6, 1, 8}, {7, 0, 7}
};


/**************************************************************************//**
 * Parses a unicode character value
 *
 * @param str String containing value
 * @return Resulting character value
 */
static unsigned int
parse_ucode_name(const char *str)
{
    unsigned int rv;
    if (toupper(str[0]) == 'U' && str[1] == '+')
    {
        char *hex = g_strdup(str);
        hex[0] = '0';
        hex[1] = 'x';
        rv = g_atoix(hex);
        g_free(hex);
    }
    else if (str[0] == '@')
    {
        rv = str[1];
    }
    else
    {
        rv = g_atoix(str);
    }
    return rv;
}

/**************************************************************************//**
 * Parses the program args
 *
 * @param argc Passed to main
 * @param @argv Passed to main
 * @param pa program_pargs structure for resulting values
 * @return !=0 for success
 */
static int
parse_program_args(int argc, char *argv[], struct program_args *pa)
{
    int params_ok = 1;
    int opt;

    pa->input_file = NULL;
    pa->output_file = NULL;
    pa->font_name[0] = '\0';
    pa->point_size = DEFAULT_POINT_SIZE;
    pa->max_ucode = DEFAULT_MAX_CHAR;
    pa->sans10_compatibility = S10_AUTO;

    while ((opt = getopt(argc, argv, "n:p:m:C:")) != -1)
    {
        switch (opt)
        {
            case 'n':
                g_snprintf(pa->font_name, FV1_MAX_FONT_NAME_SIZE + 1, "%s",
                           optarg);
                break;

            case 'p':
                pa->point_size = g_atoi(optarg);
                break;

            case 'm':
                pa->max_ucode = parse_ucode_name(optarg);
                break;

            case 'C':
                if (toupper(optarg[0]) == 'A')
                {
                    pa->sans10_compatibility = S10_AUTO;
                }
                else if (g_text2bool(optarg))
                {
                    pa->sans10_compatibility = S10_ON;
                }
                else
                {
                    pa->sans10_compatibility = S10_OFF;
                }
                break;

            default:
                LOG(LOG_LEVEL_ERROR, "Unrecognised switch '%c'", (char)opt);
                params_ok = 0;
        }
    }

    if (pa->max_ucode < FV1_MIN_CHAR)
    {
        LOG(LOG_LEVEL_ERROR, "-m argument must be at least %d",
            FV1_MIN_CHAR);
        params_ok = 0;
    }

    switch (argc - optind)
    {
        case 0:
            LOG(LOG_LEVEL_ERROR, "No input file specified");
            params_ok = 0;
            break;

        case 1:
            LOG(LOG_LEVEL_ERROR, "No output file specified");
            params_ok = 0;
            break;
        case 2:
            pa->input_file = argv[optind];
            pa->output_file = argv[optind + 1];
            break;

        default:
            LOG(LOG_LEVEL_ERROR, "Unexpected arguments after output file");
            params_ok = 0;
    }

    return params_ok;
}

/**************************************************************************//**
 * Checks whether the specified glyph row is blank
 * @param g Glyph
 * @param row Row number between 0 and g->height - 1
 * @result Boolean
 */
static int
is_blank_glyph_row(const struct fv1_glyph *g, unsigned int row)
{
    if (g->width == 0 || row >= g->height)
    {
        return 1;
    }

    const unsigned int glyph_row_size = ((g->width + 7) / 8);
    const unsigned char *dataptr = g->data + (row * glyph_row_size);
    const unsigned int masks[] =
    { 0x80, 0xc0, 0xe0, 0xf0, 0xf8, 0xfc, 0xfe, 0xff };

    /* Check for set pixels in all the leading bytes */
    unsigned int x;
    for (x = g->width ; x > 8 ; x -= 8)
    {
        if (*dataptr++ != 0)
        {
            return 0;
        }
    }

    /* Use a single test to check the pixels in the last byte */
    return ((*dataptr & masks[x - 1]) == 0);
}

/**************************************************************************//**
 * Returns a string for a freetype2 error
 * @param error Freetype2 error code
 * @param buff Pointer to buffer for error string
 * @param bufflen Length of above
 */
static void
get_ft_error(FT_Error error, char *buff, unsigned int bufflen)
{
    const char *errstr = NULL;
    unsigned int i;
    for (i = 0 ; i < (sizeof(ft_errors) / sizeof(ft_errors[0])); ++i)
    {
        if (ft_errors[i].err_code == error)
        {
            errstr = ft_errors[i].err_msg;
            break;
        }
    }

    if (errstr != NULL)
    {
        g_snprintf(buff, bufflen, "[%s]", errstr);
    }
    else
    {
        g_snprintf(buff, bufflen,
                   "[errorcode %d (no description)]", (int)error);
    }
}

/**************************************************************************//**
 * Implement sans10 compatibility for a glyph
 *
 * The original Windows font generator made a few different choices for the
 * character x offset than freetype2 does. These are particularly noticeable
 * with a small font.
 *
 * This routine checks the glyph, and implements the original offset size
 * for popular English characters, which are all that the user will probably
 * be displaying with xrdp v0.9.x
 *
 * @param g Glyph to check
 */
static void
implement_sans10_compatibility(struct fv1_glyph *g, unsigned int ucode)
{
    const unsigned int max_index =
        sizeof(original_sans10_data) / sizeof(original_sans10_data[0]);

    if (ucode < FV1_MIN_CHAR || ucode >= max_index + FV1_MIN_CHAR)
    {
        return;
    }

    const struct x_dimensions *d =
            &original_sans10_data[ucode - FV1_MIN_CHAR];

    if (g->offset != d->offset)
    {
        if (g->width != d->width)
        {
            LOG(LOG_LEVEL_WARNING,
                "Can't set compatibility offset for U+%04X: width %d != %d",
                ucode, g->width, d->width);
        }
        else if (g->inc_by != d->inc_by)
        {
            LOG(LOG_LEVEL_WARNING,
                "Can't set compatibility offset for U+%04X: inc_by %d != %d",
                ucode, g->inc_by, d->inc_by);
        }
        else
        {
            LOG(LOG_LEVEL_INFO,
                "Changing compatibility offset for U+%04X: from %d to %d",
                ucode, g->offset, d->offset);
        }

        g->offset = d->offset;
    }
}

/**************************************************************************//**
 * Converts a freetype 2 bitmap to a fv1 glyph
 * @param ft_glyph Freetype2 glyph. Must be a monochrome bitmap
 * @param ucode Unicode character for error reporting
 * @param pa Program args
 * @return fv1 glyph, or NULL for error
 */
static struct fv1_glyph *
convert_mono_glyph(FT_GlyphSlot ft_glyph, unsigned int ucode,
                   const struct program_args *pa)
{
    short width = ft_glyph->bitmap.width;
    short height =  ft_glyph->bitmap.rows;
    struct fv1_glyph *g;

    /* Number of bytes in a glyph row */
    const unsigned int glyph_row_size = ((width + 7) / 8);

    if ((g = fv1_alloc_glyph(ucode, width, height)) != NULL)
    {
        g->baseline = -(ft_glyph->metrics.horiBearingY / 64);
        g->offset = ft_glyph->metrics.horiBearingX / 64;
        g->inc_by = ft_glyph->metrics.horiAdvance / 64;

        if (FONT_DATASIZE(g) > 0)
        {
            const unsigned char *srcptr = ft_glyph->bitmap.buffer;
            unsigned char *dstptr = g->data;
            unsigned int y;

            for (y = 0; y < g->height; ++y)
            {
                g_memcpy(dstptr, srcptr, glyph_row_size);
                dstptr += glyph_row_size;
                srcptr += ft_glyph->bitmap.pitch;
            }

            /* Optimise the glyph by removing any blank lines at the bottom
             * and top */
            if (g->width > 0)
            {
                while (g->height > 0 &&
                        is_blank_glyph_row(g, g->height - 1))
                {
                    --g->height;
                }

                y = 0;
                while (y < g->height && is_blank_glyph_row(g, y))
                {
                    ++y;
                }

                if (y > 0)
                {
                    g->baseline += y;
                    g->height -= y;
                    g_memmove(g->data, g->data + (y * glyph_row_size),
                              g->height * glyph_row_size);
                }
            }
        }
    }

    if (pa->sans10_compatibility != S10_OFF)
    {
        implement_sans10_compatibility(g, ucode);
    }

    return g;
}

/**************************************************************************//**
 * Main
 *
 * @param argc Argument count
 * @param argv Arguments
 */
int
main(int argc, char *argv[])
{
    FT_Library library = NULL;   /* handle to library     */
    FT_Face face = NULL;      /* handle to face object */
    FT_Error error;
    struct fv1_glyph *g;
    struct program_args pa;
    struct log_config *logging;
    int rv = 1;

    logging = log_config_init_for_console(LOG_LEVEL_WARNING,
                                          g_getenv("MKFV1_LOG_LEVEL"));
    log_start_from_param(logging);
    log_config_free(logging);

    struct fv1_file *fv1 = fv1_file_create();

    if (fv1 == NULL)
    {
        LOG(LOG_LEVEL_ERROR, "Memory allocation failure");
    }
    else if (parse_program_args(argc, argv, &pa))
    {
        char errstr[128];

        if ((error = FT_Init_FreeType(&library)) != 0)
        {
            get_ft_error(error, errstr, sizeof(errstr));
            LOG(LOG_LEVEL_ERROR, "Error initializing freetype library %s",
                errstr);
        }
        else if ((error = FT_New_Face(library, pa.input_file, 0, &face)) != 0)
        {
            get_ft_error(error, errstr, sizeof(errstr));
            LOG(LOG_LEVEL_ERROR, "Error loading font file %s %s",
                pa.input_file, errstr);
        }
        else if ((error = FT_Set_Char_Size(face, 0,
                                           pa.point_size * 64,
                                           FV1_DEVICE_DPI, 0)) != 0)
        {
            get_ft_error(error, errstr, sizeof(errstr));
            LOG(LOG_LEVEL_ERROR, "Error setting point size to %u %s",
                pa.point_size, errstr);
        }
        else
        {
            const char *fname =
                (pa.font_name[0] != '\0') ?  pa.font_name :
                (face->family_name != NULL) ? face->family_name :
                /* Default */ "";

            g_snprintf(fv1->font_name, FV1_MAX_FONT_NAME_SIZE + 1, "%s", fname);
            fv1->point_size = pa.point_size;
            fv1->body_height = face->size->metrics.height / 64;
            fv1->min_descender = face->size->metrics.descender / 64;

            if (pa.sans10_compatibility == S10_AUTO)
            {
                if (g_strcmp(fv1->font_name, "DejaVu Sans") == 0 &&
                        fv1->point_size == 10)
                {
                    pa.sans10_compatibility = S10_ON;
                }
                else
                {
                    pa.sans10_compatibility = S10_OFF;
                }
            }

            unsigned int u;
            for (u = FV1_MIN_CHAR; u <= pa.max_ucode; ++u)
            {
                /* retrieve glyph index from character code */
                FT_UInt glyph_index = FT_Get_Char_Index(face, u);

                /* load glyph image into the slot (erase previous one) */
                error = FT_Load_Glyph(face, glyph_index,
                                      FT_LOAD_RENDER | FT_LOAD_TARGET_MONO);
                if (error)
                {
                    get_ft_error(error, errstr, sizeof(errstr));
                    LOG(LOG_LEVEL_WARNING,
                        "Unable to get bitmap for U+%04X %s", u, errstr);
                    g = NULL;
                }
                else if (face->glyph->format != FT_GLYPH_FORMAT_BITMAP ||
                         face->glyph->bitmap.pixel_mode != FT_PIXEL_MODE_MONO)
                {
                    LOG(LOG_LEVEL_WARNING,
                        "Internal error; U+%04X was not loaded as a bitmap",
                        u);
                    g = NULL;
                }
                else
                {
                    g = convert_mono_glyph(face->glyph, u, &pa);
                }
                list_add_item(fv1->glyphs, (tintptr)g);
            }

            rv = fv1_file_save(fv1,  pa.output_file);
        }
    }

    FT_Done_FreeType(library);
    fv1_file_free(fv1);
    log_end();

    return rv;
}
