/*
 * 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 <atomic>
#include "include/core/SkGraphics.h"
#include "include/core/SkTime.h"
#include "include/private/SkTArray.h"
#include "include/private/SkTemplates.h"
#include "src/core/SkOSFile.h"
#include "src/core/SkTaskGroup.h"
#include "src/pathops/SkPathOpsDebug.h"
#include "tests/PathOpsDebug.h"
#include "tests/Test.h"
#include "tools/CrashHandler.h"
#include "tools/OverwriteLine.h"
#include "tools/Resources.h"
#include "tools/flags/CommandLineFlags.h"
#include "tools/gpu/GrContextFactory.h"

using namespace skiatest;
using namespace sk_gpu_test;

static DEFINE_bool2(dumpOp, d, false, "dump the pathOps to a file to recover mid-crash.");
static DEFINE_bool2(extendedTest, x, false, "run extended tests for pathOps.");
static DEFINE_bool2(runFail, f, false, "check for success on tests known to fail.");
static DEFINE_bool2(verifyOp, y, false, "compare the pathOps result against a region.");
static DEFINE_string2(json, J, "", "write json version of tests.");
static DEFINE_bool2(verbose, v, false, "enable verbose output from the test driver.");
static DEFINE_bool2(veryVerbose, V, false, "tell individual tests to be verbose.");
static DEFINE_bool(cpu, true, "Run CPU-bound work?");
static DEFINE_bool(gpu, true, "Run GPU-bound work?");

static DEFINE_string2(match, m, nullptr,
               "[~][^]substring[$] [...] of name to run.\n"
               "Multiple matches may be separated by spaces.\n"
               "~ causes a matching name to always be skipped\n"
               "^ requires the start of the name to match\n"
               "$ requires the end of the name to match\n"
               "^ and $ requires an exact match\n"
               "If a name does not match any list entry,\n"
               "it is skipped unless some list entry starts with ~");

static DEFINE_int_2(threads, j, -1,
               "Run threadsafe tests on a threadpool with this many extra threads, "
               "defaulting to one extra thread per core.");

#if DEBUG_COIN
static DEFINE_bool2(coinTest, c, false, "detect unused coincidence algorithms.");
#endif

// need to explicitly declare this, or we get some weird infinite loop llist
template TestRegistry* TestRegistry::gHead;
void (*gVerboseFinalize)() = nullptr;

// The threads report back to this object when they are done.
class Status {
public:
    explicit Status(int total)
        : fDone(0), fTestCount(0), fFailCount(0), fTotal(total) {}
    // Threadsafe.
    void endTest(const char* testName,
                 bool success,
                 SkMSec elapsed,
                 int testCount) {
        const int done = ++fDone;
        fTestCount += testCount;
        if (!success) {
            SkDebugf("\n---- %s FAILED", testName);
        }

        SkString prefix(kSkOverwriteLine);
        SkString time;
        if (FLAGS_verbose) {
            prefix.printf("\n");
            time.printf("%5dms ", elapsed);
        }
        SkDebugf("%s[%3d/%3d] %s%s", prefix.c_str(), done, fTotal, time.c_str(),
                 testName);
    }

    void reportFailure() { fFailCount++; }

    int32_t testCount() { return fTestCount; }
    int32_t failCount() { return fFailCount; }

private:
    std::atomic<int32_t> fDone;
    std::atomic<int32_t> fTestCount;
    std::atomic<int32_t> fFailCount;
    const int fTotal;
};

class SkTestRunnable {
public:
    SkTestRunnable(const Test& test, Status* status) : fTest(test), fStatus(status) {}

    void operator()() {
        struct TestReporter : public skiatest::Reporter {
        public:
            TestReporter() : fStats(nullptr), fError(false), fTestCount(0) {}
            void bumpTestCount() override { ++fTestCount; }
            bool allowExtendedTest() const override { return FLAGS_extendedTest; }
            bool verbose() const override { return FLAGS_veryVerbose; }
            void reportFailed(const skiatest::Failure& failure) override {
                SkDebugf("\nFAILED: %s", failure.toString().c_str());
                fError = true;
            }
            void* stats() const override { return fStats; }
            void* fStats;
            bool fError;
            int fTestCount;
        } reporter;

        const Timer timer;
        fTest.proc(&reporter, GrContextOptions());
        SkMSec elapsed = timer.elapsedMsInt();
        if (reporter.fError) {
            fStatus->reportFailure();
        }
        fStatus->endTest(fTest.name, !reporter.fError, elapsed, reporter.fTestCount);
  }

private:
    Test fTest;
    Status* fStatus;
};

static bool should_run(const char* testName, bool isGPUTest) {
    if (CommandLineFlags::ShouldSkip(FLAGS_match, testName)) {
        return false;
    }
    if (!FLAGS_cpu && !isGPUTest) {
        return false;
    }
    if (!FLAGS_gpu && isGPUTest) {
        return false;
    }
    return true;
}

int main(int argc, char** argv) {
    CommandLineFlags::Parse(argc, argv);
#if DEBUG_DUMP_VERIFY
    SkPathOpsDebug::gDumpOp = FLAGS_dumpOp;
    SkPathOpsDebug::gVerifyOp = FLAGS_verifyOp;
#endif
    SkPathOpsDebug::gRunFail = FLAGS_runFail;
    SkPathOpsDebug::gVeryVerbose = FLAGS_veryVerbose;
    PathOpsDebug::gOutFirst = true;
    PathOpsDebug::gCheckForDuplicateNames = false;
    PathOpsDebug::gOutputSVG = false;
    if ((PathOpsDebug::gJson = !FLAGS_json.isEmpty())) {
        PathOpsDebug::gOut = fopen(FLAGS_json[0], "wb");
        fprintf(PathOpsDebug::gOut, "{\n");
        FLAGS_threads = 0;
        PathOpsDebug::gMarkJsonFlaky = false;
    }
    SetupCrashHandler();

    SkAutoGraphics ag;

    {
        SkString header("Skia UnitTests:");
        if (!FLAGS_match.isEmpty()) {
            header.appendf(" --match");
            for (int index = 0; index < FLAGS_match.count(); ++index) {
                header.appendf(" %s", FLAGS_match[index]);
            }
        }
        SkString tmpDir = skiatest::GetTmpDir();
        if (!tmpDir.isEmpty()) {
            header.appendf(" --tmpDir %s", tmpDir.c_str());
        }
        SkString resourcePath = GetResourcePath();
        if (!resourcePath.isEmpty()) {
            header.appendf(" --resourcePath %s", resourcePath.c_str());
        }
#if DEBUG_COIN
        if (FLAGS_coinTest) {
            header.appendf(" -c");
        }
#endif
        if (FLAGS_dumpOp) {
            header.appendf(" -d");
        }
#ifdef SK_DEBUG
        if (FLAGS_runFail) {
            header.appendf(" -f");
        }
#endif
        if (FLAGS_verbose) {
            header.appendf(" -v");
        }
        if (FLAGS_veryVerbose) {
            header.appendf(" -V");
        }
        if (FLAGS_extendedTest) {
            header.appendf(" -x");
        }
        if (FLAGS_verifyOp) {
            header.appendf(" -y");
        }
#ifdef SK_DEBUG
        header.append(" SK_DEBUG");
#else
        header.append(" SK_RELEASE");
#endif
        if (FLAGS_veryVerbose) {
            header.appendf("\n");
        }
        SkDebugf("%s", header.c_str());
    }


    // Count tests first.
    int total = 0;
    int toRun = 0;

    for (const Test& test : TestRegistry::Range()) {
        if (should_run(test.name, test.needsGpu)) {
            toRun++;
        }
        total++;
    }

    // Now run them.
    int skipCount = 0;

    SkTaskGroup::Enabler enabled(FLAGS_threads);
    SkTaskGroup cpuTests;
    SkTArray<const Test*> gpuTests;

    Status status(toRun);

    for (const Test& test : TestRegistry::Range()) {
        if (!should_run(test.name, test.needsGpu)) {
            ++skipCount;
        } else if (test.needsGpu) {
            gpuTests.push_back(&test);
        } else {
            cpuTests.add(SkTestRunnable(test, &status));
        }
    }

    // Run GPU tests on this thread.
    for (int i = 0; i < gpuTests.count(); i++) {
        SkTestRunnable(*gpuTests[i], &status)();
    }

    // Block until threaded tests finish.
    cpuTests.wait();

    if (FLAGS_verbose) {
        SkDebugf(
                "\nFinished %d tests, %d failures, %d skipped. "
                "(%d internal tests)",
                toRun, status.failCount(), skipCount, status.testCount());
        if (gVerboseFinalize) {
            (*gVerboseFinalize)();
        }
    }

    SkDebugf("\n");
#if DEBUG_COIN
    if (FLAGS_coinTest) {
        SkPathOpsDebug::DumpCoinDict();
    }
#endif
    if (PathOpsDebug::gJson) {
        fprintf(PathOpsDebug::gOut, "\n}\n");
        fclose(PathOpsDebug::gOut);
    }
    return (status.failCount() == 0) ? 0 : 1;
}
