/*
 * Copyright (C) 2015-2021 Apple Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "config.h"
#include "InspectorHeapAgent.h"

#include "HeapProfiler.h"
#include "HeapSnapshot.h"
#include "InjectedScript.h"
#include "InjectedScriptManager.h"
#include "InspectorEnvironment.h"
#include "JSBigInt.h"
#include "VM.h"
#include <wtf/Stopwatch.h>

namespace Inspector {

using namespace JSC;

InspectorHeapAgent::InspectorHeapAgent(AgentContext& context)
    : InspectorAgentBase("Heap"_s)
    , m_injectedScriptManager(context.injectedScriptManager)
    , m_frontendDispatcher(makeUnique<HeapFrontendDispatcher>(context.frontendRouter))
    , m_backendDispatcher(HeapBackendDispatcher::create(context.backendDispatcher, this))
    , m_environment(context.environment)
{
}

InspectorHeapAgent::~InspectorHeapAgent() = default;

void InspectorHeapAgent::didCreateFrontendAndBackend(FrontendRouter*, BackendDispatcher*)
{
}

void InspectorHeapAgent::willDestroyFrontendAndBackend(DisconnectReason)
{
    disable();
}

Protocol::ErrorStringOr<void> InspectorHeapAgent::enable()
{
    if (m_enabled)
        return makeUnexpected("Heap domain already enabled"_s);

    m_enabled = true;

    m_environment.vm().heap.addObserver(this);

    return { };
}

Protocol::ErrorStringOr<void> InspectorHeapAgent::disable()
{
    if (!m_enabled)
        return makeUnexpected("Heap domain already disabled"_s);

    m_enabled = false;
    m_tracking = false;

    m_environment.vm().heap.removeObserver(this);

    clearHeapSnapshots();

    return { };
}

Protocol::ErrorStringOr<void> InspectorHeapAgent::gc()
{
    VM& vm = m_environment.vm();
    JSLockHolder lock(vm);
    sanitizeStackForVM(vm);
    vm.heap.collectNow(Sync, CollectionScope::Full);

    return { };
}

Protocol::ErrorStringOr<std::tuple<double, Protocol::Heap::HeapSnapshotData>> InspectorHeapAgent::snapshot()
{
    VM& vm = m_environment.vm();
    JSLockHolder lock(vm);

    HeapSnapshotBuilder snapshotBuilder(vm.ensureHeapProfiler());
    snapshotBuilder.buildSnapshot();

    auto timestamp = m_environment.executionStopwatch().elapsedTime().seconds();
    auto snapshotData = snapshotBuilder.json([&] (const HeapSnapshotNode& node) {
        if (Structure* structure = node.cell->structure()) {
            if (JSGlobalObject* globalObject = structure->globalObject()) {
                if (!m_environment.canAccessInspectedScriptState(globalObject))
                    return false;
            }
        }
        return true;
    });

    return { { timestamp, snapshotData } };
}

Protocol::ErrorStringOr<void> InspectorHeapAgent::startTracking()
{
    if (m_tracking)
        return { };

    m_tracking = true;

    auto result = snapshot();
    if (!result)
        return makeUnexpected(WTFMove(result.error()));

    auto [timestamp, snapshotData] = WTFMove(result.value());
    m_frontendDispatcher->trackingStart(timestamp, snapshotData);

    return { };
}

Protocol::ErrorStringOr<void> InspectorHeapAgent::stopTracking()
{
    if (!m_tracking)
        return { };

    m_tracking = false;

    auto result = snapshot();
    if (!result)
        return makeUnexpected(WTFMove(result.error()));

    auto [timestamp, snapshotData] = WTFMove(result.value());
    m_frontendDispatcher->trackingComplete(timestamp, snapshotData);

    return { };
}

std::optional<HeapSnapshotNode> InspectorHeapAgent::nodeForHeapObjectIdentifier(Protocol::ErrorString& errorString, unsigned heapObjectIdentifier)
{
    HeapProfiler* heapProfiler = m_environment.vm().heapProfiler();
    if (!heapProfiler) {
        errorString = "No heap snapshot"_s;
        return std::nullopt;
    }

    HeapSnapshot* snapshot = heapProfiler->mostRecentSnapshot();
    if (!snapshot) {
        errorString = "No heap snapshot"_s;
        return std::nullopt;
    }

    const std::optional<HeapSnapshotNode> optionalNode = snapshot->nodeForObjectIdentifier(heapObjectIdentifier);
    if (!optionalNode) {
        errorString = "No object for identifier, it may have been collected"_s;
        return std::nullopt;
    }

    return optionalNode;
}

Protocol::ErrorStringOr<std::tuple<String, RefPtr<Protocol::Debugger::FunctionDetails>, RefPtr<Protocol::Runtime::ObjectPreview>>> InspectorHeapAgent::getPreview(int heapObjectId)
{
    Protocol::ErrorString errorString;

    // Prevent the cell from getting collected as we look it up.
    VM& vm = m_environment.vm();
    JSLockHolder lock(vm);
    DeferGC deferGC(vm);

    unsigned heapObjectIdentifier = static_cast<unsigned>(heapObjectId);
    const std::optional<HeapSnapshotNode> optionalNode = nodeForHeapObjectIdentifier(errorString, heapObjectIdentifier);
    if (!optionalNode)
        return makeUnexpected(errorString);

    // String preview.
    JSCell* cell = optionalNode->cell;
    if (cell->isString())
        return { { asString(cell)->tryGetValue(), nullptr, nullptr } };

    // BigInt preview.
    if (cell->isHeapBigInt())
        return { { JSBigInt::tryGetString(vm, asHeapBigInt(cell), 10), nullptr, nullptr } };

    // FIXME: Provide preview information for Internal Objects? CodeBlock, Executable, etc.

    Structure* structure = cell->structure();
    if (!structure)
        return makeUnexpected("Unable to get object details - Structure"_s);

    JSGlobalObject* globalObject = structure->globalObject();
    if (!globalObject)
        return makeUnexpected("Unable to get object details - GlobalObject"_s);

    InjectedScript injectedScript = m_injectedScriptManager.injectedScriptFor(globalObject);
    if (injectedScript.hasNoValue())
        return makeUnexpected("Unable to get object details - InjectedScript"_s);

    // Function preview.
    if (cell->inherits<JSFunction>()) {
        RefPtr<Protocol::Debugger::FunctionDetails> functionDetails;
        injectedScript.functionDetails(errorString, cell, functionDetails);
        if (!functionDetails)
            return makeUnexpected(errorString);
        return { { nullString(), functionDetails, nullptr } };
    }

    // Object preview.
    return { { nullString(), nullptr, injectedScript.previewValue(cell) } };
}

Protocol::ErrorStringOr<Ref<Protocol::Runtime::RemoteObject>> InspectorHeapAgent::getRemoteObject(int heapObjectId, const String& objectGroup)
{
    Protocol::ErrorString errorString;

    // Prevent the cell from getting collected as we look it up.
    VM& vm = m_environment.vm();
    JSLockHolder lock(vm);
    DeferGC deferGC(vm);

    unsigned heapObjectIdentifier = static_cast<unsigned>(heapObjectId);
    const std::optional<HeapSnapshotNode> optionalNode = nodeForHeapObjectIdentifier(errorString, heapObjectIdentifier);
    if (!optionalNode)
        return makeUnexpected(errorString);

    JSCell* cell = optionalNode->cell;
    Structure* structure = cell->structure();
    if (!structure)
        return makeUnexpected("Unable to get object details - Structure"_s);

    JSGlobalObject* globalObject = structure->globalObject();
    if (!globalObject)
        return makeUnexpected("Unable to get object details - GlobalObject"_s);

    InjectedScript injectedScript = m_injectedScriptManager.injectedScriptFor(globalObject);
    if (injectedScript.hasNoValue())
        return makeUnexpected("Unable to get object details - InjectedScript"_s);

    auto object = injectedScript.wrapObject(cell, objectGroup, true);
    if (!object)
        return makeUnexpected("Internal error: unable to cast Object"_s);

    return object.releaseNonNull();
}

static Protocol::Heap::GarbageCollection::Type protocolTypeForHeapOperation(CollectionScope scope)
{
    switch (scope) {
    case CollectionScope::Full:
        return Protocol::Heap::GarbageCollection::Type::Full;
    case CollectionScope::Eden:
        return Protocol::Heap::GarbageCollection::Type::Partial;
    }
    ASSERT_NOT_REACHED();
    return Protocol::Heap::GarbageCollection::Type::Full;
}

void InspectorHeapAgent::willGarbageCollect()
{
    if (!m_enabled)
        return;

    m_gcStartTime = m_environment.executionStopwatch().elapsedTime();
}

void InspectorHeapAgent::didGarbageCollect(CollectionScope scope)
{
    if (!m_enabled) {
        m_gcStartTime = Seconds::nan();
        return;
    }

    if (std::isnan(m_gcStartTime)) {
        // We were not enabled when the GC began.
        return;
    }

    // FIXME: Include number of bytes freed by collection.

    Seconds endTime = m_environment.executionStopwatch().elapsedTime();
    dispatchGarbageCollectedEvent(protocolTypeForHeapOperation(scope), m_gcStartTime, endTime);

    m_gcStartTime = Seconds::nan();
}

void InspectorHeapAgent::clearHeapSnapshots()
{
    VM& vm = m_environment.vm();
    JSLockHolder lock(vm);

    if (HeapProfiler* heapProfiler = vm.heapProfiler()) {
        heapProfiler->clearSnapshots();
        HeapSnapshotBuilder::resetNextAvailableObjectIdentifier();
    }
}

void InspectorHeapAgent::dispatchGarbageCollectedEvent(Protocol::Heap::GarbageCollection::Type type, Seconds startTime, Seconds endTime)
{
    auto protocolObject = Protocol::Heap::GarbageCollection::create()
        .setType(type)
        .setStartTime(startTime.seconds())
        .setEndTime(endTime.seconds())
        .release();

    m_frontendDispatcher->garbageCollected(WTFMove(protocolObject));
}

} // namespace Inspector
