/* -*- mode: c++; c-basic-offset: 4 -*- */

#define NO_IMPORT_ARRAY

#include <string>
#include <algorithm>

#include "ft2font.h"
#include "mplutils.h"

#ifndef M_PI
#define M_PI 3.14159265358979323846264338328
#endif

/**
 To improve the hinting of the fonts, this code uses a hack
 presented here:

 http://antigrain.com/research/font_rasterization/index.html

 The idea is to limit the effect of hinting in the x-direction, while
 preserving hinting in the y-direction.  Since freetype does not
 support this directly, the dpi in the x-direction is set higher than
 in the y-direction, which affects the hinting grid.  Then, a global
 transform is placed on the font to shrink it back to the desired
 size.  While it is a bit surprising that the dpi setting affects
 hinting, whereas the global transform does not, this is documented
 behavior of freetype, and therefore hopefully unlikely to change.
 The freetype 2 tutorial says:

      NOTE: The transformation is applied to every glyph that is
      loaded through FT_Load_Glyph and is completely independent of
      any hinting process. This means that you won't get the same
      results if you load a glyph at the size of 24 pixels, or a glyph
      at the size at 12 pixels scaled by 2 through a transform,
      because the hints will have been computed differently (except
      you have disabled hints).
 */

FT_Library _ft2Library;

FT2Image::FT2Image() : m_dirty(true), m_buffer(NULL), m_width(0), m_height(0)
{
}

FT2Image::FT2Image(unsigned long width, unsigned long height)
    : m_dirty(true), m_buffer(NULL), m_width(0), m_height(0)
{
    resize(width, height);
}

FT2Image::~FT2Image()
{
    delete[] m_buffer;
}

void FT2Image::resize(long width, long height)
{
    if (width <= 0) {
        width = 1;
    }
    if (height <= 0) {
        height = 1;
    }
    size_t numBytes = width * height;

    if ((unsigned long)width != m_width || (unsigned long)height != m_height) {
        if (numBytes > m_width * m_height) {
            delete[] m_buffer;
            m_buffer = NULL;
            m_buffer = new unsigned char[numBytes];
        }

        m_width = (unsigned long)width;
        m_height = (unsigned long)height;
    }

    if (numBytes && m_buffer) {
        memset(m_buffer, 0, numBytes);
    }

    m_dirty = true;
}

void FT2Image::draw_bitmap(FT_Bitmap *bitmap, FT_Int x, FT_Int y)
{
    FT_Int image_width = (FT_Int)m_width;
    FT_Int image_height = (FT_Int)m_height;
    FT_Int char_width = bitmap->width;
    FT_Int char_height = bitmap->rows;

    FT_Int x1 = CLAMP(x, 0, image_width);
    FT_Int y1 = CLAMP(y, 0, image_height);
    FT_Int x2 = CLAMP(x + char_width, 0, image_width);
    FT_Int y2 = CLAMP(y + char_height, 0, image_height);

    FT_Int x_start = MAX(0, -x);
    FT_Int y_offset = y1 - MAX(0, -y);

    if (bitmap->pixel_mode == FT_PIXEL_MODE_GRAY) {
        for (FT_Int i = y1; i < y2; ++i) {
            unsigned char *dst = m_buffer + (i * image_width + x1);
            unsigned char *src = bitmap->buffer + (((i - y_offset) * bitmap->pitch) + x_start);
            for (FT_Int j = x1; j < x2; ++j, ++dst, ++src)
                *dst |= *src;
        }
    } else if (bitmap->pixel_mode == FT_PIXEL_MODE_MONO) {
        for (FT_Int i = y1; i < y2; ++i) {
            unsigned char *dst = m_buffer + (i * image_width + x1);
            unsigned char *src = bitmap->buffer + ((i - y_offset) * bitmap->pitch);
            for (FT_Int j = x1; j < x2; ++j, ++dst) {
                int x = (j - x1 + x_start);
                int val = *(src + (x >> 3)) & (1 << (7 - (x & 0x7)));
                *dst = val ? 255 : *dst;
            }
        }
    } else {
        throw "Unknown pixel mode";
    }

    m_dirty = true;
}

void FT2Image::draw_rect(unsigned long x0, unsigned long y0, unsigned long x1, unsigned long y1)
{
    if (x0 > m_width || x1 > m_width || y0 > m_height || y1 > m_height) {
        throw "Rect coords outside image bounds";
    }

    size_t top = y0 * m_width;
    size_t bottom = y1 * m_width;
    for (size_t i = x0; i < x1 + 1; ++i) {
        m_buffer[i + top] = 255;
        m_buffer[i + bottom] = 255;
    }

    for (size_t j = y0 + 1; j < y1; ++j) {
        m_buffer[x0 + j * m_width] = 255;
        m_buffer[x1 + j * m_width] = 255;
    }

    m_dirty = true;
}

void
FT2Image::draw_rect_filled(unsigned long x0, unsigned long y0, unsigned long x1, unsigned long y1)
{
    x0 = std::min(x0, m_width);
    y0 = std::min(y0, m_height);
    x1 = std::min(x1 + 1, m_width);
    y1 = std::min(y1 + 1, m_height);

    for (size_t j = y0; j < y1; j++) {
        for (size_t i = x0; i < x1; i++) {
            m_buffer[i + j * m_width] = 255;
        }
    }

    m_dirty = true;
}

inline double conv(long v)
{
    return double(v) / 64.0;
}

int FT2Font::get_path_count()
{
    // get the glyph as a path, a list of (COMMAND, *args) as desribed in matplotlib.path
    // this code is from agg's decompose_ft_outline with minor modifications

    if (!face->glyph) {
        throw "No glyph loaded";
    }

    FT_Outline &outline = face->glyph->outline;

    FT_Vector v_last;
    FT_Vector v_control;
    FT_Vector v_start;

    FT_Vector *point;
    FT_Vector *limit;
    char *tags;

    int n;     // index of contour in outline
    int first; // index of first point in contour
    char tag;  // current point's state
    int count;

    count = 0;
    first = 0;
    for (n = 0; n < outline.n_contours; n++) {
        int last; // index of last point in contour
        bool starts_with_last;

        last = outline.contours[n];
        limit = outline.points + last;

        v_start = outline.points[first];
        v_last = outline.points[last];

        v_control = v_start;

        point = outline.points + first;
        tags = outline.tags + first;
        tag = FT_CURVE_TAG(tags[0]);

        // A contour cannot start with a cubic control point!
        if (tag == FT_CURVE_TAG_CUBIC) {
            throw "A contour cannot start with a cubic control point";
        } else if (tag == FT_CURVE_TAG_CONIC) {
            starts_with_last = true;
        } else {
            starts_with_last = false;
        }

        count++;

        while (point < limit) {
            if (!starts_with_last) {
                point++;
                tags++;
            }
            starts_with_last = false;

            tag = FT_CURVE_TAG(tags[0]);
            switch (tag) {
            case FT_CURVE_TAG_ON: // emit a single line_to
            {
                count++;
                continue;
            }

            case FT_CURVE_TAG_CONIC: // consume conic arcs
            {
            Count_Do_Conic:
                if (point < limit) {
                    point++;
                    tags++;
                    tag = FT_CURVE_TAG(tags[0]);

                    if (tag == FT_CURVE_TAG_ON) {
                        count += 2;
                        continue;
                    }

                    if (tag != FT_CURVE_TAG_CONIC) {
                        throw "Invalid font";
                    }

                    count += 2;

                    goto Count_Do_Conic;
                }

                count += 2;

                goto Count_Close;
            }

            default: // FT_CURVE_TAG_CUBIC
            {
                if (point + 1 > limit || FT_CURVE_TAG(tags[1]) != FT_CURVE_TAG_CUBIC) {
                    throw "Invalid font";
                }

                point += 2;
                tags += 2;

                if (point <= limit) {
                    count += 3;
                    continue;
                }

                count += 3;

                goto Count_Close;
            }
            }
        }

        count++;

    Count_Close:
        first = last + 1;
    }

    return count;
}

void FT2Font::get_path(double *outpoints, unsigned char *outcodes)
{
    FT_Outline &outline = face->glyph->outline;
    bool flip_y = false; // todo, pass me as kwarg

    FT_Vector v_last;
    FT_Vector v_control;
    FT_Vector v_start;

    FT_Vector *point;
    FT_Vector *limit;
    char *tags;

    int n;     // index of contour in outline
    int first; // index of first point in contour
    char tag;  // current point's state

    first = 0;
    for (n = 0; n < outline.n_contours; n++) {
        int last; // index of last point in contour
        bool starts_with_last;

        last = outline.contours[n];
        limit = outline.points + last;

        v_start = outline.points[first];
        v_last = outline.points[last];

        v_control = v_start;

        point = outline.points + first;
        tags = outline.tags + first;
        tag = FT_CURVE_TAG(tags[0]);

        double x, y;
        if (tag != FT_CURVE_TAG_ON) {
            x = conv(v_last.x);
            y = flip_y ? -conv(v_last.y) : conv(v_last.y);
            starts_with_last = true;
        } else {
            x = conv(v_start.x);
            y = flip_y ? -conv(v_start.y) : conv(v_start.y);
            starts_with_last = false;
        }

        *(outpoints++) = x;
        *(outpoints++) = y;
        *(outcodes++) = MOVETO;

        while (point < limit) {
            if (!starts_with_last) {
                point++;
                tags++;
            }
            starts_with_last = false;

            tag = FT_CURVE_TAG(tags[0]);
            switch (tag) {
            case FT_CURVE_TAG_ON: // emit a single line_to
            {
                double x = conv(point->x);
                double y = flip_y ? -conv(point->y) : conv(point->y);
                *(outpoints++) = x;
                *(outpoints++) = y;
                *(outcodes++) = LINETO;
                continue;
            }

            case FT_CURVE_TAG_CONIC: // consume conic arcs
            {
                v_control.x = point->x;
                v_control.y = point->y;

            Do_Conic:
                if (point < limit) {
                    FT_Vector vec;
                    FT_Vector v_middle;

                    point++;
                    tags++;
                    tag = FT_CURVE_TAG(tags[0]);

                    vec.x = point->x;
                    vec.y = point->y;

                    if (tag == FT_CURVE_TAG_ON) {
                        double xctl = conv(v_control.x);
                        double yctl = flip_y ? -conv(v_control.y) : conv(v_control.y);
                        double xto = conv(vec.x);
                        double yto = flip_y ? -conv(vec.y) : conv(vec.y);
                        *(outpoints++) = xctl;
                        *(outpoints++) = yctl;
                        *(outpoints++) = xto;
                        *(outpoints++) = yto;
                        *(outcodes++) = CURVE3;
                        *(outcodes++) = CURVE3;
                        continue;
                    }

                    v_middle.x = (v_control.x + vec.x) / 2;
                    v_middle.y = (v_control.y + vec.y) / 2;

                    double xctl = conv(v_control.x);
                    double yctl = flip_y ? -conv(v_control.y) : conv(v_control.y);
                    double xto = conv(v_middle.x);
                    double yto = flip_y ? -conv(v_middle.y) : conv(v_middle.y);
                    *(outpoints++) = xctl;
                    *(outpoints++) = yctl;
                    *(outpoints++) = xto;
                    *(outpoints++) = yto;
                    *(outcodes++) = CURVE3;
                    *(outcodes++) = CURVE3;

                    v_control = vec;
                    goto Do_Conic;
                }
                double xctl = conv(v_control.x);
                double yctl = flip_y ? -conv(v_control.y) : conv(v_control.y);
                double xto = conv(v_start.x);
                double yto = flip_y ? -conv(v_start.y) : conv(v_start.y);

                *(outpoints++) = xctl;
                *(outpoints++) = yctl;
                *(outpoints++) = xto;
                *(outpoints++) = yto;
                *(outcodes++) = CURVE3;
                *(outcodes++) = CURVE3;

                goto Close;
            }

            default: // FT_CURVE_TAG_CUBIC
            {
                FT_Vector vec1, vec2;

                vec1.x = point[0].x;
                vec1.y = point[0].y;
                vec2.x = point[1].x;
                vec2.y = point[1].y;

                point += 2;
                tags += 2;

                if (point <= limit) {
                    FT_Vector vec;

                    vec.x = point->x;
                    vec.y = point->y;

                    double xctl1 = conv(vec1.x);
                    double yctl1 = flip_y ? -conv(vec1.y) : conv(vec1.y);
                    double xctl2 = conv(vec2.x);
                    double yctl2 = flip_y ? -conv(vec2.y) : conv(vec2.y);
                    double xto = conv(vec.x);
                    double yto = flip_y ? -conv(vec.y) : conv(vec.y);

                    (*outpoints++) = xctl1;
                    (*outpoints++) = yctl1;
                    (*outpoints++) = xctl2;
                    (*outpoints++) = yctl2;
                    (*outpoints++) = xto;
                    (*outpoints++) = yto;
                    (*outcodes++) = CURVE4;
                    (*outcodes++) = CURVE4;
                    (*outcodes++) = CURVE4;
                    continue;
                }

                double xctl1 = conv(vec1.x);
                double yctl1 = flip_y ? -conv(vec1.y) : conv(vec1.y);
                double xctl2 = conv(vec2.x);
                double yctl2 = flip_y ? -conv(vec2.y) : conv(vec2.y);
                double xto = conv(v_start.x);
                double yto = flip_y ? -conv(v_start.y) : conv(v_start.y);
                (*outpoints++) = xctl1;
                (*outpoints++) = yctl1;
                (*outpoints++) = xctl2;
                (*outpoints++) = yctl2;
                (*outpoints++) = xto;
                (*outpoints++) = yto;
                (*outcodes++) = CURVE4;
                (*outcodes++) = CURVE4;
                (*outcodes++) = CURVE4;

                goto Close;
            }
            }
        }

        (*outpoints++) = 0.0;
        (*outpoints++) = 0.0;
        (*outcodes++) = ENDPOLY;

    Close:
        first = last + 1;
    }
}

FT2Font::FT2Font(FT_Open_Args &open_args, long hinting_factor_) : image(), face(NULL)
{
    clear();

    int error = FT_Open_Face(_ft2Library, &open_args, 0, &face);

    if (error == FT_Err_Unknown_File_Format) {
        throw "Can not load face.  Unknown file format.";
    } else if (error == FT_Err_Cannot_Open_Resource) {
        throw "Can not load face.  Can not open resource.";
    } else if (error == FT_Err_Invalid_File_Format) {
        throw "Can not load face.  Invalid file format.";
    } else if (error) {
        throw "Can not load face.";
    }

    // set a default fontsize 12 pt at 72dpi
    hinting_factor = hinting_factor_;

    error = FT_Set_Char_Size(face, 12 * 64, 0, 72 * (unsigned int)hinting_factor, 72);
    if (error) {
        throw "Could not set the fontsize";
    }

    static FT_Matrix transform = { 65536 / hinting_factor, 0, 0, 65536 };
    FT_Set_Transform(face, &transform, 0);
}

FT2Font::~FT2Font()
{
    for (size_t i = 0; i < glyphs.size(); i++) {
        FT_Done_Glyph(glyphs[i]);
    }

    if (face) {
        FT_Done_Face(face);
    }
}

void FT2Font::clear()
{
    angle = 0.0;

    pen.x = 0;
    pen.y = 0;

    for (size_t i = 0; i < glyphs.size(); i++) {
        FT_Done_Glyph(glyphs[i]);
    }

    glyphs.clear();
}

void FT2Font::set_size(double ptsize, double dpi)
{
    int error = FT_Set_Char_Size(
        face, (long)(ptsize * 64), 0, (unsigned int)(dpi * hinting_factor), (unsigned int)dpi);
    static FT_Matrix transform = { 65536 / hinting_factor, 0, 0, 65536 };
    FT_Set_Transform(face, &transform, 0);

    if (error) {
        throw "Could not set the fontsize";
    }
}

void FT2Font::set_charmap(int i)
{
    if (i >= face->num_charmaps) {
        throw "i exceeds the available number of char maps";
    }
    FT_CharMap charmap = face->charmaps[i];
    if (FT_Set_Charmap(face, charmap)) {
        throw "Could not set the charmap";
    }
}

void FT2Font::select_charmap(unsigned long i)
{
    if (FT_Select_Charmap(face, (FT_Encoding)i)) {
        throw "Could not set the charmap";
    }
}

int FT2Font::get_kerning(int left, int right, int mode)
{
    if (!FT_HAS_KERNING(face)) {
        return 0;
    }
    FT_Vector delta;

    if (!FT_Get_Kerning(face, left, right, mode, &delta)) {
        return (int)(delta.x) / (hinting_factor << 6);
    } else {
        return 0;
    }
}

void FT2Font::set_text(
    size_t N, uint32_t *codepoints, double angle, FT_UInt32 flags, std::vector<double> &xys)
{
    angle = angle / 360.0 * 2 * M_PI;

    // this computes width and height in subpixels so we have to divide by 64
    matrix.xx = (FT_Fixed)(cos(angle) * 0x10000L);
    matrix.xy = (FT_Fixed)(-sin(angle) * 0x10000L);
    matrix.yx = (FT_Fixed)(sin(angle) * 0x10000L);
    matrix.yy = (FT_Fixed)(cos(angle) * 0x10000L);

    FT_Bool use_kerning = FT_HAS_KERNING(face);
    FT_UInt previous = 0;

    glyphs.resize(0);
    pen.x = 0;
    pen.y = 0;

    bbox.xMin = bbox.yMin = 32000;
    bbox.xMax = bbox.yMax = -32000;

    for (unsigned int n = 0; n < N; n++) {
        std::string thischar("?");
        FT_UInt glyph_index;
        FT_BBox glyph_bbox;
        FT_Pos last_advance;

        glyph_index = FT_Get_Char_Index(face, codepoints[n]);

        // retrieve kerning distance and move pen position
        if (use_kerning && previous && glyph_index) {
            FT_Vector delta;
            FT_Get_Kerning(face, previous, glyph_index, FT_KERNING_DEFAULT, &delta);
            pen.x += (delta.x << 10) / (hinting_factor << 16);
        }
        error = FT_Load_Glyph(face, glyph_index, flags);
        if (error) {
            throw "could not load glyph";
        }
        // ignore errors, jump to next glyph

        // extract glyph image and store it in our table

        FT_Glyph thisGlyph;
        error = FT_Get_Glyph(face->glyph, &thisGlyph);

        if (error) {
            throw "could not get glyph";
        }
        // ignore errors, jump to next glyph

        last_advance = face->glyph->advance.x;
        FT_Glyph_Transform(thisGlyph, 0, &pen);
        FT_Glyph_Transform(thisGlyph, &matrix, 0);
        xys.push_back(pen.x);
        xys.push_back(pen.y);

        FT_Glyph_Get_CBox(thisGlyph, ft_glyph_bbox_subpixels, &glyph_bbox);

        bbox.xMin = std::min(bbox.xMin, glyph_bbox.xMin);
        bbox.xMax = std::max(bbox.xMax, glyph_bbox.xMax);
        bbox.yMin = std::min(bbox.yMin, glyph_bbox.yMin);
        bbox.yMax = std::max(bbox.yMax, glyph_bbox.yMax);

        pen.x += last_advance;

        previous = glyph_index;
        glyphs.push_back(thisGlyph);
    }

    FT_Vector_Transform(&pen, &matrix);
    advance = pen.x;

    if (bbox.xMin > bbox.xMax) {
        bbox.xMin = bbox.yMin = bbox.xMax = bbox.yMax = 0;
    }
}

void FT2Font::load_char(long charcode, FT_UInt32 flags)
{
    int error = FT_Load_Char(face, (unsigned long)charcode, flags);

    if (error) {
        throw "Could not load charcode";
    }

    FT_Glyph thisGlyph;
    error = FT_Get_Glyph(face->glyph, &thisGlyph);

    if (error) {
        throw "Could not get glyph";
    }

    glyphs.push_back(thisGlyph);
}

void FT2Font::load_glyph(FT_UInt glyph_index, FT_UInt32 flags)
{
    int error = FT_Load_Glyph(face, glyph_index, flags);

    if (error) {
        throw "Could not load glyph";
    }

    FT_Glyph thisGlyph;
    error = FT_Get_Glyph(face->glyph, &thisGlyph);

    if (error) {
        throw "Could not load glyph";
    }

    glyphs.push_back(thisGlyph);
}

void FT2Font::get_width_height(long *width, long *height)
{
    *width = advance;
    *height = bbox.yMax - bbox.yMin;
}

long FT2Font::get_descent()
{
    return -bbox.yMin;
}

void FT2Font::get_bitmap_offset(long *x, long *y)
{
    *x = bbox.xMin;
    *y = 0;
}

void FT2Font::draw_glyphs_to_bitmap(bool antialiased)
{
    size_t width = (bbox.xMax - bbox.xMin) / 64 + 2;
    size_t height = (bbox.yMax - bbox.yMin) / 64 + 2;

    image.resize(width, height);

    for (size_t n = 0; n < glyphs.size(); n++) {
        error = FT_Glyph_To_Bitmap(
            &glyphs[n], antialiased ? FT_RENDER_MODE_NORMAL : FT_RENDER_MODE_MONO, 0, 1);
        if (error) {
            throw "Could not convert glyph to bitmap";
        }

        FT_BitmapGlyph bitmap = (FT_BitmapGlyph)glyphs[n];
        // now, draw to our target surface (convert position)

        // bitmap left and top in pixel, string bbox in subpixel
        FT_Int x = (FT_Int)(bitmap->left - (bbox.xMin / 64.));
        FT_Int y = (FT_Int)((bbox.yMax / 64.) - bitmap->top + 1);

        image.draw_bitmap(&bitmap->bitmap, x, y);
    }
}

void FT2Font::get_xys(bool antialiased, std::vector<double> &xys)
{
    for (size_t n = 0; n < glyphs.size(); n++) {

        error = FT_Glyph_To_Bitmap(
            &glyphs[n], antialiased ? FT_RENDER_MODE_NORMAL : FT_RENDER_MODE_MONO, 0, 1);
        if (error) {
            throw "Could not convert glyph to bitmap";
        }

        FT_BitmapGlyph bitmap = (FT_BitmapGlyph)glyphs[n];

        // bitmap left and top in pixel, string bbox in subpixel
        FT_Int x = (FT_Int)(bitmap->left - bbox.xMin / 64.);
        FT_Int y = (FT_Int)(bbox.yMax / 64. - bitmap->top + 1);
        // make sure the index is non-neg
        x = x < 0 ? 0 : x;
        y = y < 0 ? 0 : y;
        xys.push_back(x);
        xys.push_back(y);
    }
}

void FT2Font::draw_glyph_to_bitmap(FT2Image &im, int x, int y, size_t glyphInd, bool antialiased)
{
    FT_Vector sub_offset;
    sub_offset.x = 0; // int((xd - (double)x) * 64.0);
    sub_offset.y = 0; // int((yd - (double)y) * 64.0);

    if (glyphInd >= glyphs.size()) {
        throw "glyph num is out of range";
    }

    error = FT_Glyph_To_Bitmap(&glyphs[glyphInd],
                               antialiased ? FT_RENDER_MODE_NORMAL : FT_RENDER_MODE_MONO,
                               &sub_offset, // additional translation
                               1 // destroy image
                               );
    if (error) {
        throw "Could not convert glyph to bitmap";
    }

    FT_BitmapGlyph bitmap = (FT_BitmapGlyph)glyphs[glyphInd];

    im.draw_bitmap(&bitmap->bitmap, x + bitmap->left, y);
}

void FT2Font::get_glyph_name(unsigned int glyph_number, char *buffer)
{
    if (!FT_HAS_GLYPH_NAMES(face)) {
        /* Note that this generated name must match the name that
           is generated by ttconv in ttfont_CharStrings_getname. */
        PyOS_snprintf(buffer, 128, "uni%08x", glyph_number);
    } else {
        if (FT_Get_Glyph_Name(face, glyph_number, buffer, 128)) {
            throw "Could not get glyph names.";
        }
    }
}

long FT2Font::get_name_index(char *name)
{
    return FT_Get_Name_Index(face, (FT_String *)name);
}
