/*
  ==============================================================================

   This file is part of the JUCE library.
   Copyright (c) 2020 - Raw Material Software Limited

   JUCE is an open source library subject to commercial or open-source
   licensing.

   By using JUCE, you agree to the terms of both the JUCE 6 End-User License
   Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).

   End User License Agreement: www.juce.com/juce-6-licence
   Privacy Policy: www.juce.com/juce-privacy-policy

   Or: You may also use this code under the terms of the GPL v3 (see
   www.gnu.org/licenses).

   JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
   EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
   DISCLAIMED.

  ==============================================================================
*/

namespace juce
{

static constexpr float referenceFontSize = 1024.0f;

static CTFontRef getCTFontFromTypeface (const Font&);

namespace CoreTextTypeLayout
{
    static String findBestAvailableStyle (const Font& font, CGAffineTransform& requiredTransform)
    {
        auto availableStyles = Font::findAllTypefaceStyles (font.getTypefaceName());
        auto style = font.getTypefaceStyle();

        if (! availableStyles.contains (style))
        {
            if (font.isItalic())  // Fake-up an italic font if there isn't a real one.
                requiredTransform = CGAffineTransformMake (1.0f, 0, 0.1f, 1.0f, 0, 0);

            return availableStyles[0];
        }

        return style;
    }

    static float getFontTotalHeight (CTFontRef font)
    {
        return std::abs ((float) CTFontGetAscent (font))
             + std::abs ((float) CTFontGetDescent (font));
    }

    static float getHeightToPointsFactor (CTFontRef font)
    {
        return referenceFontSize / getFontTotalHeight (font);
    }

    static CFUniquePtr<CTFontRef> getFontWithPointSize (CTFontRef font, float pointSize)
    {
        return CFUniquePtr<CTFontRef> (CTFontCreateCopyWithAttributes (font, pointSize, nullptr, nullptr));
    }

    static CFUniquePtr<CTFontRef> createCTFont (const Font& font, const float fontSizePoints, CGAffineTransform& transformRequired)
    {
        CFUniquePtr<CFStringRef> cfFontFamily (FontStyleHelpers::getConcreteFamilyName (font).toCFString());
        CFUniquePtr<CFStringRef> cfFontStyle (findBestAvailableStyle (font, transformRequired).toCFString());
        CFStringRef keys[] = { kCTFontFamilyNameAttribute, kCTFontStyleNameAttribute };
        CFTypeRef values[] = { cfFontFamily.get(), cfFontStyle.get() };

        CFUniquePtr<CFDictionaryRef> fontDescAttributes (CFDictionaryCreate (nullptr,
                                                                             (const void**) &keys,
                                                                             (const void**) &values,
                                                                             numElementsInArray (keys),
                                                                             &kCFTypeDictionaryKeyCallBacks,
                                                                             &kCFTypeDictionaryValueCallBacks));

        CFUniquePtr<CTFontDescriptorRef> ctFontDescRef (CTFontDescriptorCreateWithAttributes (fontDescAttributes.get()));

        return CFUniquePtr<CTFontRef> (CTFontCreateWithFontDescriptor (ctFontDescRef.get(), fontSizePoints, nullptr));
    }

    //==============================================================================
    struct Advances
    {
        Advances (CTRunRef run, CFIndex numGlyphs) : advances (CTRunGetAdvancesPtr (run))
        {
            if (advances == nullptr)
            {
                local.malloc (numGlyphs);
                CTRunGetAdvances (run, CFRangeMake (0, 0), local);
                advances = local;
            }
        }

        const CGSize* advances;
        HeapBlock<CGSize> local;
    };

    struct Glyphs
    {
        Glyphs (CTRunRef run, size_t numGlyphs) : glyphs (CTRunGetGlyphsPtr (run))
        {
            if (glyphs == nullptr)
            {
                local.malloc (numGlyphs);
                CTRunGetGlyphs (run, CFRangeMake (0, 0), local);
                glyphs = local;
            }
        }

        const CGGlyph* glyphs;
        HeapBlock<CGGlyph> local;
    };

    struct Positions
    {
        Positions (CTRunRef run, size_t numGlyphs) : points (CTRunGetPositionsPtr (run))
        {
            if (points == nullptr)
            {
                local.malloc (numGlyphs);
                CTRunGetPositions (run, CFRangeMake (0, 0), local);
                points = local;
            }
        }

        const CGPoint* points;
        HeapBlock<CGPoint> local;
    };

    struct LineInfo
    {
        LineInfo (CTFrameRef frame, CTLineRef line, CFIndex lineIndex)
        {
            CTFrameGetLineOrigins (frame, CFRangeMake (lineIndex, 1), &origin);
            CTLineGetTypographicBounds (line, &ascent,  &descent, &leading);
        }

        CGPoint origin;
        CGFloat ascent, descent, leading;
    };

    static CFUniquePtr<CTFontRef> getOrCreateFont (const Font& f)
    {
        if (auto ctf = getCTFontFromTypeface (f))
        {
            CFRetain (ctf);
            return CFUniquePtr<CTFontRef> (ctf);
        }

        CGAffineTransform transform;
        return createCTFont (f, referenceFontSize, transform);
    }

    //==============================================================================
    static CTTextAlignment getTextAlignment (const AttributedString& text)
    {
        switch (text.getJustification().getOnlyHorizontalFlags())
        {
           #if defined (MAC_OS_X_VERSION_10_8) && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_8
            case Justification::right:                  return kCTTextAlignmentRight;
            case Justification::horizontallyCentred:    return kCTTextAlignmentCenter;
            case Justification::horizontallyJustified:  return kCTTextAlignmentJustified;
            default:                                    return kCTTextAlignmentLeft;
           #else
            case Justification::right:                  return kCTRightTextAlignment;
            case Justification::horizontallyCentred:    return kCTCenterTextAlignment;
            case Justification::horizontallyJustified:  return kCTJustifiedTextAlignment;
            default:                                    return kCTLeftTextAlignment;
           #endif
        }
    }

    static CTLineBreakMode getLineBreakMode (const AttributedString& text)
    {
        switch (text.getWordWrap())
        {
            case AttributedString::none:        return kCTLineBreakByClipping;
            case AttributedString::byChar:      return kCTLineBreakByCharWrapping;
            case AttributedString::byWord:
            default:                            return kCTLineBreakByWordWrapping;
        }
    }

    static CTWritingDirection getWritingDirection (const AttributedString& text)
    {
        switch (text.getReadingDirection())
        {
            case AttributedString::rightToLeft:   return kCTWritingDirectionRightToLeft;
            case AttributedString::leftToRight:   return kCTWritingDirectionLeftToRight;
            case AttributedString::natural:
            default:                              return kCTWritingDirectionNatural;
        }
    }

    //==============================================================================
    struct AttributedStringAndFontMap
    {
        CFUniquePtr<CFAttributedStringRef> string;
        std::map<CTFontRef, Font> fontMap;
    };

    static AttributedStringAndFontMap createCFAttributedString (const AttributedString& text)
    {
        std::map<CTFontRef, Font> fontMap;

        const detail::ColorSpacePtr rgbColourSpace { CGColorSpaceCreateWithName (kCGColorSpaceSRGB) };

        auto attribString = CFAttributedStringCreateMutable (kCFAllocatorDefault, 0);
        CFUniquePtr<CFStringRef> cfText (text.getText().toCFString());
        CFAttributedStringReplaceString (attribString, CFRangeMake (0, 0), cfText.get());

        auto numCharacterAttributes = text.getNumAttributes();
        auto attribStringLen = CFAttributedStringGetLength (attribString);

        for (int i = 0; i < numCharacterAttributes; ++i)
        {
            auto& attr = text.getAttribute (i);
            auto rangeStart = attr.range.getStart();

            if (rangeStart >= attribStringLen)
                continue;

            auto range = CFRangeMake (rangeStart, jmin (attr.range.getEnd(), (int) attribStringLen) - rangeStart);

            if (auto ctFontRef = getOrCreateFont (attr.font))
            {
                ctFontRef = getFontWithPointSize (ctFontRef.get(), attr.font.getHeight() * getHeightToPointsFactor (ctFontRef.get()));
                fontMap.emplace (ctFontRef.get(), attr.font);

                CFAttributedStringSetAttribute (attribString, range, kCTFontAttributeName, ctFontRef.get());

                if (attr.font.isUnderlined())
                {
                    auto underline = kCTUnderlineStyleSingle;

                    CFUniquePtr<CFNumberRef> numberRef (CFNumberCreate (nullptr, kCFNumberIntType, &underline));
                    CFAttributedStringSetAttribute (attribString, range, kCTUnderlineStyleAttributeName, numberRef.get());
                }

                auto extraKerning = attr.font.getExtraKerningFactor();

                if (extraKerning != 0)
                {
                    extraKerning *= attr.font.getHeight();

                    CFUniquePtr<CFNumberRef> numberRef (CFNumberCreate (nullptr, kCFNumberFloatType, &extraKerning));
                    CFAttributedStringSetAttribute (attribString, range, kCTKernAttributeName, numberRef.get());
                }
            }

            {
                auto col = attr.colour;

                const CGFloat components[] = { col.getFloatRed(),
                                               col.getFloatGreen(),
                                               col.getFloatBlue(),
                                               col.getFloatAlpha() };
                auto colour = CGColorCreate (rgbColourSpace.get(), components);

                CFAttributedStringSetAttribute (attribString, range, kCTForegroundColorAttributeName, colour);
                CGColorRelease (colour);
            }
        }

        // Paragraph Attributes
        auto ctTextAlignment = getTextAlignment (text);
        auto ctLineBreakMode = getLineBreakMode (text);
        auto ctWritingDirection = getWritingDirection (text);
        CGFloat ctLineSpacing = text.getLineSpacing();

        CTParagraphStyleSetting settings[] =
        {
            { kCTParagraphStyleSpecifierAlignment,              sizeof (CTTextAlignment),    &ctTextAlignment },
            { kCTParagraphStyleSpecifierLineBreakMode,          sizeof (CTLineBreakMode),    &ctLineBreakMode },
            { kCTParagraphStyleSpecifierBaseWritingDirection,   sizeof (CTWritingDirection), &ctWritingDirection},
            { kCTParagraphStyleSpecifierLineSpacingAdjustment,  sizeof (CGFloat),            &ctLineSpacing }
        };

        CFUniquePtr<CTParagraphStyleRef> ctParagraphStyleRef (CTParagraphStyleCreate (settings, (size_t) numElementsInArray (settings)));
        CFAttributedStringSetAttribute (attribString, CFRangeMake (0, CFAttributedStringGetLength (attribString)),
                                        kCTParagraphStyleAttributeName, ctParagraphStyleRef.get());
        return { CFUniquePtr<CFAttributedStringRef> (attribString), std::move (fontMap) };
    }

    struct FramesetterAndFontMap
    {
        CFUniquePtr<CTFramesetterRef> framesetter;
        std::map<CTFontRef, Font> fontMap;
    };

    static FramesetterAndFontMap createCTFramesetter (const AttributedString& text)
    {
        auto attribStringAndMap = createCFAttributedString (text);
        return { CFUniquePtr<CTFramesetterRef> (CTFramesetterCreateWithAttributedString (attribStringAndMap.string.get())),
                 std::move (attribStringAndMap.fontMap) };
    }

    static CFUniquePtr<CTFrameRef> createCTFrame (CTFramesetterRef framesetter, CGRect bounds)
    {
        auto path = CGPathCreateMutable();
        CGPathAddRect (path, nullptr, bounds);

        CFUniquePtr<CTFrameRef> frame (CTFramesetterCreateFrame (framesetter, CFRangeMake (0, 0), path, nullptr));
        CGPathRelease (path);

        return frame;
    }

    struct FrameAndFontMap
    {
        CFUniquePtr<CTFrameRef> frame;
        std::map<CTFontRef, Font> fontMap;
    };

    static FrameAndFontMap createCTFrame (const AttributedString& text, CGRect bounds)
    {
        auto framesetterAndMap = createCTFramesetter (text);
        return { createCTFrame (framesetterAndMap.framesetter.get(), bounds),
                 std::move (framesetterAndMap.fontMap) };
    }

    static Range<float> getLineVerticalRange (CTFrameRef frame, CFArrayRef lines, int lineIndex)
    {
        LineInfo info (frame, (CTLineRef) CFArrayGetValueAtIndex (lines, lineIndex), lineIndex);

        return { (float) (info.origin.y - info.descent),
                 (float) (info.origin.y + info.ascent) };
    }

    static float findCTFrameHeight (CTFrameRef frame)
    {
        auto lines = CTFrameGetLines (frame);
        auto numLines = CFArrayGetCount (lines);

        if (numLines == 0)
            return 0;

        auto range = getLineVerticalRange (frame, lines, 0);

        if (numLines > 1)
            range = range.getUnionWith (getLineVerticalRange (frame, lines, (int) numLines - 1));

        return range.getLength();
    }

    static bool areAllFontsDefaultWidth (const AttributedString& text)
    {
        auto numCharacterAttributes = text.getNumAttributes();

        for (int i = 0; i < numCharacterAttributes; ++i)
            if (text.getAttribute (i).font.getHorizontalScale() != 1.0f)
                return false;

        return true;
    }

    static bool drawToCGContext (const AttributedString& text, const Rectangle<float>& area,
                                 const CGContextRef& context, float flipHeight)
    {
        if (! areAllFontsDefaultWidth (text))
            return false;

        auto framesetter = createCTFramesetter (text).framesetter;

        // Ugly hack to fix a bug in OS X Sierra where the CTFrame needs to be slightly
        // larger than the font height - otherwise the CTFrame will be invalid

        CFRange fitrange;
        auto suggestedSingleLineFrameSize =
            CTFramesetterSuggestFrameSizeWithConstraints (framesetter.get(), CFRangeMake (0, 0), nullptr,
                                                          CGSizeMake (CGFLOAT_MAX, CGFLOAT_MAX), &fitrange);
        auto minCTFrameHeight = (float) suggestedSingleLineFrameSize.height;

        auto verticalJustification = text.getJustification().getOnlyVerticalFlags();

        auto ctFrameArea = [area, minCTFrameHeight, verticalJustification]
        {
            if (minCTFrameHeight < area.getHeight())
                return area;

            if (verticalJustification == Justification::verticallyCentred)
                return area.withSizeKeepingCentre (area.getWidth(), minCTFrameHeight);

            auto frameArea = area.withHeight (minCTFrameHeight);

            if (verticalJustification == Justification::bottom)
                return frameArea.withBottomY (area.getBottom());

            return frameArea;
        }();

        auto frame = createCTFrame (framesetter.get(), CGRectMake ((CGFloat) ctFrameArea.getX(), flipHeight - (CGFloat) ctFrameArea.getBottom(),
                                                                   (CGFloat) ctFrameArea.getWidth(), (CGFloat) ctFrameArea.getHeight()));

        auto textMatrix = CGContextGetTextMatrix (context);
        CGContextSaveGState (context);

        if (verticalJustification == Justification::verticallyCentred
         || verticalJustification == Justification::bottom)
        {
            auto adjust = ctFrameArea.getHeight() - findCTFrameHeight (frame.get());

            if (verticalJustification == Justification::verticallyCentred)
                adjust *= 0.5f;

            CGContextTranslateCTM (context, 0, -adjust);
        }

        CTFrameDraw (frame.get(), context);

        CGContextRestoreGState (context);
        CGContextSetTextMatrix (context, textMatrix);

        return true;
    }

    static void createLayout (TextLayout& glyphLayout, const AttributedString& text)
    {
        auto boundsHeight = glyphLayout.getHeight();
        auto frameAndMap = createCTFrame (text, CGRectMake (0, 0, glyphLayout.getWidth(), boundsHeight));
        auto lines = CTFrameGetLines (frameAndMap.frame.get());
        auto numLines = CFArrayGetCount (lines);

        glyphLayout.ensureStorageAllocated ((int) numLines);

        for (CFIndex i = 0; i < numLines; ++i)
        {
            auto line = (CTLineRef) CFArrayGetValueAtIndex (lines, i);
            auto runs = CTLineGetGlyphRuns (line);
            auto numRuns = CFArrayGetCount (runs);

            auto cfrlineStringRange = CTLineGetStringRange (line);
            auto lineStringEnd = cfrlineStringRange.location + cfrlineStringRange.length;
            Range<int> lineStringRange ((int) cfrlineStringRange.location, (int) lineStringEnd);

            LineInfo lineInfo (frameAndMap.frame.get(), line, i);

            auto glyphLine = std::make_unique<TextLayout::Line> (lineStringRange,
                                                                 Point<float> ((float) lineInfo.origin.x,
                                                                               (float) (boundsHeight - lineInfo.origin.y)),
                                                                 (float) lineInfo.ascent,
                                                                 (float) lineInfo.descent,
                                                                 (float) lineInfo.leading,
                                                                 (int) numRuns);

            for (CFIndex j = 0; j < numRuns; ++j)
            {
                auto run = (CTRunRef) CFArrayGetValueAtIndex (runs, j);
                auto numGlyphs = CTRunGetGlyphCount (run);
                auto runStringRange = CTRunGetStringRange (run);

                auto glyphRun = new TextLayout::Run (Range<int> ((int) runStringRange.location,
                                                                 (int) (runStringRange.location + runStringRange.length - 1)),
                                                     (int) numGlyphs);
                glyphLine->runs.add (glyphRun);

                CFDictionaryRef runAttributes = CTRunGetAttributes (run);

                CTFontRef ctRunFont;
                if (CFDictionaryGetValueIfPresent (runAttributes, kCTFontAttributeName, (const void**) &ctRunFont))
                {
                    glyphRun->font = [&]
                    {
                        auto it = frameAndMap.fontMap.find (ctRunFont);

                        if (it != frameAndMap.fontMap.end())
                            return it->second;

                        CFUniquePtr<CFStringRef> cfsFontName (CTFontCopyPostScriptName (ctRunFont));
                        CFUniquePtr<CTFontRef> ctFontRef (CTFontCreateWithName (cfsFontName.get(), referenceFontSize, nullptr));

                        auto fontHeightToPointsFactor = getHeightToPointsFactor (ctFontRef.get());

                        CFUniquePtr<CFStringRef> cfsFontFamily ((CFStringRef) CTFontCopyAttribute (ctRunFont, kCTFontFamilyNameAttribute));
                        CFUniquePtr<CFStringRef> cfsFontStyle ((CFStringRef) CTFontCopyAttribute (ctRunFont, kCTFontStyleNameAttribute));

                        Font result (String::fromCFString (cfsFontFamily.get()),
                                     String::fromCFString (cfsFontStyle.get()),
                                     (float) (CTFontGetSize (ctRunFont) / fontHeightToPointsFactor));

                        auto isUnderlined = [&]
                        {
                            CFNumberRef underlineStyle;

                            if (CFDictionaryGetValueIfPresent (runAttributes, kCTUnderlineStyleAttributeName, (const void**) &underlineStyle))
                            {
                                if (CFGetTypeID (underlineStyle) == CFNumberGetTypeID())
                                {
                                    int value = 0;
                                    CFNumberGetValue (underlineStyle, kCFNumberLongType, (void*) &value);

                                    return value != 0;
                                }
                            }

                            return false;
                        }();

                        result.setUnderline (isUnderlined);
                        return result;
                    }();
                }

                CGColorRef cgRunColor;

                if (CFDictionaryGetValueIfPresent (runAttributes, kCTForegroundColorAttributeName, (const void**) &cgRunColor)
                     && CGColorGetNumberOfComponents (cgRunColor) == 4)
                {
                    auto* components = CGColorGetComponents (cgRunColor);

                    glyphRun->colour = Colour::fromFloatRGBA ((float) components[0],
                                                              (float) components[1],
                                                              (float) components[2],
                                                              (float) components[3]);
                }

                const Glyphs glyphs (run, (size_t) numGlyphs);
                const Advances advances (run, numGlyphs);
                const Positions positions (run, (size_t) numGlyphs);

                for (CFIndex k = 0; k < numGlyphs; ++k)
                    glyphRun->glyphs.add (TextLayout::Glyph (glyphs.glyphs[k],
                                                             convertToPointFloat (positions.points[k]),
                                                             (float) advances.advances[k].width));
            }

            glyphLayout.addLine (std::move (glyphLine));
        }
    }
}


//==============================================================================
class OSXTypeface  : public Typeface
{
public:
    OSXTypeface (const Font& font)
        : Typeface (font.getTypefaceName(), font.getTypefaceStyle()), canBeUsedForLayout (true)
    {
        ctFontRef = CoreTextTypeLayout::createCTFont (font, referenceFontSize, renderingTransform);

        if (ctFontRef != nullptr)
        {
            fontRef = CTFontCopyGraphicsFont (ctFontRef.get(), nullptr);
            initialiseMetrics();
        }
    }

    OSXTypeface (const void* data, size_t dataSize)
        : Typeface ({}, {}), canBeUsedForLayout (false), dataCopy (data, dataSize)
    {
        // We can't use CFDataCreate here as this triggers a false positive in ASAN
        // so copy the data manually and use CFDataCreateWithBytesNoCopy
        CFUniquePtr<CFDataRef> cfData (CFDataCreateWithBytesNoCopy (kCFAllocatorDefault, (const UInt8*) dataCopy.getData(),
                                                                    (CFIndex) dataCopy.getSize(), kCFAllocatorNull));
        auto provider = CGDataProviderCreateWithCFData (cfData.get());

       #if JUCE_IOS
        // Workaround for a an obscure iOS bug which can cause the app to dead-lock
        // when loading custom type faces. See: http://www.openradar.me/18778790 and
        // http://stackoverflow.com/questions/40242370/app-hangs-in-simulator
        [UIFont systemFontOfSize: 12];
       #endif

        fontRef = CGFontCreateWithDataProvider (provider);
        CGDataProviderRelease (provider);

        if (fontRef != nullptr)
        {
           #if JUCE_MAC && defined (MAC_OS_X_VERSION_10_8) && MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_8
            if (SystemStats::getOperatingSystemType() >= SystemStats::OperatingSystemType::MacOSX_10_11)
                canBeUsedForLayout = CTFontManagerRegisterGraphicsFont (fontRef, nullptr);
           #endif

            ctFontRef.reset (CTFontCreateWithGraphicsFont (fontRef, referenceFontSize, nullptr, nullptr));

            if (ctFontRef != nullptr)
            {
                if (auto fontName = CFUniquePtr<CFStringRef> (CTFontCopyName (ctFontRef.get(), kCTFontFamilyNameKey)))
                    name = String::fromCFString (fontName.get());

                if (auto fontStyle = CFUniquePtr<CFStringRef> (CTFontCopyName (ctFontRef.get(), kCTFontStyleNameKey)))
                    style = String::fromCFString (fontStyle.get());

                initialiseMetrics();
            }
        }
    }

    void initialiseMetrics()
    {
        auto ctAscent  = std::abs ((float) CTFontGetAscent  (ctFontRef.get()));
        auto ctDescent = std::abs ((float) CTFontGetDescent (ctFontRef.get()));
        auto ctTotalHeight = ctAscent + ctDescent;

        ascent = ctAscent / ctTotalHeight;
        unitsToHeightScaleFactor = 1.0f / ctTotalHeight;
        pathTransform = AffineTransform::scale (unitsToHeightScaleFactor);

        fontHeightToPointsFactor = referenceFontSize / ctTotalHeight;

        const short zero = 0;
        CFUniquePtr<CFNumberRef> numberRef (CFNumberCreate (nullptr, kCFNumberShortType, &zero));

        CFStringRef keys[] = { kCTFontAttributeName, kCTLigatureAttributeName };
        CFTypeRef values[] = { ctFontRef.get(), numberRef.get() };
        attributedStringAtts.reset (CFDictionaryCreate (nullptr, (const void**) &keys,
                                                        (const void**) &values, numElementsInArray (keys),
                                                        &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks));
    }

    ~OSXTypeface() override
    {
        if (fontRef != nullptr)
        {
           #if JUCE_MAC && defined (MAC_OS_X_VERSION_10_8) && MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_8
            if (dataCopy.getSize() != 0)
                CTFontManagerUnregisterGraphicsFont (fontRef, nullptr);
           #endif

            CGFontRelease (fontRef);
        }
    }

    float getAscent() const override                 { return ascent; }
    float getDescent() const override                { return 1.0f - ascent; }
    float getHeightToPointsFactor() const override   { return fontHeightToPointsFactor; }

    float getStringWidth (const String& text) override
    {
        float x = 0;

        if (ctFontRef != nullptr && text.isNotEmpty())
        {
            CFUniquePtr<CFStringRef> cfText (text.toCFString());
            CFUniquePtr<CFAttributedStringRef> attribString (CFAttributedStringCreate (kCFAllocatorDefault, cfText.get(), attributedStringAtts.get()));

            CFUniquePtr<CTLineRef> line (CTLineCreateWithAttributedString (attribString.get()));
            auto runArray = CTLineGetGlyphRuns (line.get());

            for (CFIndex i = 0; i < CFArrayGetCount (runArray); ++i)
            {
                auto run = (CTRunRef) CFArrayGetValueAtIndex (runArray, i);
                auto length = CTRunGetGlyphCount (run);

                const CoreTextTypeLayout::Advances advances (run, length);

                for (int j = 0; j < length; ++j)
                    x += (float) advances.advances[j].width;
            }

            x *= unitsToHeightScaleFactor;
        }

        return x;
    }

    void getGlyphPositions (const String& text, Array<int>& resultGlyphs, Array<float>& xOffsets) override
    {
        xOffsets.add (0);

        if (ctFontRef != nullptr && text.isNotEmpty())
        {
            float x = 0;

            CFUniquePtr<CFStringRef> cfText (text.toCFString());
            CFUniquePtr<CFAttributedStringRef> attribString (CFAttributedStringCreate (kCFAllocatorDefault, cfText.get(), attributedStringAtts.get()));

            CFUniquePtr<CTLineRef> line (CTLineCreateWithAttributedString (attribString.get()));
            auto runArray = CTLineGetGlyphRuns (line.get());

            for (CFIndex i = 0; i < CFArrayGetCount (runArray); ++i)
            {
                auto run = (CTRunRef) CFArrayGetValueAtIndex (runArray, i);
                auto length = CTRunGetGlyphCount (run);

                const CoreTextTypeLayout::Advances advances (run, length);
                const CoreTextTypeLayout::Glyphs glyphs (run, (size_t) length);

                for (int j = 0; j < length; ++j)
                {
                    x += (float) advances.advances[j].width;
                    xOffsets.add (x * unitsToHeightScaleFactor);
                    resultGlyphs.add (glyphs.glyphs[j]);
                }
            }
        }
    }

    bool getOutlineForGlyph (int glyphNumber, Path& path) override
    {
        jassert (path.isEmpty());  // we might need to apply a transform to the path, so this must be empty

        if (auto pathRef = CFUniquePtr<CGPathRef> (CTFontCreatePathForGlyph (ctFontRef.get(), (CGGlyph) glyphNumber, &renderingTransform)))
        {
            CGPathApply (pathRef.get(), &path, pathApplier);

            if (! pathTransform.isIdentity())
                path.applyTransform (pathTransform);

            return true;
        }

        return false;
    }

    //==============================================================================
    CGFontRef fontRef = {};
    CFUniquePtr<CTFontRef> ctFontRef;

    float fontHeightToPointsFactor = 1.0f;
    CGAffineTransform renderingTransform = CGAffineTransformIdentity;

    bool canBeUsedForLayout;

private:
    MemoryBlock dataCopy;
    CFUniquePtr<CFDictionaryRef> attributedStringAtts;
    float ascent = 0, unitsToHeightScaleFactor = 0;
    AffineTransform pathTransform;

    static void pathApplier (void* info, const CGPathElement* element)
    {
        auto& path = *static_cast<Path*> (info);
        auto* p = element->points;

        switch (element->type)
        {
            case kCGPathElementMoveToPoint:         path.startNewSubPath ((float) p[0].x, (float) -p[0].y); break;
            case kCGPathElementAddLineToPoint:      path.lineTo          ((float) p[0].x, (float) -p[0].y); break;
            case kCGPathElementAddQuadCurveToPoint: path.quadraticTo     ((float) p[0].x, (float) -p[0].y,
                                                                          (float) p[1].x, (float) -p[1].y); break;
            case kCGPathElementAddCurveToPoint:     path.cubicTo         ((float) p[0].x, (float) -p[0].y,
                                                                          (float) p[1].x, (float) -p[1].y,
                                                                          (float) p[2].x, (float) -p[2].y); break;
            case kCGPathElementCloseSubpath:        path.closeSubPath(); break;
            default:                                jassertfalse; break;
        }
    }

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (OSXTypeface)
};

CTFontRef getCTFontFromTypeface (const Font& f)
{
    if (auto* tf = dynamic_cast<OSXTypeface*> (f.getTypeface()))
        return tf->ctFontRef.get();

    return {};
}

StringArray Font::findAllTypefaceNames()
{
    StringArray names;

   #if JUCE_MAC
    // CTFontManager only exists on OS X 10.6 and later, it does not exist on iOS
    CFUniquePtr<CFArrayRef> fontFamilyArray (CTFontManagerCopyAvailableFontFamilyNames());

    for (CFIndex i = 0; i < CFArrayGetCount (fontFamilyArray.get()); ++i)
    {
        auto family = String::fromCFString ((CFStringRef) CFArrayGetValueAtIndex (fontFamilyArray.get(), i));

        if (! family.startsWithChar ('.')) // ignore fonts that start with a '.'
            names.addIfNotAlreadyThere (family);
    }
   #else
    CFUniquePtr<CTFontCollectionRef> fontCollectionRef (CTFontCollectionCreateFromAvailableFonts (nullptr));
    CFUniquePtr<CFArrayRef> fontDescriptorArray (CTFontCollectionCreateMatchingFontDescriptors (fontCollectionRef.get()));

    for (CFIndex i = 0; i < CFArrayGetCount (fontDescriptorArray.get()); ++i)
    {
        auto ctFontDescriptorRef = (CTFontDescriptorRef) CFArrayGetValueAtIndex (fontDescriptorArray.get(), i);
        CFUniquePtr<CFStringRef> cfsFontFamily ((CFStringRef) CTFontDescriptorCopyAttribute (ctFontDescriptorRef, kCTFontFamilyNameAttribute));

        names.addIfNotAlreadyThere (String::fromCFString (cfsFontFamily.get()));
    }
   #endif

    names.sort (true);
    return names;
}

StringArray Font::findAllTypefaceStyles (const String& family)
{
    if (FontStyleHelpers::isPlaceholderFamilyName (family))
        return findAllTypefaceStyles (FontStyleHelpers::getConcreteFamilyNameFromPlaceholder (family));

    StringArray results;

    CFUniquePtr<CFStringRef> cfsFontFamily (family.toCFString());
    CFStringRef keys[] = { kCTFontFamilyNameAttribute };
    CFTypeRef values[] = { cfsFontFamily.get() };

    CFUniquePtr<CFDictionaryRef> fontDescAttributes (CFDictionaryCreate (nullptr, (const void**) &keys, (const void**) &values, numElementsInArray (keys),
                                                                         &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks));

    CFUniquePtr<CTFontDescriptorRef> ctFontDescRef (CTFontDescriptorCreateWithAttributes (fontDescAttributes.get()));
    CFUniquePtr<CFArrayRef> fontFamilyArray (CFArrayCreate (kCFAllocatorDefault, (const void**) &ctFontDescRef, 1, &kCFTypeArrayCallBacks));

    CFUniquePtr<CTFontCollectionRef> fontCollectionRef (CTFontCollectionCreateWithFontDescriptors (fontFamilyArray.get(), nullptr));

    if (auto fontDescriptorArray = CFUniquePtr<CFArrayRef> (CTFontCollectionCreateMatchingFontDescriptors (fontCollectionRef.get())))
    {
        for (CFIndex i = 0; i < CFArrayGetCount (fontDescriptorArray.get()); ++i)
        {
            auto ctFontDescriptorRef = (CTFontDescriptorRef) CFArrayGetValueAtIndex (fontDescriptorArray.get(), i);
            CFUniquePtr<CFStringRef> cfsFontStyle ((CFStringRef) CTFontDescriptorCopyAttribute (ctFontDescriptorRef, kCTFontStyleNameAttribute));
            results.add (String::fromCFString (cfsFontStyle.get()));
        }
    }

    return results;
}


//==============================================================================
Typeface::Ptr Typeface::createSystemTypefaceFor (const Font& font)                  { return *new OSXTypeface (font); }
Typeface::Ptr Typeface::createSystemTypefaceFor (const void* data, size_t size)     { return *new OSXTypeface (data, size); }

void Typeface::scanFolderForFonts (const File& folder)
{
    for (auto& file : folder.findChildFiles (File::findFiles, false, "*.otf;*.ttf"))
        if (auto urlref = CFUniquePtr<CFURLRef> (CFURLCreateWithFileSystemPath (kCFAllocatorDefault, file.getFullPathName().toCFString(), kCFURLPOSIXPathStyle, true)))
            CTFontManagerRegisterFontsForURL (urlref.get(), kCTFontManagerScopeProcess, nullptr);
}

struct DefaultFontNames
{
   #if JUCE_IOS
    String defaultSans  { "Helvetica" },
           defaultSerif { "Times New Roman" },
           defaultFixed { "Courier New" };
   #else
    String defaultSans  { "Lucida Grande" },
           defaultSerif { "Times New Roman" },
           defaultFixed { "Menlo" };
   #endif
};

Typeface::Ptr Font::getDefaultTypefaceForFont (const Font& font)
{
    static DefaultFontNames defaultNames;

    auto newFont = font;
    auto& faceName = font.getTypefaceName();

    if (faceName == getDefaultSansSerifFontName())       newFont.setTypefaceName (defaultNames.defaultSans);
    else if (faceName == getDefaultSerifFontName())      newFont.setTypefaceName (defaultNames.defaultSerif);
    else if (faceName == getDefaultMonospacedFontName()) newFont.setTypefaceName (defaultNames.defaultFixed);

    if (font.getTypefaceStyle() == getDefaultStyle())
        newFont.setTypefaceStyle ("Regular");

    return Typeface::createSystemTypefaceFor (newFont);
}

static bool canAllTypefacesBeUsedInLayout (const AttributedString& text)
{
    auto numCharacterAttributes = text.getNumAttributes();

    for (int i = 0; i < numCharacterAttributes; ++i)
    {
        if (auto tf = dynamic_cast<OSXTypeface*> (text.getAttribute (i).font.getTypeface()))
            if (tf->canBeUsedForLayout)
                continue;

        return false;
    }

    return true;
}

bool TextLayout::createNativeLayout (const AttributedString& text)
{
    if (canAllTypefacesBeUsedInLayout (text) && CoreTextTypeLayout::areAllFontsDefaultWidth (text))
    {
        CoreTextTypeLayout::createLayout (*this, text);
        return true;
    }

    return false;
}

} // namespace juce
