/*
 * 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 "src/pdf/SkPDFShader.h"

#include "include/core/SkBitmap.h"
#include "include/core/SkCanvas.h"
#include "include/core/SkImage.h"
#include "include/core/SkImageInfo.h"
#include "include/core/SkPaint.h"
#include "include/core/SkRefCnt.h"
#include "include/core/SkSamplingOptions.h"
#include "include/core/SkScalar.h"
#include "include/core/SkShader.h"
#include "include/core/SkSize.h"
#include "include/core/SkStream.h"
#include "include/core/SkSurface.h"
#include "include/core/SkTileMode.h"
#include "include/private/base/SkTPin.h"
#include "src/core/SkDevice.h"
#include "src/core/SkTHash.h"
#include "src/pdf/SkKeyedImage.h"
#include "src/pdf/SkPDFDevice.h"
#include "src/pdf/SkPDFDocumentPriv.h"
#include "src/pdf/SkPDFGradientShader.h"
#include "src/pdf/SkPDFUtils.h"
#include "src/shaders/SkShaderBase.h"

#include <memory>
#include <utility>

static void draw(SkCanvas* canvas, const SkImage* image, SkColor4f paintColor) {
    SkPaint paint(paintColor);
    canvas->drawImage(image, 0, 0, SkSamplingOptions(), &paint);
}

static SkBitmap to_bitmap(const SkImage* image) {
    SkBitmap bitmap;
    if (!SkPDFUtils::ToBitmap(image, &bitmap)) {
        bitmap.allocN32Pixels(image->width(), image->height());
        bitmap.eraseColor(0x00000000);
    }
    return bitmap;
}

static void draw_matrix(SkCanvas* canvas, const SkImage* image,
                        const SkMatrix& matrix, SkColor4f paintColor) {
    SkAutoCanvasRestore acr(canvas, true);
    canvas->concat(matrix);
    draw(canvas, image, paintColor);
}

static void draw_bitmap_matrix(SkCanvas* canvas, const SkBitmap& bm,
                               const SkMatrix& matrix, SkColor4f paintColor) {
    SkAutoCanvasRestore acr(canvas, true);
    canvas->concat(matrix);
    SkPaint paint(paintColor);
    canvas->drawImage(bm.asImage(), 0, 0, SkSamplingOptions(), &paint);
}

static void fill_color_from_bitmap(SkCanvas* canvas,
                                   float left, float top, float right, float bottom,
                                   const SkBitmap& bitmap, int x, int y, float alpha) {
    SkRect rect{left, top, right, bottom};
    if (!rect.isEmpty()) {
        SkColor4f color = SkColor4f::FromColor(bitmap.getColor(x, y));
        SkPaint paint(SkColor4f{color.fR, color.fG, color.fB, alpha * color.fA});
        canvas->drawRect(rect, paint);
    }
}

static SkMatrix scale_translate(SkScalar sx, SkScalar sy, SkScalar tx, SkScalar ty) {
    SkMatrix m;
    m.setScaleTranslate(sx, sy, tx, ty);
    return m;
}

static bool is_tiled(SkTileMode m) { return SkTileMode::kMirror == m || SkTileMode::kRepeat == m; }

static SkPDFIndirectReference make_image_shader(SkPDFDocument* doc,
                                                SkMatrix finalMatrix,
                                                SkTileMode tileModesX,
                                                SkTileMode tileModesY,
                                                SkRect bBox,
                                                const SkImage* image,
                                                SkColor4f paintColor) {
    // 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.

    SkRect deviceBounds = bBox;
    if (!SkPDFUtils::InverseTransformBBox(finalMatrix, &deviceBounds)) {
        return SkPDFIndirectReference();
    }

    SkRect bitmapBounds = SkRect::MakeSize(SkSize::Make(image->dimensions()));

    // 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.
    if (is_tiled(tileModesX) || is_tiled(tileModesY)) {
        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::MakeSize(SkSize::Make(image->dimensions()));
    SkScalar width = patternBBox.width();
    SkScalar height = patternBBox.height();

    // 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.
    draw(&canvas, image, paintColor);

    // Tiling is implied.  First we handle mirroring.
    if (tileModesX == SkTileMode::kMirror) {
        draw_matrix(&canvas, image, scale_translate(-1, 1, 2 * width, 0), paintColor);
        patternBBox.fRight += width;
    }
    if (tileModesY == SkTileMode::kMirror) {
        draw_matrix(&canvas, image, scale_translate(1, -1, 0, 2 * height), paintColor);
        patternBBox.fBottom += height;
    }
    if (tileModesX == SkTileMode::kMirror && tileModesY == SkTileMode::kMirror) {
        draw_matrix(&canvas, image, scale_translate(-1, -1, 2 * width, 2 * height), paintColor);
    }

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

    SkBitmap bitmap;
    if (tileModesX == SkTileMode::kClamp || tileModesY == SkTileMode::kClamp) {
        // For now, the easiest way to access the colors in the corners and sides is
        // to just make a bitmap from the image.
        bitmap = to_bitmap(image);
    }

    // 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 (tileModesX == SkTileMode::kClamp && tileModesY == SkTileMode::kClamp) {
        SkASSERT(!bitmap.drawsNothing());

        fill_color_from_bitmap(&canvas, deviceBounds.left(), deviceBounds.top(), 0, 0,
                               bitmap, 0, 0, paintColor.fA);

        fill_color_from_bitmap(&canvas, width, deviceBounds.top(), deviceBounds.right(), 0,
                               bitmap, bitmap.width() - 1, 0, paintColor.fA);

        fill_color_from_bitmap(&canvas, width, height, deviceBounds.right(), deviceBounds.bottom(),
                               bitmap, bitmap.width() - 1, bitmap.height() - 1, paintColor.fA);

        fill_color_from_bitmap(&canvas, deviceBounds.left(), height, 0, deviceBounds.bottom(),
                               bitmap, 0, bitmap.height() - 1, paintColor.fA);
    }

    // Then expand the left, right, top, then bottom.
    if (tileModesX == SkTileMode::kClamp) {
        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 = scale_translate(-deviceBounds.left(), 1, deviceBounds.left(), 0);
            draw_bitmap_matrix(&canvas, left, leftMatrix, paintColor);

            if (tileModesY == SkTileMode::kMirror) {
                leftMatrix.postScale(SK_Scalar1, -SK_Scalar1);
                leftMatrix.postTranslate(0, 2 * height);
                draw_bitmap_matrix(&canvas, left, leftMatrix, paintColor);
            }
            patternBBox.fLeft = 0;
        }

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

            SkMatrix rightMatrix = scale_translate(deviceBounds.right() - width, 1, width, 0);
            draw_bitmap_matrix(&canvas, right, rightMatrix, paintColor);

            if (tileModesY == SkTileMode::kMirror) {
                rightMatrix.postScale(SK_Scalar1, -SK_Scalar1);
                rightMatrix.postTranslate(0, 2 * height);
                draw_bitmap_matrix(&canvas, right, rightMatrix, paintColor);
            }
            patternBBox.fRight = deviceBounds.width();
        }
    }
    if (tileModesX == SkTileMode::kDecal) {
        if (deviceBounds.left() < 0) {
            patternBBox.fLeft = 0;
        }
        if (deviceBounds.right() > width) {
            patternBBox.fRight = deviceBounds.width();
        }
    }

    if (tileModesY == SkTileMode::kClamp) {
        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 = scale_translate(1, -deviceBounds.top(), 0, deviceBounds.top());
            draw_bitmap_matrix(&canvas, top, topMatrix, paintColor);

            if (tileModesX == SkTileMode::kMirror) {
                topMatrix.postScale(-1, 1);
                topMatrix.postTranslate(2 * width, 0);
                draw_bitmap_matrix(&canvas, top, topMatrix, paintColor);
            }
            patternBBox.fTop = 0;
        }

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

            SkMatrix bottomMatrix = scale_translate(1, deviceBounds.bottom() - height, 0, height);
            draw_bitmap_matrix(&canvas, bottom, bottomMatrix, paintColor);

            if (tileModesX == SkTileMode::kMirror) {
                bottomMatrix.postScale(-1, 1);
                bottomMatrix.postTranslate(2 * width, 0);
                draw_bitmap_matrix(&canvas, bottom, bottomMatrix, paintColor);
            }
            patternBBox.fBottom = deviceBounds.height();
        }
    }
    if (tileModesY == SkTileMode::kDecal) {
        if (deviceBounds.top() < 0) {
            patternBBox.fTop = 0;
        }
        if (deviceBounds.bottom() > height) {
            patternBBox.fBottom = deviceBounds.height();
        }
    }

    auto imageShader = patternDevice->content();
    std::unique_ptr<SkPDFDict> resourceDict = patternDevice->makeResourceDict();
    std::unique_ptr<SkPDFDict> dict = SkPDFMakeDict();
    SkPDFUtils::PopulateTilingPatternDict(dict.get(),
                                          patternBBox, is_tiled(tileModesX), is_tiled(tileModesY),
                                          std::move(resourceDict), finalMatrix);
    return SkPDFStreamOut(std::move(dict), std::move(imageShader), doc);
}

// Generic fallback for unsupported shaders:
//  * allocate a surfaceBBox-sized bitmap
//  * shade the whole area
//  * use the result as a bitmap shader
static SkPDFIndirectReference make_fallback_shader(SkPDFDocument* doc,
                                                   SkShader* shader,
                                                   const SkMatrix& canvasTransform,
                                                   const SkIRect& surfaceBBox,
                                                   SkColor4f paintColor) {
    // 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 SkPDFIndirectReference();
    }
    // Clamp the bitmap size to about 1M pixels
    static const int kMaxBitmapArea = 1024 * 1024;
    SkScalar bitmapArea = (float)surfaceBBox.width() * (float)surfaceBBox.height();
    SkScalar rasterScale = 1.0f;
    if (bitmapArea > (float)kMaxBitmapArea) {
        rasterScale *= SkScalarSqrt((float)kMaxBitmapArea / bitmapArea);
    }

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

    auto surface = SkSurfaces::Raster(SkImageInfo::MakeN32Premul(size.width(), size.height()));
    SkASSERT(surface);
    SkCanvas* canvas = surface->getCanvas();
    canvas->clear(SK_ColorTRANSPARENT);

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

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

    auto shaderTransform = SkMatrix::Translate(shaderRect.x(), shaderRect.y());
    shaderTransform.preScale(1 / scale.width(), 1 / scale.height());

    sk_sp<SkImage> image = surface->makeImageSnapshot();
    SkASSERT(image);
    return make_image_shader(doc,
                             SkMatrix::Concat(canvasTransform, shaderTransform),
                             SkTileMode::kClamp, SkTileMode::kClamp,
                             SkRect::Make(surfaceBBox),
                             image.get(),
                             paintColor);
}

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

SkPDFIndirectReference SkPDFMakeShader(SkPDFDocument* doc,
                                       SkShader* shader,
                                       const SkMatrix& canvasTransform,
                                       const SkIRect& surfaceBBox,
                                       SkColor4f paintColor) {
    SkASSERT(shader);
    SkASSERT(doc);
    if (as_SB(shader)->asGradient() != SkShaderBase::GradientType::kNone) {
        return SkPDFGradientShader::Make(doc, shader, canvasTransform, surfaceBBox);
    }
    if (surfaceBBox.isEmpty()) {
        return SkPDFIndirectReference();
    }
    SkBitmap image;

    paintColor = adjust_color(shader, paintColor);
    SkMatrix shaderTransform;
    SkTileMode imageTileModes[2];
    if (SkImage* skimg = shader->isAImage(&shaderTransform, imageTileModes)) {
        SkMatrix finalMatrix = SkMatrix::Concat(canvasTransform, shaderTransform);
        SkPDFImageShaderKey key = {
            finalMatrix,
            surfaceBBox,
            SkBitmapKeyFromImage(skimg),
            {imageTileModes[0], imageTileModes[1]},
            paintColor};
        SkPDFIndirectReference* shaderPtr = doc->fImageShaderMap.find(key);
        if (shaderPtr) {
            return *shaderPtr;
        }
        SkPDFIndirectReference pdfShader =
                make_image_shader(doc,
                                  finalMatrix,
                                  imageTileModes[0],
                                  imageTileModes[1],
                                  SkRect::Make(surfaceBBox),
                                  skimg,
                                  paintColor);
        doc->fImageShaderMap.set(std::move(key), pdfShader);
        return pdfShader;
    }
    // Don't bother to de-dup fallback shader.
    return make_fallback_shader(doc, shader, canvasTransform, surfaceBBox, paintColor);
}
