/*
 * 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/SkCanvas.h"
#include "include/core/SkPicture.h"
#include "src/core/SkPictureFlat.h"
#include "src/core/SkPictureRecord.h"
#include "tests/Test.h"
#include "tools/debugger/DebugCanvas.h"

// This test exercises the Matrix/Clip State collapsing system. It generates
// example skps and the compares the actual stored operations to the expected
// operations. The test works by emitting canvas operations at three levels:
// overall structure, bodies that draw something and model/clip state changes.
//
// Structure methods only directly emit save and restores but call the
// ModelClip and Body helper methods to fill in the structure. Since they only
// emit saves and restores the operations emitted by the structure methods will
// be completely removed by the matrix/clip collapse. Note: every save in
// a structure method is followed by a call to a ModelClip helper.
//
// Body methods only directly emit draw ops and saveLayer/restore pairs but call
// the ModelClip helper methods. Since the body methods emit the ops that cannot
// be collapsed (i.e., draw ops, saveLayer/restore) they also generate the
// expected result information. Note: every saveLayer in a body method is
// followed by a call to a ModelClip helper.
//
// The ModelClip methods output matrix and clip ops in various orders and
// combinations. They contribute to the expected result by outputting the
// expected matrix & clip ops. Note that, currently, the entire clip stack
// is output for each MC state so the clip operations accumulate down the
// save/restore stack.

// TODOs:
//   check on clip offsets
//      - not sure if this is possible. The desire is to verify that the clip
//        operations' offsets point to the correct follow-on operations. This
//        could be difficult since there is no good way to communicate the
//        offset stored in the SkPicture to the debugger's clip objects
//   add comparison of rendered before & after images?
//      - not sure if this would be useful since it somewhat duplicates the
//        correctness test of running render_pictures in record mode and
//        rendering before and after images. Additionally the matrix/clip collapse
//        is sure to cause some small differences so an automated test might
//        yield too many false positives.
//   run the matrix/clip collapse system on the 10K skp set
//      - this should give us warm fuzzies that the matrix clip collapse
//        system is ready for prime time
//   bench the recording times with/without matrix/clip collapsing

#ifdef SK_COLLAPSE_MATRIX_CLIP_STATE

// Enable/disable debugging helper code
//#define TEST_COLLAPSE_MATRIX_CLIP_STATE 1

// Extract the command ops from the input SkPicture
static void gets_ops(SkPicture& input, SkTDArray<DrawType>* ops) {
    DebugCanvas debugCanvas(input.width(), input.height());
    debugCanvas.setBounds(input.width(), input.height());
    input.draw(&debugCanvas);

    ops->setCount(debugCanvas.getSize());
    for (int i = 0; i < debugCanvas.getSize(); ++i) {
        (*ops)[i] = debugCanvas.getDrawCommandAt(i)->getType();
    }
}

enum ClipType {
    kNone_ClipType,
    kRect_ClipType,
    kRRect_ClipType,
    kPath_ClipType,
    kRegion_ClipType,

    kLast_ClipType = kRRect_ClipType
};

static const int kClipTypeCount = kLast_ClipType + 1;

enum MatType {
    kNone_MatType,
    kTranslate_MatType,
    kScale_MatType,
    kSkew_MatType,
    kRotate_MatType,
    kConcat_MatType,
    kSetMatrix_MatType,

    kLast_MatType = kScale_MatType
};

static const int kMatTypeCount = kLast_MatType + 1;

// TODO: implement the rest of the draw ops
enum DrawOpType {
    kNone_DrawOpType,
#if 0
    kBitmap_DrawOpType,
    kBitmapMatrix_DrawOpType,
    kBitmapNone_DrawOpType,
    kBitmapRectToRect_DrawOpType,
#endif
    kClear_DrawOpType,
#if 0
    kData_DrawOpType,
#endif
    kOval_DrawOpType,
#if 0
    kPaint_DrawOpType,
    kPath_DrawOpType,
    kPicture_DrawOpType,
    kPoints_DrawOpType,
    kPosText_DrawOpType,
    kPosTextTopBottom_DrawOpType,
    kPosTextH_DrawOpType,
    kPosTextHTopBottom_DrawOpType,
#endif
    kRect_DrawOpType,
    kRRect_DrawOpType,
#if 0
    kSprite_DrawOpType,
    kText_DrawOpType,
    kTextOnPath_DrawOpType,
    kTextTopBottom_DrawOpType,
    kDrawVertices_DrawOpType,
#endif

    kLastNonSaveLayer_DrawOpType = kRect_DrawOpType,

    // saveLayer's have to handled apart from the other draw operations
    // since they also alter the save/restore structure.
    kSaveLayer_DrawOpType,
};

static const int kNonSaveLayerDrawOpTypeCount = kLastNonSaveLayer_DrawOpType + 1;

typedef void (*PFEmitMC)(SkCanvas* canvas, MatType mat, ClipType clip,
                         DrawOpType draw, SkTDArray<DrawType>* expected,
                         int accumulatedClips);
typedef void (*PFEmitBody)(SkCanvas* canvas, PFEmitMC emitMC, MatType mat,
                           ClipType clip, DrawOpType draw,
                           SkTDArray<DrawType>* expected, int accumulatedClips);
typedef void (*PFEmitStruct)(SkCanvas* canvas, PFEmitMC emitMC, MatType mat,
                             ClipType clip, PFEmitBody emitBody, DrawOpType draw,
                             SkTDArray<DrawType>* expected);

//////////////////////////////////////////////////////////////////////////////

// TODO: expand the testing to include the different ops & AA types!
static void emit_clip(SkCanvas* canvas, ClipType clip) {
    switch (clip) {
        case kNone_ClipType:
            break;
        case kRect_ClipType: {
            SkRect r = SkRect::MakeLTRB(10, 10, 90, 90);
            canvas->clipRect(r, SkRegion::kIntersect_Op, true);
            break;
        }
        case kRRect_ClipType: {
            SkRect r = SkRect::MakeLTRB(10, 10, 90, 90);
            SkRRect rr;
            rr.setRectXY(r, 10, 10);
            canvas->clipRRect(rr, SkRegion::kIntersect_Op, true);
            break;
        }
        case kPath_ClipType: {
            SkPath p;
            p.moveTo(5.0f, 5.0f);
            p.lineTo(50.0f, 50.0f);
            p.lineTo(100.0f, 5.0f);
            p.close();
            canvas->clipPath(p, SkRegion::kIntersect_Op, true);
            break;
        }
        case kRegion_ClipType: {
            SkIRect rects[2] = {
                { 1, 1, 55, 55 },
                { 45, 45, 99, 99 },
            };
            SkRegion r;
            r.setRects(rects, 2);
            canvas->clipRegion(r, SkRegion::kIntersect_Op);
            break;
        }
        default:
            SkASSERT(0);
    }
}

static void add_clip(ClipType clip, MatType mat, SkTDArray<DrawType>* expected) {
    if (nullptr == expected) {
        // expected is nullptr if this clip will be fused into later clips
        return;
    }

    switch (clip) {
        case kNone_ClipType:
            break;
        case kRect_ClipType:
            *expected->append() = CONCAT;
            *expected->append() = CLIP_RECT;
            break;
        case kRRect_ClipType:
            *expected->append() = CONCAT;
            *expected->append() = CLIP_RRECT;
            break;
        case kPath_ClipType:
            *expected->append() = CONCAT;
            *expected->append() = CLIP_PATH;
            break;
        case kRegion_ClipType:
            *expected->append() = CONCAT;
            *expected->append() = CLIP_REGION;
            break;
        default:
            SkASSERT(0);
    }
}

static void emit_mat(SkCanvas* canvas, MatType mat) {
    switch (mat) {
    case kNone_MatType:
        break;
    case kTranslate_MatType:
        canvas->translate(5.0f, 5.0f);
        break;
    case kScale_MatType:
        canvas->scale(1.1f, 1.1f);
        break;
    case kSkew_MatType:
        canvas->skew(1.1f, 1.1f);
        break;
    case kRotate_MatType:
        canvas->rotate(1.0f);
        break;
    case kConcat_MatType: {
        SkMatrix m;
        m.setTranslate(1.0f, 1.0f);
        canvas->concat(m);
        break;
    }
    case kSetMatrix_MatType: {
        SkMatrix m;
        m.setTranslate(1.0f, 1.0f);
        canvas->setMatrix(m);
        break;
    }
    default:
        SkASSERT(0);
    }
}

static void add_mat(MatType mat, SkTDArray<DrawType>* expected) {
    if (nullptr == expected) {
        // expected is nullptr if this matrix call will be fused into later ones
        return;
    }

    switch (mat) {
    case kNone_MatType:
        break;
    case kTranslate_MatType:    // fall thru
    case kScale_MatType:        // fall thru
    case kSkew_MatType:         // fall thru
    case kRotate_MatType:       // fall thru
    case kConcat_MatType:       // fall thru
    case kSetMatrix_MatType:
        // TODO: this system currently converts a setMatrix to concat. If we wanted to
        // really preserve the setMatrix semantics we should keep it a setMatrix. I'm
        // not sure if this is a good idea though since this would keep things like pinch
        // zoom from working.
        *expected->append() = CONCAT;
        break;
    default:
        SkASSERT(0);
    }
}

static void emit_draw(SkCanvas* canvas, DrawOpType draw, SkTDArray<DrawType>* expected) {
    switch (draw) {
        case kNone_DrawOpType:
            break;
        case kClear_DrawOpType:
            canvas->clear(SK_ColorRED);
            *expected->append() = DRAW_CLEAR;
            break;
        case kOval_DrawOpType: {
            SkRect r = SkRect::MakeLTRB(10, 10, 90, 90);
            SkPaint p;
            canvas->drawOval(r, p);
            *expected->append() = DRAW_OVAL;
            break;
        }
        case kRect_DrawOpType: {
            SkRect r = SkRect::MakeLTRB(10, 10, 90, 90);
            SkPaint p;
            canvas->drawRect(r, p);
            *expected->append() = DRAW_RECT;
            break;
        }
        case kRRect_DrawOpType: {
            SkRect r = SkRect::MakeLTRB(10.0f, 10.0f, 90.0f, 90.0f);
            SkRRect rr;
            rr.setRectXY(r, 5.0f, 5.0f);
            SkPaint p;
            canvas->drawRRect(rr, p);
            *expected->append() = DRAW_RRECT;
            break;
        }
        default:
            SkASSERT(0);
    }
}

//////////////////////////////////////////////////////////////////////////////

// Emit:
//  clip
//  matrix
// Simple case - the clip isn't effect by the matrix
static void emit_clip_and_mat(SkCanvas* canvas, MatType mat, ClipType clip,
                              DrawOpType draw, SkTDArray<DrawType>* expected,
                              int accumulatedClips) {
    emit_clip(canvas, clip);
    emit_mat(canvas, mat);

    if (kNone_DrawOpType == draw) {
        return;
    }

    for (int i = 0; i < accumulatedClips; ++i) {
        add_clip(clip, mat, expected);
    }
    add_mat(mat, expected);
}

// Emit:
//  matrix
//  clip
// Emitting the matrix first is more challenging since the matrix has to be
// pushed across (i.e., applied to) the clip.
static void emit_mat_and_clip(SkCanvas* canvas, MatType mat, ClipType clip,
                              DrawOpType draw, SkTDArray<DrawType>* expected,
                              int accumulatedClips) {
    emit_mat(canvas, mat);
    emit_clip(canvas, clip);

    if (kNone_DrawOpType == draw) {
        return;
    }

    // the matrix & clip order will be reversed once collapsed!
    for (int i = 0; i < accumulatedClips; ++i) {
        add_clip(clip, mat, expected);
    }
    add_mat(mat, expected);
}

// Emit:
//  matrix
//  clip
//  matrix
//  clip
// This tests that the matrices and clips coalesce when collapsed
static void emit_double_mat_and_clip(SkCanvas* canvas, MatType mat, ClipType clip,
                                     DrawOpType draw, SkTDArray<DrawType>* expected,
                                     int accumulatedClips) {
    emit_mat(canvas, mat);
    emit_clip(canvas, clip);
    emit_mat(canvas, mat);
    emit_clip(canvas, clip);

    if (kNone_DrawOpType == draw) {
        return;
    }

    for (int i = 0; i < accumulatedClips; ++i) {
        add_clip(clip, mat, expected);
        add_clip(clip, mat, expected);
    }
    add_mat(mat, expected);
}

// Emit:
//  matrix
//  clip
//  clip
// This tests accumulation of clips in same transform state. It also tests pushing
// of the matrix across both the clips.
static void emit_mat_clip_clip(SkCanvas* canvas, MatType mat, ClipType clip,
                               DrawOpType draw, SkTDArray<DrawType>* expected,
                               int accumulatedClips) {
    emit_mat(canvas, mat);
    emit_clip(canvas, clip);
    emit_clip(canvas, clip);

    if (kNone_DrawOpType == draw) {
        return;
    }

    for (int i = 0; i < accumulatedClips; ++i) {
        add_clip(clip, mat, expected);
        add_clip(clip, mat, expected);
    }
    add_mat(mat, expected);
}

//////////////////////////////////////////////////////////////////////////////

// Emit:
//  matrix & clip calls
//  draw op
static void emit_body0(SkCanvas* canvas, PFEmitMC emitMC, MatType mat,
                       ClipType clip, DrawOpType draw,
                       SkTDArray<DrawType>* expected, int accumulatedClips) {
    bool needsSaveRestore = kNone_DrawOpType != draw &&
                            (kNone_MatType != mat || kNone_ClipType != clip);

    if (needsSaveRestore) {
        *expected->append() = SAVE;
    }
    (*emitMC)(canvas, mat, clip, draw, expected, accumulatedClips+1);
    emit_draw(canvas, draw, expected);
    if (needsSaveRestore) {
        *expected->append() = RESTORE;
    }
}

// Emit:
//  matrix & clip calls
//  draw op
//  matrix & clip calls
//  draw op
static void emit_body1(SkCanvas* canvas, PFEmitMC emitMC, MatType mat,
                       ClipType clip, DrawOpType draw,
                       SkTDArray<DrawType>* expected, int accumulatedClips) {
    bool needsSaveRestore = kNone_DrawOpType != draw &&
                            (kNone_MatType != mat || kNone_ClipType != clip);

    if (needsSaveRestore) {
        *expected->append() = SAVE;
    }
    (*emitMC)(canvas, mat, clip, draw, expected, accumulatedClips+1);
    emit_draw(canvas, draw, expected);
    if (needsSaveRestore) {
        *expected->append() = RESTORE;
        *expected->append() = SAVE;
    }
    (*emitMC)(canvas, mat, clip, draw, expected, accumulatedClips+2);
    emit_draw(canvas, draw, expected);
    if (needsSaveRestore) {
        *expected->append() = RESTORE;
    }
}

// Emit:
//  matrix & clip calls
//  SaveLayer
//      matrix & clip calls
//      draw op
//  Restore
static void emit_body2(SkCanvas* canvas, PFEmitMC emitMC, MatType mat,
                       ClipType clip, DrawOpType draw,
                       SkTDArray<DrawType>* expected, int accumulatedClips) {
    bool needsSaveRestore = kNone_DrawOpType != draw &&
                            (kNone_MatType != mat || kNone_ClipType != clip);

    if (kNone_MatType != mat || kNone_ClipType != clip) {
        *expected->append() = SAVE;
    }
    (*emitMC)(canvas, mat, clip, kSaveLayer_DrawOpType, expected, accumulatedClips+1);
    *expected->append() = SAVE_LAYER;
    // TODO: widen testing to exercise saveLayer's parameters
    canvas->saveLayer(nullptr, nullptr);
        if (needsSaveRestore) {
            *expected->append() = SAVE;
        }
        (*emitMC)(canvas, mat, clip, draw, expected, 1);
        emit_draw(canvas, draw, expected);
        if (needsSaveRestore) {
            *expected->append() = RESTORE;
        }
    canvas->restore();
    *expected->append() = RESTORE;
    if (kNone_MatType != mat || kNone_ClipType != clip) {
        *expected->append() = RESTORE;
    }
}

// Emit:
//  matrix & clip calls
//  SaveLayer
//      matrix & clip calls
//      SaveLayer
//          matrix & clip calls
//          draw op
//      Restore
//      matrix & clip calls (will be ignored)
//  Restore
static void emit_body3(SkCanvas* canvas, PFEmitMC emitMC, MatType mat,
                       ClipType clip, DrawOpType draw,
                       SkTDArray<DrawType>* expected, int accumulatedClips) {
    bool needsSaveRestore = kNone_DrawOpType != draw &&
                            (kNone_MatType != mat || kNone_ClipType != clip);

    if (kNone_MatType != mat || kNone_ClipType != clip) {
        *expected->append() = SAVE;
    }
    (*emitMC)(canvas, mat, clip, kSaveLayer_DrawOpType, expected, accumulatedClips+1);
    *expected->append() = SAVE_LAYER;
    // TODO: widen testing to exercise saveLayer's parameters
    canvas->saveLayer(nullptr, nullptr);
        (*emitMC)(canvas, mat, clip, kSaveLayer_DrawOpType, expected, 1);
        if (kNone_MatType != mat || kNone_ClipType != clip) {
            *expected->append() = SAVE;
        }
        *expected->append() = SAVE_LAYER;
        // TODO: widen testing to exercise saveLayer's parameters
        canvas->saveLayer(nullptr, nullptr);
            if (needsSaveRestore) {
                *expected->append() = SAVE;
            }
            (*emitMC)(canvas, mat, clip, draw, expected, 1);
            emit_draw(canvas, draw, expected);
            if (needsSaveRestore) {
                *expected->append() = RESTORE;
            }
        canvas->restore();             // for saveLayer
        *expected->append() = RESTORE; // for saveLayer
        if (kNone_MatType != mat || kNone_ClipType != clip) {
            *expected->append() = RESTORE;
        }
    canvas->restore();
    // required to match forced SAVE_LAYER
    *expected->append() = RESTORE;
    if (kNone_MatType != mat || kNone_ClipType != clip) {
        *expected->append() = RESTORE;
    }
}

//////////////////////////////////////////////////////////////////////////////

// Emit:
//  Save
//      some body
//  Restore
// Note: the outer save/restore are provided by beginRecording/endRecording
static void emit_struct0(SkCanvas* canvas,
                         PFEmitMC emitMC, MatType mat, ClipType clip,
                         PFEmitBody emitBody, DrawOpType draw,
                         SkTDArray<DrawType>* expected) {
    (*emitBody)(canvas, emitMC, mat, clip, draw, expected, 0);
}

// Emit:
//  Save
//      matrix & clip calls
//      Save
//          some body
//      Restore
//      matrix & clip calls (will be ignored)
//  Restore
// Note: the outer save/restore are provided by beginRecording/endRecording
static void emit_struct1(SkCanvas* canvas,
                         PFEmitMC emitMC, MatType mat, ClipType clip,
                         PFEmitBody emitBody, DrawOpType draw,
                         SkTDArray<DrawType>* expected) {
    (*emitMC)(canvas, mat, clip, draw, nullptr, 0); // these get fused into later ops
    canvas->save();
        (*emitBody)(canvas, emitMC, mat, clip, draw, expected, 1);
    canvas->restore();
    (*emitMC)(canvas, mat, clip, draw, nullptr, 0); // these will get removed
}

// Emit:
//  Save
//      matrix & clip calls
//      Save
//          some body
//      Restore
//      Save
//          some body
//      Restore
//      matrix & clip calls (will be ignored)
//  Restore
// Note: the outer save/restore are provided by beginRecording/endRecording
static void emit_struct2(SkCanvas* canvas,
                         PFEmitMC emitMC, MatType mat, ClipType clip,
                         PFEmitBody emitBody, DrawOpType draw,
                         SkTDArray<DrawType>* expected) {
    (*emitMC)(canvas, mat, clip, draw, nullptr, 1); // these will get fused into later ops
    canvas->save();
        (*emitBody)(canvas, emitMC, mat, clip, draw, expected, 1);
    canvas->restore();
    canvas->save();
        (*emitBody)(canvas, emitMC, mat, clip, draw, expected, 1);
    canvas->restore();
    (*emitMC)(canvas, mat, clip, draw, nullptr, 1); // these will get removed
}

// Emit:
//  Save
//      matrix & clip calls
//      Save
//          some body
//      Restore
//      Save
//          matrix & clip calls
//          Save
//              some body
//          Restore
//      Restore
//      matrix & clip calls (will be ignored)
//  Restore
// Note: the outer save/restore are provided by beginRecording/endRecording
static void emit_struct3(SkCanvas* canvas,
                         PFEmitMC emitMC, MatType mat, ClipType clip,
                         PFEmitBody emitBody, DrawOpType draw,
                         SkTDArray<DrawType>* expected) {
    (*emitMC)(canvas, mat, clip, draw, nullptr, 0); // these will get fused into later ops
    canvas->save();
        (*emitBody)(canvas, emitMC, mat, clip, draw, expected, 1);
    canvas->restore();
    canvas->save();
        (*emitMC)(canvas, mat, clip, draw, nullptr, 1); // these will get fused into later ops
        canvas->save();
            (*emitBody)(canvas, emitMC, mat, clip, draw, expected, 2);
        canvas->restore();
    canvas->restore();
    (*emitMC)(canvas, mat, clip, draw, nullptr, 0); // these will get removed
}

//////////////////////////////////////////////////////////////////////////////

#ifdef SK_COLLAPSE_MATRIX_CLIP_STATE
static void print(const SkTDArray<DrawType>& expected, const SkTDArray<DrawType>& actual) {
    SkDebugf("\n\nexpected %d --- actual %d\n", expected.count(), actual.count());
    int max = SkMax32(expected.count(), actual.count());

    for (int i = 0; i < max; ++i) {
        if (i < expected.count()) {
            SkDebugf("%16s,    ", DrawCommand::GetCommandString(expected[i]));
        } else {
            SkDebugf("%16s,    ", " ");
        }

        if (i < actual.count()) {
            SkDebugf("%s\n", DrawCommand::GetCommandString(actual[i]));
        } else {
            SkDebugf("\n");
        }
    }
    SkDebugf("\n\n");
    SkASSERT(0);
}
#endif

static void test_collapse(skiatest::Reporter* reporter) {
    PFEmitStruct gStructure[] = { emit_struct0, emit_struct1, emit_struct2, emit_struct3 };
    PFEmitBody gBody[] = { emit_body0, emit_body1, emit_body2, emit_body3 };
    PFEmitMC gMCs[] = { emit_clip_and_mat, emit_mat_and_clip,
                        emit_double_mat_and_clip, emit_mat_clip_clip };

    for (size_t i = 0; i < SK_ARRAY_COUNT(gStructure); ++i) {
        for (size_t j = 0; j < SK_ARRAY_COUNT(gBody); ++j) {
            for (size_t k = 0; k < SK_ARRAY_COUNT(gMCs); ++k) {
                for (int l = 0; l < kMatTypeCount; ++l) {
                    for (int m = 0; m < kClipTypeCount; ++m) {
                        for (int n = 0; n < kNonSaveLayerDrawOpTypeCount; ++n) {
#ifdef TEST_COLLAPSE_MATRIX_CLIP_STATE
                            static int testID = -1;
                            ++testID;
                            if (testID < -1) {
                                continue;
                            }
                            SkDebugf("test: %d\n", testID);
#endif

                            SkTDArray<DrawType> expected, actual;

                            SkPicture picture;

                            // Note: beginRecording/endRecording add a save/restore pair
                            SkCanvas* canvas = picture.beginRecording(100, 100);
                            (*gStructure[i])(canvas,
                                             gMCs[k],
                                             (MatType) l,
                                             (ClipType) m,
                                             gBody[j],
                                             (DrawOpType) n,
                                             &expected);
                            picture.endRecording();

                            gets_ops(picture, &actual);

                            REPORTER_ASSERT(reporter, expected.count() == actual.count());

                            if (expected.count() != actual.count()) {
#ifdef TEST_COLLAPSE_MATRIX_CLIP_STATE
                                print(expected, actual);
#endif
                                continue;
                            }

                            for (int i = 0; i < expected.count(); ++i) {
                                REPORTER_ASSERT(reporter, expected[i] == actual[i]);
#ifdef TEST_COLLAPSE_MATRIX_CLIP_STATE
                                if (expected[i] != actual[i]) {
                                    print(expected, actual);
                                }
#endif
                                break;
                            }
                        }
                    }
                }
            }
        }
    }
}

DEF_TEST(MatrixClipCollapse, reporter) {
    test_collapse(reporter);
}

#endif
