/*
 * Copyright (c) 2013, 2014, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

package com.sun.javafx.font.coretext;

import com.sun.javafx.font.FontResource;
import com.sun.javafx.font.Glyph;
import com.sun.javafx.geom.RectBounds;
import com.sun.javafx.geom.Shape;

class CTGlyph implements Glyph {
    private CTFontStrike strike;
    private int glyphCode;
    private CGRect bounds;
    private double xAdvance;
    private double yAdvance;
    private boolean drawShapes;

    /* Always using BRGA context has the same performance as gray */
    private static boolean LCD_CONTEXT = true;
    private static boolean CACHE_CONTEXT = true;

    private static long cachedContextRef;
    private static final int BITMAP_WIDTH = 256;
    private static final int BITMAP_HEIGHT = 256;
    private static final int MAX_SIZE = 320;
    private static final long GRAY_COLORSPACE = OS.CGColorSpaceCreateDeviceGray();
    private static final long RGB_COLORSPACE = OS.CGColorSpaceCreateDeviceRGB();

    CTGlyph(CTFontStrike strike, int glyphCode, boolean drawShapes) {
        this.strike = strike;
        this.glyphCode = glyphCode;
        this.drawShapes = drawShapes;
    }

    @Override public int getGlyphCode() {
        return glyphCode;
    }

    /* Note, according to javadoc these bounds should be
     * in user space but T2K uses device space.
     */
    @Override public RectBounds getBBox() {
        /* IN T2k this is the bounds of the glyph path see GeneralPath.cpp */
        CGRect rect = strike.getBBox(glyphCode);
        if (rect == null) return new RectBounds();
        return new RectBounds((float)rect.origin.x,
                              (float)rect.origin.y,
                              (float)(rect.origin.x + rect.size.width),
                              (float)(rect.origin.y + rect.size.height));
    }

    private void checkBounds() {
        if (bounds != null) return;
        bounds = new CGRect();
        if (strike.getSize() == 0) return;

        long fontRef = strike.getFontRef();
        if (fontRef == 0) return;
        int orientation = OS.kCTFontOrientationDefault;
        CGSize size = new CGSize();
        OS.CTFontGetAdvancesForGlyphs(fontRef, orientation, (short)glyphCode, size);
        xAdvance = size.width;
        yAdvance = -size.height;   /*Inverted coordinates system */

        if (drawShapes) return;

        /* Avoid CTFontGetBoundingRectsForGlyphs as it is too slow */
//        bounds = OS.CTFontGetBoundingRectsForGlyphs(fontRef, orientation, (short)glyphCode, null, 1);

        CTFontFile fr = (CTFontFile)strike.getFontResource();
        float[] bb = new float[4];
        fr.getGlyphBoundingBox((short)glyphCode, strike.getSize(), bb);
        bounds.origin.x = bb[0];
        bounds.origin.y = bb[1];
        bounds.size.width = (bb[2] - bb[0]);
        bounds.size.height = (bb[3] - bb[1]);
        if (strike.matrix != null) {
            /* Need to use the native matrix as it is y up */
            OS.CGRectApplyAffineTransform(bounds, strike.matrix);
        }

        if (bounds.size.width < 0 || bounds.size.height < 0 ||
            bounds.size.width > MAX_SIZE || bounds.size.height > MAX_SIZE) {
            /* Negative values for dimensions can indicate the font is corrupted.
             * Overly large dimensions also indicate problem with the font as
             * JavaFX uses path rasterizers for fontSize greater than 80pt.
             */
            bounds.origin.x = bounds.origin.y = bounds.size.width = bounds.size.height = 0;
        } else {

            /* The box is increased to capture all fragments from LCD rendering  */
            bounds.origin.x = (int)Math.floor(bounds.origin.x) - 1;
            bounds.origin.y = (int)Math.floor(bounds.origin.y) - 1;
            bounds.size.width = (int)Math.ceil(bounds.size.width) + 1 + 1 + 1;
            bounds.size.height = (int)Math.ceil(bounds.size.height) + 1 + 1 + 1;
        }
    }

    @Override public Shape getShape() {
        return strike.createGlyphOutline(glyphCode);
    }

    private long createContext(boolean lcd, int width, int height) {
        long space;
        int bpc = 8, bpr, flags;
        if (lcd) {
            space = RGB_COLORSPACE;
            bpr = width * 4;
            flags = OS.kCGBitmapByteOrder32Host | OS.kCGImageAlphaPremultipliedFirst;
        } else {
            space = GRAY_COLORSPACE;
            bpr = width;
            flags = OS.kCGImageAlphaNone;
        }
        long context =  OS.CGBitmapContextCreate(0, width, height, bpc, bpr, space, flags);

        boolean subPixel = strike.isSubPixelGlyph();
        OS.CGContextSetAllowsFontSmoothing(context, lcd);
        OS.CGContextSetAllowsAntialiasing(context, true);
        OS.CGContextSetAllowsFontSubpixelPositioning(context, subPixel);
        OS.CGContextSetAllowsFontSubpixelQuantization(context, subPixel);
        return context;

    }

    private long getCachedContext(boolean lcd) {
        if (cachedContextRef == 0) {
            cachedContextRef = createContext(lcd, BITMAP_WIDTH, BITMAP_HEIGHT);
        }
        return cachedContextRef;
    }

    private synchronized byte[] getImage(double x, double y, int w, int h, int subPixel) {

        if (w == 0 || h == 0) return new byte[0];

        long fontRef = strike.getFontRef();
        boolean lcd = isLCDGlyph();
        boolean lcdContext = LCD_CONTEXT || lcd;
        CGAffineTransform matrix = strike.matrix;
        boolean cache = CACHE_CONTEXT & BITMAP_WIDTH >= w & BITMAP_HEIGHT >= h;
        long context = cache ? getCachedContext(lcdContext) :
                               createContext(lcdContext, w, h);
        if (context == 0) return new byte[0];

        /* Fill background with white */
        OS.CGContextSetRGBFillColor(context, 1, 1, 1, 1);
        CGRect rect = new CGRect();
        rect.size.width = w;
        rect.size.height = h;
        OS.CGContextFillRect(context, rect);

        double drawX = 0, drawY = 0;
        if (matrix != null) {
            OS.CGContextTranslateCTM(context, -x, -y);
        } else {
            drawX = x - strike.getSubPixelPosition(subPixel);
            drawY = y;
        }

        /* Draw the text with black */
        OS.CGContextSetRGBFillColor(context, 0, 0, 0, 1);
        OS.CTFontDrawGlyphs(fontRef, (short)glyphCode, -drawX, -drawY, context);

        if (matrix != null) {
            OS.CGContextTranslateCTM(context, x, y);
        }

        byte[] imageData;
        if (lcd) {
            imageData = OS.CGBitmapContextGetData(context, w, h, 24);
        } else {
            imageData = OS.CGBitmapContextGetData(context, w, h, 8);
        }
        if (imageData == null) {
            bounds = new CGRect();
            imageData = new byte[0];
        }

        if (!cache) {
            OS.CGContextRelease(context);
        }
        return imageData;
    }

    @Override public byte[] getPixelData() {
        return getPixelData(0);
    }

    @Override public byte[] getPixelData(int subPixel) {
        checkBounds();
        return getImage(bounds.origin.x, bounds.origin.y,
                        (int)bounds.size.width, (int)bounds.size.height, subPixel);
    }

    @Override public float getAdvance() {
        checkBounds();
        //TODO should be user space (this method is not used)
        return (float)xAdvance;
    }

    @Override public float getPixelXAdvance() {
        checkBounds();
        return (float)xAdvance;
    }

    @Override public float getPixelYAdvance() {
        checkBounds();
        return (float)yAdvance;
    }

    @Override public int getWidth() {
        checkBounds();
        int w = (int)bounds.size.width;
        return isLCDGlyph() ? w * 3 : w;
    }

    @Override public int getHeight() {
        checkBounds();
        return (int)bounds.size.height;
    }

    @Override public int getOriginX() {
        checkBounds();
        return (int)bounds.origin.x;
    }

    @Override public int getOriginY() {
        checkBounds();
        int h = (int)bounds.size.height;
        int y = (int)bounds.origin.y;
        return -h - y; /*Inverted coordinates system */
    }

    @Override public boolean isLCDGlyph() {
        return strike.getAAMode() == FontResource.AA_LCD;
    }

}
