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

   This file is part of the JUCE library.
   Copyright (c) 2022 - 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 7 End-User License
   Agreement and JUCE Privacy Policy.

   End User License Agreement: www.juce.com/juce-7-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
{

// this will throw an assertion if you try to draw something that's not
// possible in postscript
#define WARN_ABOUT_NON_POSTSCRIPT_OPERATIONS 0

//==============================================================================
#if JUCE_DEBUG && WARN_ABOUT_NON_POSTSCRIPT_OPERATIONS
 #define notPossibleInPostscriptAssert jassertfalse
#else
 #define notPossibleInPostscriptAssert
#endif

//==============================================================================
LowLevelGraphicsPostScriptRenderer::LowLevelGraphicsPostScriptRenderer (OutputStream& resultingPostScript,
                                                                        const String& documentTitle,
                                                                        const int totalWidth_,
                                                                        const int totalHeight_)
    : out (resultingPostScript),
      totalWidth (totalWidth_),
      totalHeight (totalHeight_),
      needToClip (true)
{
    stateStack.add (new SavedState());
    stateStack.getLast()->clip = Rectangle<int> (totalWidth_, totalHeight_);

    const float scale = jmin ((520.0f / (float) totalWidth_), (750.0f / (float) totalHeight));

    out << "%!PS-Adobe-3.0 EPSF-3.0"
           "\n%%BoundingBox: 0 0 600 824"
           "\n%%Pages: 0"
           "\n%%Creator: Raw Material Software Limited - JUCE"
           "\n%%Title: " << documentTitle <<
           "\n%%CreationDate: none"
           "\n%%LanguageLevel: 2"
           "\n%%EndComments"
           "\n%%BeginProlog"
           "\n%%BeginResource: JRes"
           "\n/bd {bind def} bind def"
           "\n/c {setrgbcolor} bd"
           "\n/m {moveto} bd"
           "\n/l {lineto} bd"
           "\n/rl {rlineto} bd"
           "\n/ct {curveto} bd"
           "\n/cp {closepath} bd"
           "\n/pr {3 index 3 index moveto 1 index 0 rlineto 0 1 index rlineto pop neg 0 rlineto pop pop closepath} bd"
           "\n/doclip {initclip newpath} bd"
           "\n/endclip {clip newpath} bd"
           "\n%%EndResource"
           "\n%%EndProlog"
           "\n%%BeginSetup"
           "\n%%EndSetup"
           "\n%%Page: 1 1"
           "\n%%BeginPageSetup"
           "\n%%EndPageSetup\n\n"
        << "40 800 translate\n"
        << scale << ' ' << scale << " scale\n\n";
}

//==============================================================================
bool LowLevelGraphicsPostScriptRenderer::isVectorDevice() const
{
    return true;
}

void LowLevelGraphicsPostScriptRenderer::setOrigin (Point<int> o)
{
    if (! o.isOrigin())
    {
        stateStack.getLast()->xOffset += o.x;
        stateStack.getLast()->yOffset += o.y;
        needToClip = true;
    }
}

void LowLevelGraphicsPostScriptRenderer::addTransform (const AffineTransform& /*transform*/)
{
    //xxx
    jassertfalse;
}

float LowLevelGraphicsPostScriptRenderer::getPhysicalPixelScaleFactor()    { return 1.0f; }

bool LowLevelGraphicsPostScriptRenderer::clipToRectangle (const Rectangle<int>& r)
{
    needToClip = true;
    return stateStack.getLast()->clip.clipTo (r.translated (stateStack.getLast()->xOffset, stateStack.getLast()->yOffset));
}

bool LowLevelGraphicsPostScriptRenderer::clipToRectangleList (const RectangleList<int>& clipRegion)
{
    needToClip = true;
    return stateStack.getLast()->clip.clipTo (clipRegion);
}

void LowLevelGraphicsPostScriptRenderer::excludeClipRectangle (const Rectangle<int>& r)
{
    needToClip = true;
    stateStack.getLast()->clip.subtract (r.translated (stateStack.getLast()->xOffset, stateStack.getLast()->yOffset));
}

void LowLevelGraphicsPostScriptRenderer::clipToPath (const Path& path, const AffineTransform& transform)
{
    writeClip();

    Path p (path);
    p.applyTransform (transform.translated ((float) stateStack.getLast()->xOffset, (float) stateStack.getLast()->yOffset));
    writePath (p);
    out << "clip\n";
}

void LowLevelGraphicsPostScriptRenderer::clipToImageAlpha (const Image& /*sourceImage*/, const AffineTransform& /*transform*/)
{
    needToClip = true;
    jassertfalse; // xxx
}

bool LowLevelGraphicsPostScriptRenderer::clipRegionIntersects (const Rectangle<int>& r)
{
    return stateStack.getLast()->clip.intersectsRectangle (r.translated (stateStack.getLast()->xOffset, stateStack.getLast()->yOffset));
}

Rectangle<int> LowLevelGraphicsPostScriptRenderer::getClipBounds() const
{
    return stateStack.getLast()->clip.getBounds().translated (-stateStack.getLast()->xOffset,
                                                              -stateStack.getLast()->yOffset);
}

bool LowLevelGraphicsPostScriptRenderer::isClipEmpty() const
{
    return stateStack.getLast()->clip.isEmpty();
}

//==============================================================================
LowLevelGraphicsPostScriptRenderer::SavedState::SavedState()
    : xOffset (0),
      yOffset (0)
{
}

void LowLevelGraphicsPostScriptRenderer::saveState()
{
    stateStack.add (new SavedState (*stateStack.getLast()));
}

void LowLevelGraphicsPostScriptRenderer::restoreState()
{
    jassert (stateStack.size() > 0);

    if (stateStack.size() > 0)
        stateStack.removeLast();
}

void LowLevelGraphicsPostScriptRenderer::beginTransparencyLayer (float)
{
}

void LowLevelGraphicsPostScriptRenderer::endTransparencyLayer()
{
}

//==============================================================================
void LowLevelGraphicsPostScriptRenderer::writeClip()
{
    if (needToClip)
    {
        needToClip = false;

        out << "doclip ";

        int itemsOnLine = 0;

        for (auto& i : stateStack.getLast()->clip)
        {
            if (++itemsOnLine == 6)
            {
                itemsOnLine = 0;
                out << '\n';
            }

            out << i.getX() << ' ' << -i.getY() << ' '
                << i.getWidth() << ' ' << -i.getHeight() << " pr ";
        }

        out << "endclip\n";
    }
}

void LowLevelGraphicsPostScriptRenderer::writeColour (Colour colour)
{
    Colour c (Colours::white.overlaidWith (colour));

    if (lastColour != c)
    {
        lastColour = c;

        out << String (c.getFloatRed(), 3) << ' '
            << String (c.getFloatGreen(), 3) << ' '
            << String (c.getFloatBlue(), 3) << " c\n";
    }
}

void LowLevelGraphicsPostScriptRenderer::writeXY (const float x, const float y) const
{
    out << String (x, 2) << ' '
        << String (-y, 2) << ' ';
}

void LowLevelGraphicsPostScriptRenderer::writePath (const Path& path) const
{
    out << "newpath ";

    float lastX = 0.0f;
    float lastY = 0.0f;
    int itemsOnLine = 0;

    Path::Iterator i (path);

    while (i.next())
    {
        if (++itemsOnLine == 4)
        {
            itemsOnLine = 0;
            out << '\n';
        }

        switch (i.elementType)
        {
        case Path::Iterator::startNewSubPath:
            writeXY (i.x1, i.y1);
            lastX = i.x1;
            lastY = i.y1;
            out << "m ";
            break;

        case Path::Iterator::lineTo:
            writeXY (i.x1, i.y1);
            lastX = i.x1;
            lastY = i.y1;
            out << "l ";
            break;

        case Path::Iterator::quadraticTo:
            {
                const float cp1x = lastX + (i.x1 - lastX) * 2.0f / 3.0f;
                const float cp1y = lastY + (i.y1 - lastY) * 2.0f / 3.0f;
                const float cp2x = cp1x + (i.x2 - lastX) / 3.0f;
                const float cp2y = cp1y + (i.y2 - lastY) / 3.0f;

                writeXY (cp1x, cp1y);
                writeXY (cp2x, cp2y);
                writeXY (i.x2, i.y2);
                out << "ct ";
                lastX = i.x2;
                lastY = i.y2;
            }
            break;

        case Path::Iterator::cubicTo:
            writeXY (i.x1, i.y1);
            writeXY (i.x2, i.y2);
            writeXY (i.x3, i.y3);
            out << "ct ";
            lastX = i.x3;
            lastY = i.y3;
            break;

        case Path::Iterator::closePath:
            out << "cp ";
            break;

        default:
            jassertfalse;
            break;
        }
    }

    out << '\n';
}

void LowLevelGraphicsPostScriptRenderer::writeTransform (const AffineTransform& trans) const
{
    out << "[ "
        << trans.mat00 << ' '
        << trans.mat10 << ' '
        << trans.mat01 << ' '
        << trans.mat11 << ' '
        << trans.mat02 << ' '
        << trans.mat12 << " ] concat ";
}

//==============================================================================
void LowLevelGraphicsPostScriptRenderer::setFill (const FillType& fillType)
{
    stateStack.getLast()->fillType = fillType;
}

void LowLevelGraphicsPostScriptRenderer::setOpacity (float /*opacity*/)
{
}

void LowLevelGraphicsPostScriptRenderer::setInterpolationQuality (Graphics::ResamplingQuality /*quality*/)
{
}

//==============================================================================
void LowLevelGraphicsPostScriptRenderer::fillRect (const Rectangle<int>& r, const bool /*replaceExistingContents*/)
{
    fillRect (r.toFloat());
}

void LowLevelGraphicsPostScriptRenderer::fillRect (const Rectangle<float>& r)
{
    if (stateStack.getLast()->fillType.isColour())
    {
        writeClip();
        writeColour (stateStack.getLast()->fillType.colour);

        auto r2 = r.translated ((float) stateStack.getLast()->xOffset,
                                (float) stateStack.getLast()->yOffset);

        out << r2.getX() << ' ' << -r2.getBottom() << ' ' << r2.getWidth() << ' ' << r2.getHeight() << " rectfill\n";
    }
    else
    {
        Path p;
        p.addRectangle (r);
        fillPath (p, AffineTransform());
    }
}

void LowLevelGraphicsPostScriptRenderer::fillRectList (const RectangleList<float>& list)
{
    fillPath (list.toPath(), AffineTransform());
}

//==============================================================================
void LowLevelGraphicsPostScriptRenderer::fillPath (const Path& path, const AffineTransform& t)
{
    if (stateStack.getLast()->fillType.isColour())
    {
        writeClip();

        Path p (path);
        p.applyTransform (t.translated ((float) stateStack.getLast()->xOffset,
                                        (float) stateStack.getLast()->yOffset));
        writePath (p);

        writeColour (stateStack.getLast()->fillType.colour);

        out << "fill\n";
    }
    else if (stateStack.getLast()->fillType.isGradient())
    {
        // this doesn't work correctly yet - it could be improved to handle solid gradients, but
        // postscript can't do semi-transparent ones.
        notPossibleInPostscriptAssert;   // you can disable this warning by setting the WARN_ABOUT_NON_POSTSCRIPT_OPERATIONS flag at the top of this file

        writeClip();
        out << "gsave ";

        {
            Path p (path);
            p.applyTransform (t.translated ((float) stateStack.getLast()->xOffset, (float) stateStack.getLast()->yOffset));
            writePath (p);
            out << "clip\n";
        }

        auto bounds = stateStack.getLast()->clip.getBounds();

        // ideally this would draw lots of lines or ellipses to approximate the gradient, but for the
        // time-being, this just fills it with the average colour..
        writeColour (stateStack.getLast()->fillType.gradient->getColourAtPosition (0.5f));
        out << bounds.getX() << ' ' << -bounds.getBottom() << ' ' << bounds.getWidth() << ' ' << bounds.getHeight() << " rectfill\n";

        out << "grestore\n";
    }
}

//==============================================================================
void LowLevelGraphicsPostScriptRenderer::writeImage (const Image& im,
                                                     const int sx, const int sy,
                                                     const int maxW, const int maxH) const
{
    out << "{<\n";

    const int w = jmin (maxW, im.getWidth());
    const int h = jmin (maxH, im.getHeight());

    int charsOnLine = 0;
    const Image::BitmapData srcData (im, 0, 0, w, h);
    Colour pixel;

    for (int y = h; --y >= 0;)
    {
        for (int x = 0; x < w; ++x)
        {
            const uint8* pixelData = srcData.getPixelPointer (x, y);

            if (x >= sx && y >= sy)
            {
                if (im.isARGB())
                {
                    PixelARGB p (*(const PixelARGB*) pixelData);
                    p.unpremultiply();
                    pixel = Colours::white.overlaidWith (Colour (p));
                }
                else if (im.isRGB())
                {
                    pixel = Colour (*((const PixelRGB*) pixelData));
                }
                else
                {
                    pixel = Colour ((uint8) 0, (uint8) 0, (uint8) 0, *pixelData);
                }
            }
            else
            {
                pixel = Colours::transparentWhite;
            }

            const uint8 pixelValues[3] = { pixel.getRed(), pixel.getGreen(), pixel.getBlue() };

            out << String::toHexString (pixelValues, 3, 0);
            charsOnLine += 3;

            if (charsOnLine > 100)
            {
                out << '\n';
                charsOnLine = 0;
            }
        }
    }

    out << "\n>}\n";
}

void LowLevelGraphicsPostScriptRenderer::drawImage (const Image& sourceImage, const AffineTransform& transform)
{
    const int w = sourceImage.getWidth();
    const int h = sourceImage.getHeight();

    writeClip();

    out << "gsave ";
    writeTransform (transform.translated ((float) stateStack.getLast()->xOffset, (float) stateStack.getLast()->yOffset)
                             .scaled (1.0f, -1.0f));

    RectangleList<int> imageClip;
    sourceImage.createSolidAreaMask (imageClip, 0.5f);

    out << "newpath ";
    int itemsOnLine = 0;

    for (auto& i : imageClip)
    {
        if (++itemsOnLine == 6)
        {
            out << '\n';
            itemsOnLine = 0;
        }

        out << i.getX() << ' ' << i.getY() << ' ' << i.getWidth() << ' ' << i.getHeight() << " pr ";
    }

    out << " clip newpath\n";

    out << w << ' ' << h << " scale\n";
    out << w << ' ' << h << " 8 [" << w << " 0 0 -" << h << ' ' << (int) 0 << ' ' << h << " ]\n";

    writeImage (sourceImage, 0, 0, w, h);

    out << "false 3 colorimage grestore\n";
    needToClip = true;
}


//==============================================================================
void LowLevelGraphicsPostScriptRenderer::drawLine (const Line <float>& line)
{
    Path p;
    p.addLineSegment (line, 1.0f);
    fillPath (p, AffineTransform());
}

//==============================================================================
void LowLevelGraphicsPostScriptRenderer::setFont (const Font& newFont)
{
    stateStack.getLast()->font = newFont;
}

const Font& LowLevelGraphicsPostScriptRenderer::getFont()
{
    return stateStack.getLast()->font;
}

void LowLevelGraphicsPostScriptRenderer::drawGlyph (int glyphNumber, const AffineTransform& transform)
{
    Path p;
    Font& font = stateStack.getLast()->font;
    font.getTypefacePtr()->getOutlineForGlyph (glyphNumber, p);
    fillPath (p, AffineTransform::scale (font.getHeight() * font.getHorizontalScale(), font.getHeight()).followedBy (transform));
}

} // namespace juce
