/*
 * Copyright 2011 Google Inc.
 *
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */


#include "SkPDFShader.h"

#include "SkData.h"
#include "SkPDFCanon.h"
#include "SkPDFDevice.h"
#include "SkPDFDocument.h"
#include "SkPDFFormXObject.h"
#include "SkPDFGradientShader.h"
#include "SkPDFGraphicState.h"
#include "SkPDFResourceDict.h"
#include "SkPDFUtils.h"
#include "SkScalar.h"
#include "SkStream.h"
#include "SkSurface.h"
#include "SkTemplates.h"


static void draw_image_matrix(SkCanvas* canvas, const SkImage* img,
                              const SkMatrix& matrix, const SkPaint& paint) {
    SkAutoCanvasRestore acr(canvas, true);
    canvas->concat(matrix);
    canvas->drawImage(img, 0, 0, &paint);
}

static void draw_bitmap_matrix(SkCanvas* canvas, const SkBitmap& bm,
                               const SkMatrix& matrix, const SkPaint& paint) {
    SkAutoCanvasRestore acr(canvas, true);
    canvas->concat(matrix);
    canvas->drawBitmap(bm, 0, 0, &paint);
}

static sk_sp<SkPDFStream> make_image_shader(SkPDFDocument* doc,
                                            const SkPDFImageShaderKey& key,
                                            SkImage* image) {
    SkASSERT(image);

    // The image shader pattern cell will be drawn into a separate device
    // in pattern cell space (no scaling on the bitmap, though there may be
    // translations so that all content is in the device, coordinates > 0).

    // Map clip bounds to shader space to ensure the device is large enough
    // to handle fake clamping.
    SkMatrix finalMatrix = key.fCanvasTransform;
    finalMatrix.preConcat(key.fShaderTransform);
    SkRect deviceBounds = SkRect::Make(key.fBBox);
    if (!SkPDFUtils::InverseTransformBBox(finalMatrix, &deviceBounds)) {
        return nullptr;
    }

    SkRect bitmapBounds = SkRect::Make(image->bounds());

    // For tiling modes, the bounds should be extended to include the bitmap,
    // otherwise the bitmap gets clipped out and the shader is empty and awful.
    // For clamp modes, we're only interested in the clip region, whether
    // or not the main bitmap is in it.
    SkShader::TileMode tileModes[2];
    tileModes[0] = key.fImageTileModes[0];
    tileModes[1] = key.fImageTileModes[1];
    if (tileModes[0] != SkShader::kClamp_TileMode ||
            tileModes[1] != SkShader::kClamp_TileMode) {
        deviceBounds.join(bitmapBounds);
    }

    SkISize patternDeviceSize = {SkScalarCeilToInt(deviceBounds.width()),
                                 SkScalarCeilToInt(deviceBounds.height())};
    auto patternDevice = sk_make_sp<SkPDFDevice>(patternDeviceSize, doc);
    SkCanvas canvas(patternDevice);

    SkRect patternBBox = SkRect::Make(image->bounds());

    // Translate the canvas so that the bitmap origin is at (0, 0).
    canvas.translate(-deviceBounds.left(), -deviceBounds.top());
    patternBBox.offset(-deviceBounds.left(), -deviceBounds.top());
    // Undo the translation in the final matrix
    finalMatrix.preTranslate(deviceBounds.left(), deviceBounds.top());

    // If the bitmap is out of bounds (i.e. clamp mode where we only see the
    // stretched sides), canvas will clip this out and the extraneous data
    // won't be saved to the PDF.
    canvas.drawImage(image, 0, 0);

    SkScalar width = SkIntToScalar(image->width());
    SkScalar height = SkIntToScalar(image->height());

    SkPaint paint;
    paint.setColor(key.fPaintColor);
    // Tiling is implied.  First we handle mirroring.
    if (tileModes[0] == SkShader::kMirror_TileMode) {
        SkMatrix xMirror;
        xMirror.setScale(-1, 1);
        xMirror.postTranslate(2 * width, 0);
        draw_image_matrix(&canvas, image, xMirror, paint);
        patternBBox.fRight += width;
    }
    if (tileModes[1] == SkShader::kMirror_TileMode) {
        SkMatrix yMirror;
        yMirror.setScale(SK_Scalar1, -SK_Scalar1);
        yMirror.postTranslate(0, 2 * height);
        draw_image_matrix(&canvas, image, yMirror, paint);
        patternBBox.fBottom += height;
    }
    if (tileModes[0] == SkShader::kMirror_TileMode &&
            tileModes[1] == SkShader::kMirror_TileMode) {
        SkMatrix mirror;
        mirror.setScale(-1, -1);
        mirror.postTranslate(2 * width, 2 * height);
        draw_image_matrix(&canvas, image, mirror, paint);
    }

    // Then handle Clamping, which requires expanding the pattern canvas to
    // cover the entire surfaceBBox.

    SkBitmap bitmap;
    if (tileModes[0] == SkShader::kClamp_TileMode ||
        tileModes[1] == SkShader::kClamp_TileMode) {
        // For now, the easiest way to access the colors in the corners and sides is
        // to just make a bitmap from the image.
        if (!SkPDFUtils::ToBitmap(image, &bitmap)) {
            bitmap.allocN32Pixels(image->width(), image->height());
            bitmap.eraseColor(0x00000000);
        }
    }

    // If both x and y are in clamp mode, we start by filling in the corners.
    // (Which are just a rectangles of the corner colors.)
    if (tileModes[0] == SkShader::kClamp_TileMode &&
            tileModes[1] == SkShader::kClamp_TileMode) {
        SkASSERT(!bitmap.drawsNothing());
        SkPaint paint;
        SkRect rect;
        rect = SkRect::MakeLTRB(deviceBounds.left(), deviceBounds.top(), 0, 0);
        if (!rect.isEmpty()) {
            paint.setColor(bitmap.getColor(0, 0));
            canvas.drawRect(rect, paint);
        }

        rect = SkRect::MakeLTRB(width, deviceBounds.top(),
                                deviceBounds.right(), 0);
        if (!rect.isEmpty()) {
            paint.setColor(bitmap.getColor(bitmap.width() - 1, 0));
            canvas.drawRect(rect, paint);
        }

        rect = SkRect::MakeLTRB(width, height,
                                deviceBounds.right(), deviceBounds.bottom());
        if (!rect.isEmpty()) {
            paint.setColor(bitmap.getColor(bitmap.width() - 1,
                                           bitmap.height() - 1));
            canvas.drawRect(rect, paint);
        }

        rect = SkRect::MakeLTRB(deviceBounds.left(), height,
                                0, deviceBounds.bottom());
        if (!rect.isEmpty()) {
            paint.setColor(bitmap.getColor(0, bitmap.height() - 1));
            canvas.drawRect(rect, paint);
        }
    }

    // Then expand the left, right, top, then bottom.
    if (tileModes[0] == SkShader::kClamp_TileMode) {
        SkASSERT(!bitmap.drawsNothing());
        SkIRect subset = SkIRect::MakeXYWH(0, 0, 1, bitmap.height());
        if (deviceBounds.left() < 0) {
            SkBitmap left;
            SkAssertResult(bitmap.extractSubset(&left, subset));

            SkMatrix leftMatrix;
            leftMatrix.setScale(-deviceBounds.left(), 1);
            leftMatrix.postTranslate(deviceBounds.left(), 0);
            draw_bitmap_matrix(&canvas, left, leftMatrix, paint);

            if (tileModes[1] == SkShader::kMirror_TileMode) {
                leftMatrix.postScale(SK_Scalar1, -SK_Scalar1);
                leftMatrix.postTranslate(0, 2 * height);
                draw_bitmap_matrix(&canvas, left, leftMatrix, paint);
            }
            patternBBox.fLeft = 0;
        }

        if (deviceBounds.right() > width) {
            SkBitmap right;
            subset.offset(bitmap.width() - 1, 0);
            SkAssertResult(bitmap.extractSubset(&right, subset));

            SkMatrix rightMatrix;
            rightMatrix.setScale(deviceBounds.right() - width, 1);
            rightMatrix.postTranslate(width, 0);
            draw_bitmap_matrix(&canvas, right, rightMatrix, paint);

            if (tileModes[1] == SkShader::kMirror_TileMode) {
                rightMatrix.postScale(SK_Scalar1, -SK_Scalar1);
                rightMatrix.postTranslate(0, 2 * height);
                draw_bitmap_matrix(&canvas, right, rightMatrix, paint);
            }
            patternBBox.fRight = deviceBounds.width();
        }
    }

    if (tileModes[1] == SkShader::kClamp_TileMode) {
        SkASSERT(!bitmap.drawsNothing());
        SkIRect subset = SkIRect::MakeXYWH(0, 0, bitmap.width(), 1);
        if (deviceBounds.top() < 0) {
            SkBitmap top;
            SkAssertResult(bitmap.extractSubset(&top, subset));

            SkMatrix topMatrix;
            topMatrix.setScale(SK_Scalar1, -deviceBounds.top());
            topMatrix.postTranslate(0, deviceBounds.top());
            draw_bitmap_matrix(&canvas, top, topMatrix, paint);

            if (tileModes[0] == SkShader::kMirror_TileMode) {
                topMatrix.postScale(-1, 1);
                topMatrix.postTranslate(2 * width, 0);
                draw_bitmap_matrix(&canvas, top, topMatrix, paint);
            }
            patternBBox.fTop = 0;
        }

        if (deviceBounds.bottom() > height) {
            SkBitmap bottom;
            subset.offset(0, bitmap.height() - 1);
            SkAssertResult(bitmap.extractSubset(&bottom, subset));

            SkMatrix bottomMatrix;
            bottomMatrix.setScale(SK_Scalar1, deviceBounds.bottom() - height);
            bottomMatrix.postTranslate(0, height);
            draw_bitmap_matrix(&canvas, bottom, bottomMatrix, paint);

            if (tileModes[0] == SkShader::kMirror_TileMode) {
                bottomMatrix.postScale(-1, 1);
                bottomMatrix.postTranslate(2 * width, 0);
                draw_bitmap_matrix(&canvas, bottom, bottomMatrix, paint);
            }
            patternBBox.fBottom = deviceBounds.height();
        }
    }

    auto imageShader = sk_make_sp<SkPDFStream>(patternDevice->content());
    SkPDFUtils::PopulateTilingPatternDict(imageShader->dict(), patternBBox,
                                          patternDevice->makeResourceDict(), finalMatrix);
    return imageShader;
}

// Generic fallback for unsupported shaders:
//  * allocate a surfaceBBox-sized bitmap
//  * shade the whole area
//  * use the result as a bitmap shader
static sk_sp<SkPDFObject> make_fallback_shader(SkPDFDocument* doc,
                                               SkShader* shader,
                                               const SkMatrix& canvasTransform,
                                               const SkIRect& surfaceBBox,
                                               SkColor paintColor) {
    // TODO(vandebo) This drops SKComposeShader on the floor.  We could
    // handle compose shader by pulling things up to a layer, drawing with
    // the first shader, applying the xfer mode and drawing again with the
    // second shader, then applying the layer to the original drawing.
    SkPDFImageShaderKey key = {
        canvasTransform,
        SkMatrix::I(),
        surfaceBBox,
        {{0, 0, 0, 0}, 0},  // don't need the key; won't de-dup.
        {SkShader::kClamp_TileMode, SkShader::kClamp_TileMode},
        paintColor};

    key.fShaderTransform = shader->getLocalMatrix();

    // surfaceBBox is in device space. While that's exactly what we
    // want for sizing our bitmap, we need to map it into
    // shader space for adjustments (to match
    // MakeImageShader's behavior).
    SkRect shaderRect = SkRect::Make(surfaceBBox);
    if (!SkPDFUtils::InverseTransformBBox(canvasTransform, &shaderRect)) {
        return nullptr;
    }
    // Clamp the bitmap size to about 1M pixels
    static const SkScalar kMaxBitmapArea = 1024 * 1024;
    SkScalar bitmapArea = surfaceBBox.width() * surfaceBBox.height();
    SkScalar rasterScale = 1.0f;
    if (bitmapArea > kMaxBitmapArea) {
        rasterScale *= SkScalarSqrt(kMaxBitmapArea / bitmapArea);
    }

    SkISize size = {SkScalarRoundToInt(rasterScale * surfaceBBox.width()),
                    SkScalarRoundToInt(rasterScale * surfaceBBox.height())};
    SkSize scale = {SkIntToScalar(size.width()) / shaderRect.width(),
                    SkIntToScalar(size.height()) / shaderRect.height()};

    auto surface = SkSurface::MakeRasterN32Premul(size.width(), size.height());
    SkCanvas* canvas = surface->getCanvas();
    canvas->clear(SK_ColorTRANSPARENT);

    SkPaint p;
    p.setShader(sk_ref_sp(shader));
    p.setColor(paintColor);

    canvas->scale(scale.width(), scale.height());
    canvas->translate(-shaderRect.x(), -shaderRect.y());
    canvas->drawPaint(p);

    key.fShaderTransform.setTranslate(shaderRect.x(), shaderRect.y());
    key.fShaderTransform.preScale(1 / scale.width(), 1 / scale.height());

    sk_sp<SkImage> image = surface->makeImageSnapshot();
    return make_image_shader(doc, key, image.get());
}

static SkColor adjust_color(SkShader* shader, SkColor paintColor) {
    if (SkImage* img = shader->isAImage(nullptr, nullptr)) {
        if (img->isAlphaOnly()) {
            return paintColor;
        }
    }
    // only preserve the alpha.
    return paintColor & SK_ColorBLACK;
}

sk_sp<SkPDFObject> SkPDFMakeShader(SkPDFDocument* doc,
                                  SkShader* shader,
                                  const SkMatrix& canvasTransform,
                                  const SkIRect& surfaceBBox,
                                  SkColor paintColor) {
    SkASSERT(shader);
    SkASSERT(doc);
    if (SkShader::kNone_GradientType != shader->asAGradient(nullptr)) {
        return SkPDFGradientShader::Make(doc, shader, canvasTransform, surfaceBBox);
    }
    if (surfaceBBox.isEmpty()) {
        return nullptr;
    }
    SkBitmap image;
    SkPDFImageShaderKey key = {
        canvasTransform,
        SkMatrix::I(),
        surfaceBBox,
        {{0, 0, 0, 0}, 0},
        {SkShader::kClamp_TileMode, SkShader::kClamp_TileMode},
        adjust_color(shader, paintColor)};

    SkASSERT(shader->asAGradient(nullptr) == SkShader::kNone_GradientType) ;
    if (SkImage* skimg = shader->isAImage(&key.fShaderTransform, key.fImageTileModes)) {
        key.fBitmapKey = SkBitmapKeyFromImage(skimg);
        SkPDFCanon* canon = doc->canon();
        sk_sp<SkPDFObject>* shaderPtr = canon->fImageShaderMap.find(key);
        if (shaderPtr) {
            return *shaderPtr;
        }
        sk_sp<SkPDFObject> pdfShader = make_image_shader(doc, key, skimg);
        canon->fImageShaderMap.set(std::move(key), pdfShader);
        return pdfShader;
    }
    // Don't bother to de-dup fallback shader.
    return make_fallback_shader(doc, shader, canvasTransform, surfaceBBox, key.fPaintColor);
}
