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

   This file is part of the JUCE library - "Jules' Utility Class Extensions"
   Copyright 2004-6 by Raw Material Software ltd.

  ------------------------------------------------------------------------------

   JUCE can be redistributed and/or modified under the terms of the
   GNU General Public License, as published by the Free Software Foundation;
   either version 2 of the License, or (at your option) any later version.

   JUCE 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 General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with JUCE; if not, visit www.gnu.org/licenses or write to the
   Free Software Foundation, Inc., 59 Temple Place, Suite 330, 
   Boston, MA 02111-1307 USA

  ------------------------------------------------------------------------------

   If you'd like to release a closed-source product which uses JUCE, commercial
   licenses are also available: visit www.rawmaterialsoftware.com/juce for
   more information.

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

#include "../../../../juce_core/basics/juce_StandardHeader.h"

BEGIN_JUCE_NAMESPACE


#include "juce_Graphics.h"
#include "../fonts/juce_GlyphArrangement.h"
#include "../geometry/juce_PathStrokeType.h"
#include "juce_EdgeTable.h"
#include "juce_LowLevelGraphicsContext.h"

static const Graphics::ResamplingQuality defaultQuality = Graphics::mediumResamplingQuality;


//==============================================================================
LowLevelGraphicsContext::LowLevelGraphicsContext()
{
}

LowLevelGraphicsContext::~LowLevelGraphicsContext()
{
}

//==============================================================================
Graphics::Graphics (Image& imageToDrawOnto)
    : context (imageToDrawOnto.createLowLevelContext()),
      ownsContext (true),
      isVectorBased (false),
      currentBrush (0),
      quality (defaultQuality)
{
}

Graphics::Graphics (LowLevelGraphicsContext* const internalContext)
    : context (internalContext),
      ownsContext (false),
      isVectorBased (internalContext->isVectorDevice()),
      currentBrush (0),
      quality (defaultQuality)
{
}

Graphics::Graphics (const Graphics& parent) throw()
    : context (parent.context->createCopy()),
      ownsContext (true),
      isVectorBased (parent.isVectorBased),
      currentBrush (0),
      quality (defaultQuality)
{
}

Graphics::~Graphics() throw()
{
    if (currentBrush != 0)
    {
        currentBrush->ownerGraphics = 0;
        currentBrush = 0;
    }

    if (ownsContext)
        delete context;
}

//==============================================================================
void Graphics::resetToDefaultState()
{
    setColour (Colours::black);
    currentFont.resetToDefaultState();
    quality = defaultQuality;
}

bool Graphics::reduceClipRegion (const int x, const int y,
                                 const int w, const int h)
{
    return context->reduceClipRegion (x, y, w, h);
}

void Graphics::excludeClipRegion (const int x, const int y,
                                  const int w, const int h)
{
    context->excludeClipRegion (x, y, w, h);
}

void Graphics::setClipRegion (const RectangleList& newRegion)
{
    context->setClipRegion (newRegion);
}

bool Graphics::isClipEmpty() const
{
    return context->isClipEmpty();
}

const Rectangle Graphics::getClipBounds() const
{
    return context->getClipBounds();
}

const RectangleList Graphics::getClipRegion() const
{
    return context->getClipRegion();
}

void Graphics::setOrigin (const int newOriginX,
                          const int newOriginY)
{
    context->setOrigin (newOriginX, newOriginY);
}

bool Graphics::clipRegionIntersects (const int x, const int y,
                                     const int w, const int h) const throw()
{
    return context->clipRegionIntersects (x, y, w, h);
}

//==============================================================================
void Graphics::setColour (const Colour& newColour) throw()
{
    colourBrush.setColour (newColour);

    if (currentBrush != 0)
    {
        currentBrush->ownerGraphics = 0;
        currentBrush = 0;
    }
}

const Colour& Graphics::getCurrentColour() const throw()
{
    return colourBrush.getColour();
}

void Graphics::setOpacity (const float newOpacity) throw()
{
    colourBrush.setColour (colourBrush.getColour().withAlpha (newOpacity));
}

void Graphics::setBrush (Brush* const newBrush)
{
    if (currentBrush != newBrush)
    {
        if (currentBrush != 0)
            currentBrush->ownerGraphics = 0;

        currentBrush = newBrush;

        if (newBrush != 0)
        {
            jassert (newBrush->ownerGraphics == 0); // you can't select a brush into more than one Graphics
                                                    // context at the same time..
            newBrush->ownerGraphics = this;
        }
    }
}

Brush* Graphics::getBrushToUse() throw()
{
    return (currentBrush == 0) ? &colourBrush
                               : currentBrush;
}

//==============================================================================
void Graphics::setFont (const Font& newFont) throw()
{
    currentFont = newFont;
}

void Graphics::setFont (const float newFontHeight,
                        const int newFontStyleFlags) throw()
{
    currentFont.setSizeAndStyle (newFontHeight, newFontStyleFlags, 1.0f, 0.0f);
}

const Font& Graphics::getCurrentFont() const
{
    return currentFont;
}

//==============================================================================
void Graphics::drawSingleLineText (const String& text,
                                   const int startX,
                                   const int baselineY)
{
    if (text.isNotEmpty()
         && startX < context->getClipBounds().getRight())
    {
        GlyphArrangement arr;
        arr.addLineOfText (currentFont, text, (float) startX, (float) baselineY);
        arr.draw (*this);
    }
}

void Graphics::drawTextAsPath (const String& text,
                               const AffineTransform& transform)
{
    if (text.isNotEmpty())
    {
        GlyphArrangement arr;
        arr.addLineOfText (currentFont, text, 0.0f, 0.0f);
        arr.draw (*this, transform);
    }
}

void Graphics::drawMultiLineText (const String& text,
                                  const int startX,
                                  const int baselineY,
                                  const int maximumLineWidth)
{
    if (text.isNotEmpty()
         && startX < context->getClipBounds().getRight())
    {
        GlyphArrangement arr;
        arr.addJustifiedText (currentFont, text,
                              (float) startX, (float) baselineY, (float) maximumLineWidth,
                              Justification::left);
        arr.draw (*this);
    }
}

void Graphics::drawText (const String& text,
                         const int x,
                         const int y,
                         const int width,
                         const int height,
                         const Justification& justificationType,
                         const bool useEllipsesIfTooBig)
{
    if (text.isNotEmpty() && context->clipRegionIntersects (x, y, width, height))
    {
        GlyphArrangement arr;

        arr.addCurtailedLineOfText (currentFont, text,
                                    0.0f, 0.0f, (float)width,
                                    useEllipsesIfTooBig);

        arr.justifyGlyphs (0, arr.getNumGlyphs(),
                           (float) x, (float) y,
                           (float) width, (float) height,
                           justificationType);
        arr.draw (*this);
    }
}

void Graphics::drawFittedText (const String& text,
                               const int x,
                               const int y,
                               const int width,
                               const int height,
                               const Justification& justification,
                               const int maximumNumberOfLines)
{
    if (text.isNotEmpty()
         && width > 0 && height > 0
         && context->clipRegionIntersects (x, y, width, height))
    {
        GlyphArrangement arr;

        arr.addFittedText (currentFont, text,
                           (float) x, (float) y,
                           (float) width, (float) height,
                           justification,
                           maximumNumberOfLines);

        arr.draw (*this);
    }
}


//==============================================================================
void Graphics::fillRect (int x,
                         int y,
                         int width,
                         int height)
{
    getBrushToUse()->paintRectangle (*context, x, y, width, height);
}

void Graphics::fillRect (const Rectangle& r)
{
    fillRect (r.getX(),
              r.getY(),
              r.getWidth(),
              r.getHeight());
}

void Graphics::fillRect (const float x,
                         const float y,
                         const float width,
                         const float height)
{
    Path p;
    p.addRectangle (x, y, width, height);
    fillPath (p);
}

void Graphics::setPixel (int x, int y)
{
    if (context->clipRegionIntersects (x, y, 1, 1))
    {
        getBrushToUse()->paintRectangle (*context, x, y, 1, 1);
    }
}

void Graphics::fillAll()
{
    fillRect (context->getClipBounds());
}

void Graphics::fillAll (const Colour& colourToUse)
{
    const Rectangle clip (context->getClipBounds());

    context->fillRectWithColour (clip.getX(), clip.getY(), clip.getWidth(), clip.getHeight(),
                                 colourToUse, false);
}


//==============================================================================
void Graphics::fillPath (const Path& path,
                         const AffineTransform& transform)
{
    if ((! context->isClipEmpty()) && ! path.isEmpty())
    {
        getBrushToUse()->paintPath (*context, path, transform);
    }
}

void Graphics::strokePath (const Path& path,
                           const PathStrokeType& strokeType,
                           const AffineTransform& transform)
{
    if (! colourBrush.getColour().isTransparent())
    {
        Path stroke;
        strokeType.createStrokedPath (stroke, path, transform);
        fillPath (stroke);
    }
}

//==============================================================================
void Graphics::drawRect (const int x,
                         const int y,
                         const int width,
                         const int height,
                         const int lineThickness)
{
    fillRect (x, y, width, lineThickness);
    fillRect (x, y + lineThickness, lineThickness, height - lineThickness * 2);
    fillRect (x + width - lineThickness, y + lineThickness, lineThickness, height - lineThickness * 2);
    fillRect (x, y + height - lineThickness, width, lineThickness);
}

void Graphics::drawBevel (const int x,
                          const int y,
                          const int width,
                          const int height,
                          const int bevelThickness,
                          const Colour& topLeftColour,
                          const Colour& bottomRightColour,
                          const bool useGradient)
{
    if (clipRegionIntersects (x, y, width, height))
    {
        const float oldOpacity = colourBrush.getColour().getFloatAlpha();
        const float ramp = oldOpacity / bevelThickness;

        for (int i = bevelThickness; --i >= 0;)
        {
            const float op = useGradient ? ramp * (bevelThickness - i)
                                         : oldOpacity;

            setColour (topLeftColour.withMultipliedAlpha (op));
            fillRect (x + i, y + i, width - i * 2, 1);

            setColour (topLeftColour.withMultipliedAlpha (op * 0.75f));
            fillRect (x + i, y + i + 1, 1, height - i * 2 - 2);

            setColour (bottomRightColour.withMultipliedAlpha (op));
            fillRect (x + i, y + height - i - 1, width - i * 2, 1);

            setColour (bottomRightColour.withMultipliedAlpha (op  * 0.75f));
            fillRect (x + width - i - 1, y + i + 1, 1, height - i * 2 - 2);
        }
    }
}

//==============================================================================
void Graphics::fillEllipse (const float x,
                            const float y,
                            const float width,
                            const float height)
{
    Path p;
    p.addEllipse (x, y, width, height);
    fillPath (p);
}

void Graphics::drawEllipse (const float x,
                            const float y,
                            const float width,
                            const float height,
                            const float lineThickness)
{
    Path p;
    p.addEllipse (x, y, width, height);
    strokePath (p, PathStrokeType (lineThickness));
}

void Graphics::fillRoundedRectangle (const float x,
                                     const float y,
                                     const float width,
                                     const float height,
                                     const float cornerSize)
{
    Path p;
    p.addRoundedRectangle (x, y, width, height, cornerSize);
    fillPath (p);
}

void Graphics::drawRoundedRectangle (const float x,
                                     const float y,
                                     const float width,
                                     const float height,
                                     const float cornerSize,
                                     const float lineThickness)
{
    Path p;
    p.addRoundedRectangle (x, y, width, height, cornerSize);
    strokePath (p, PathStrokeType (lineThickness));
}

void Graphics::drawArrow (const float startX,
                          const float startY,
                          const float endX,
                          const float endY,
                          const float lineThickness,
                          const float arrowheadWidth,
                          const float arrowheadLength)
{
    Path p;
    p.addArrow (startX, startY, endX, endY,
                lineThickness, arrowheadWidth, arrowheadLength);
    fillPath (p);
}

//==============================================================================
void Graphics::drawVerticalLine (const int x, float top, float bottom)
{
    getBrushToUse()->paintVerticalLine (*context, x, top, bottom);
}

void Graphics::drawHorizontalLine (const int y, float left, float right)
{
    getBrushToUse()->paintHorizontalLine (*context, y, left, right);
}

void Graphics::drawLine (float x1, float y1,
                         float x2, float y2)
{
    if (! context->isClipEmpty())
        getBrushToUse()->paintLine (*context, x1, y1, x2, y2);
}

void Graphics::drawLine (const float startX,
                         const float startY,
                         const float endX,
                         const float endY,
                         const float lineThickness)
{
    Path p;
    p.addLineSegment (startX, startY, endX, endY, lineThickness);
    fillPath (p);
}

void Graphics::drawLine (const Line& line)
{
    drawLine (line.getStartX(), line.getStartY(), line.getEndX(), line.getEndY());
}

void Graphics::drawLine (const Line& line,
                         const float lineThickness)
{
    drawLine (line.getStartX(), line.getStartY(), line.getEndX(), line.getEndY(), lineThickness);
}

void Graphics::drawDashedLine (const float startX,
                               const float startY,
                               const float endX,
                               const float endY,
                               const float* const dashLengths,
                               const int numDashLengths,
                               const float lineThickness)
{
    const double dx = endX - startX;
    const double dy = endY - startY;
    const double totalLen = juce_hypot (dx, dy);

    if (totalLen >= 0.5)
    {
        const double onePixAlpha = 1.0 / totalLen;

        double alpha = 0.0;
        float x = startX;
        float y = startY;
        int n = 0;

        while (alpha < 1.0f)
        {
            alpha = jmin (1.0, alpha + dashLengths[n++] * onePixAlpha);
            n = n % numDashLengths;

            const float oldX = x;
            const float oldY = y;

            x = (float) (startX + dx * alpha);
            y = (float) (startY + dy * alpha);

            if ((n & 1) != 0)
            {
                if (lineThickness != 1.0f)
                    drawLine (oldX, oldY, x, y, lineThickness);
                else
                    drawLine (oldX, oldY, x, y);
            }
        }
    }
}

//==============================================================================
void Graphics::setImageResamplingQuality (const Graphics::ResamplingQuality newQuality)
{
    quality = newQuality;
}

//==============================================================================
void Graphics::drawImageAt (const Image* const imageToDraw,
                            const int topLeftX,
                            const int topLeftY,
                            const bool fillAlphaChannelWithCurrentBrush)
{
    if (imageToDraw != 0)
    {
        const int imageW = imageToDraw->getWidth();
        const int imageH = imageToDraw->getHeight();

        drawImage (imageToDraw,
                   topLeftX, topLeftY, imageW, imageH,
                   0, 0, imageW, imageH,
                   fillAlphaChannelWithCurrentBrush);
    }
}

void Graphics::drawImageWithin (const Image* const imageToDraw,
                                const int destX,
                                const int destY,
                                const int destW,
                                const int destH,
                                const Justification& justification,
                                const bool onlyReduceInSize,
                                const bool fillAlphaChannelWithCurrentBrush)
{
    if (imageToDraw != 0)
    {
        const int imageW = imageToDraw->getWidth();
        const int imageH = imageToDraw->getHeight();

        if (imageW > 0 && imageH > 0
             && destW > 0 && destH > 0)
        {
            int newW, newH;

            if (onlyReduceInSize && imageW <= destW && imageH <= destH)
            {
                newW = imageW;
                newH = imageH;
            }
            else
            {
                const double imageRatio = imageH / (double) imageW;
                const double targetRatio = destH / (double) destW;

                if (imageRatio <= targetRatio)
                {
                    newW = destW;
                    newH = roundDoubleToInt (newW * imageRatio);
                }
                else
                {
                    newH = destH;
                    newW = roundDoubleToInt (newH / imageRatio);
                }
            }

            if (newW > 0 && newH > 0)
            {
                int newX = 0, newY = 0;

                justification.applyToRectangle (newX, newY, newW, newH,
                                                destX, destY, destW, destH);

                drawImage (imageToDraw,
                           newX, newY, newW, newH,
                           0, 0, imageW, imageH,
                           fillAlphaChannelWithCurrentBrush);
            }
        }
    }
}

void Graphics::drawImage (const Image* const imageToDraw,
                          int dx, int dy, int dw, int dh,
                          int sx, int sy, int sw, int sh,
                          const bool fillAlphaChannelWithCurrentBrush)
{
    if (imageToDraw == 0 || ! context->clipRegionIntersects  (dx, dy, dw, dh))
        return;

    if (sw == dw && sh == dh)
    {
        if (sx < 0)
        {
            dx -= sx;
            dw += sx;
            sw += sx;
            sx = 0;
        }

        if (sx + sw > imageToDraw->getWidth())
        {
            const int amount = sx + sw - imageToDraw->getWidth();
            dw -= amount;
            sw -= amount;
        }

        if (sy < 0)
        {
            dy -= sy;
            dh += sy;
            sh += sy;
            sy = 0;
        }

        if (sy + sh > imageToDraw->getHeight())
        {
            const int amount = sy + sh - imageToDraw->getHeight();
            dh -= amount;
            sh -= amount;
        }

        if (dw <= 0 || dh <= 0 || sw <= 0 || sh <= 0)
            return;

        if (fillAlphaChannelWithCurrentBrush)
        {
            getBrushToUse()->paintAlphaChannel (*context, *imageToDraw,
                                                dx - sx, dy - sy,
                                                dx, dy,
                                                dw, dh);
        }
        else
        {
            context->blendImage (*imageToDraw,
                                 dx, dy, dw, dh, sx, sy,
                                 colourBrush.getColour().getFloatAlpha());
        }
    }
    else
    {
        if (sx < 0)
        {
            dx -= sx;
            dw += sx;
            sw += sx;
            sx = 0;
        }

        if (sx + sw > imageToDraw->getWidth())
        {
            const int amount = sx + sw - imageToDraw->getWidth();
            dw -= amount;
            sw -= amount;
        }

        if (sy < 0)
        {
            dy -= sy;
            dh += sy;
            sh += sy;
            sy = 0;
        }

        if (sy + sh > imageToDraw->getHeight())
        {
            const int amount = sy + sh - imageToDraw->getHeight();
            dh -= amount;
            sh -= amount;
        }

        if (dw <= 0 || dh <= 0 || sw <= 0 || sh <= 0)
            return;

        if (fillAlphaChannelWithCurrentBrush)
        {
            if (imageToDraw->isRGB())
            {
                fillRect (dx, dy, dw, dh);
            }
            else
            {
                int tx = dx;
                int ty = dy;
                int tw = dw;
                int th = dh;

                if (context->getClipBounds().intersectRectangle (tx, ty, tw, th))
                {
                    Image temp (imageToDraw->getFormat(), tw, th, true);
                    Graphics g (temp);
                    g.setImageResamplingQuality (quality);
                    g.setOrigin (dx - tx, dy - ty);

                    g.drawImage (imageToDraw,
                                 0, 0, dw, dh,
                                 sx, sy, sw, sh,
                                 false);

                    getBrushToUse()->paintAlphaChannel (*context, temp, tx, ty, tx, ty, tw, th);
                }
            }
        }
        else
        {
            context->blendImageRescaling (*imageToDraw,
                                          dx, dy, dw, dh,
                                          sx, sy, sw, sh,
                                          colourBrush.getColour().getFloatAlpha(),
                                          quality);
        }
    }
}

void Graphics::drawImageTransformed (const Image* const imageToDraw,
                                     int sourceClipX,
                                     int sourceClipY,
                                     int sourceClipWidth,
                                     int sourceClipHeight,
                                     const AffineTransform& transform,
                                     const bool fillAlphaChannelWithCurrentBrush)
{
    if (imageToDraw != 0
         && (! context->isClipEmpty())
         && ! transform.isSingularity())
    {
        if (fillAlphaChannelWithCurrentBrush)
        {
            Path p;
            p.addRectangle ((float) sourceClipX, (float) sourceClipY,
                            (float) sourceClipWidth, (float) sourceClipHeight);

            p.applyTransform (transform);

            float dx, dy, dw, dh;
            p.getBounds (dx, dy, dw, dh);
            int tx = (int) dx;
            int ty = (int) dy;
            int tw = roundFloatToInt (dw) + 2;
            int th = roundFloatToInt (dh) + 2;

            if (context->getClipBounds().intersectRectangle (tx, ty, tw, th))
            {
                Image temp (imageToDraw->getFormat(), tw, th, true);
                Graphics g (temp);
                g.setImageResamplingQuality (quality);

                g.drawImageTransformed (imageToDraw,
                                        sourceClipX,
                                        sourceClipY,
                                        sourceClipWidth,
                                        sourceClipHeight,
                                        transform.translated ((float) -tx, (float) -ty),
                                        false);

                getBrushToUse()->paintAlphaChannel (*context, temp, tx, ty, tx, ty, tw, th);
            }
        }
        else
        {
            context->blendImageWarping (*imageToDraw,
                                        sourceClipX,
                                        sourceClipY,
                                        sourceClipWidth,
                                        sourceClipHeight,
                                        transform,
                                        colourBrush.getColour().getFloatAlpha(),
                                        quality);
        }
    }
}


END_JUCE_NAMESPACE
