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

#include "include/core/SkBBHFactory.h"
#include "include/core/SkBitmap.h"
#include "include/core/SkBlendMode.h"
#include "include/core/SkCanvas.h"
#include "include/core/SkImage.h"
#include "include/core/SkImageInfo.h"
#include "include/core/SkPaint.h"
#include "include/core/SkPicture.h"
#include "include/core/SkPictureRecorder.h"
#include "include/core/SkRect.h"
#include "include/core/SkRefCnt.h"
#include "include/core/SkSamplingOptions.h"
#include "include/core/SkScalar.h"
#include "include/core/SkString.h"
#include "include/core/SkSurface.h"
#include "tests/Test.h"

#include <cstring>

// Verify that replay of a recording into a clipped canvas
// produces the correct bitmap.
// This arose from http://crbug.com/401593 which has
// https://code.google.com/p/skia/issues/detail?id=1291 as its root cause.

namespace {

class Drawer {
 public:
    explicit Drawer() : fImageInfo(SkImageInfo::MakeN32Premul(200, 100)) {
        auto surf = SkSurfaces::Raster(SkImageInfo::MakeN32Premul(100, 100));
        surf->getCanvas()->clear(0xffffffff);
        SkPaint circlePaint;
        circlePaint.setColor(0xff000000);
        surf->getCanvas()->drawCircle(50, 50, 50, circlePaint);
        fCircleImage = surf->makeImageSnapshot();
    }

    const SkImageInfo& imageInfo() const { return fImageInfo; }

    void draw(SkCanvas* canvas, const SkRect& clipRect, SkBlendMode mode) const {
        SkPaint greenPaint;
        greenPaint.setColor(0xff008000);
        SkPaint blackPaint;
        blackPaint.setColor(0xff000000);
        SkPaint whitePaint;
        whitePaint.setColor(0xffffffff);
        SkPaint layerPaint;
        layerPaint.setColor(0xff000000);
        layerPaint.setBlendMode(mode);
        SkRect canvasRect(SkRect::MakeWH(SkIntToScalar(fImageInfo.width()),
                                         SkIntToScalar(fImageInfo.height())));

        canvas->clipRect(clipRect);
        canvas->clear(0xff000000);

        canvas->saveLayer(nullptr, &blackPaint);
            canvas->drawRect(canvasRect, greenPaint);
            canvas->saveLayer(nullptr, &layerPaint);
                canvas->drawImageRect(fCircleImage, SkRect::MakeXYWH(20,20,60,60),
                                      SkSamplingOptions(), &blackPaint);
            canvas->restore();
        canvas->restore();
    }

 private:
    const SkImageInfo fImageInfo;
    sk_sp<SkImage> fCircleImage;
};

class RecordingStrategy {
 public:
    virtual ~RecordingStrategy() {}
    virtual const SkBitmap& recordAndReplay(const Drawer& drawer,
                                            const SkRect& intoClip,
                                            SkBlendMode) = 0;
};

class BitmapBackedCanvasStrategy : public RecordingStrategy {
    // This version just draws into a bitmap-backed canvas.
 public:
    BitmapBackedCanvasStrategy(const SkImageInfo& imageInfo) {
        fBitmap.allocPixels(imageInfo);
    }

    const SkBitmap& recordAndReplay(const Drawer& drawer, const SkRect& intoClip,
                                    SkBlendMode mode) override {
        SkCanvas canvas(fBitmap);
        canvas.clear(0xffffffff);
        // Note that the scene is drawn just into the clipped region!
        canvas.clipRect(intoClip);
        drawer.draw(&canvas, intoClip, mode); // Shouild be canvas-wide...
        return fBitmap;
    }

 private:
    SkBitmap fBitmap;
};

class PictureStrategy : public RecordingStrategy {
    // This version draws the entire scene into an SkPictureRecorder.
    // Then it then replays the scene through a clip rectangle.
    // This backend proved to be buggy.
 public:
    PictureStrategy(const SkImageInfo& imageInfo) {
        fBitmap.allocPixels(imageInfo);
        fWidth  = imageInfo.width();
        fHeight = imageInfo.height();
    }

    const SkBitmap& recordAndReplay(const Drawer& drawer, const SkRect& intoClip,
                                    SkBlendMode mode) override {
        SkRTreeFactory factory;
        SkPictureRecorder recorder;
        SkRect canvasRect(SkRect::MakeWH(SkIntToScalar(fWidth),SkIntToScalar(fHeight)));
        SkCanvas* canvas = recorder.beginRecording(SkIntToScalar(fWidth),
                                                   SkIntToScalar(fHeight),
                                                   &factory);
        drawer.draw(canvas, canvasRect, mode);
        sk_sp<SkPicture> picture(recorder.finishRecordingAsPicture());

        SkCanvas replayCanvas(fBitmap);
        replayCanvas.clear(0xffffffff);
        replayCanvas.clipRect(intoClip);
        picture->playback(&replayCanvas);
        return fBitmap;
    }

 private:
    SkBitmap fBitmap;
    int fWidth;
    int fHeight;
};

} // namespace


DEF_TEST(SkRecordingAccuracyXfermode, reporter) {
#define FINEGRAIN 0
    const Drawer drawer;

    BitmapBackedCanvasStrategy golden(drawer.imageInfo());
    PictureStrategy picture(drawer.imageInfo());

#if !FINEGRAIN
    unsigned numErrors = 0;
    SkString errors;
#endif

    for (int iMode = 0; iMode < kSkBlendModeCount; iMode++) {
        const SkRect& clip = SkRect::MakeXYWH(100, 0, 100, 100);
        SkBlendMode mode = SkBlendMode(iMode);

        const SkBitmap& goldenBM = golden.recordAndReplay(drawer, clip, mode);
        const SkBitmap& pictureBM = picture.recordAndReplay(drawer, clip, mode);

        size_t pixelsSize = goldenBM.computeByteSize();
        REPORTER_ASSERT(reporter, pixelsSize == pictureBM.computeByteSize());

        // The pixel arrays should match.
#if FINEGRAIN
        REPORTER_ASSERT(reporter,
                        0 == memcmp(goldenBM.getPixels(), pictureBM.getPixels(), pixelsSize));
#else
        if (0 != memcmp(goldenBM.getPixels(), pictureBM.getPixels(), pixelsSize)) {
            numErrors++;
            errors.appendf("For SkBlendMode %d %s:    SkPictureRecorder bitmap is wrong\n",
                           iMode, SkBlendMode_Name(mode));
        }
#endif
    }

#if !FINEGRAIN
    REPORTER_ASSERT(reporter, 0 == numErrors, "%s", errors.c_str());
#endif
}
