/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.server.wm;

import static android.os.Build.IS_USER;
import static com.android.server.wm.proto.WindowManagerTraceFileProto.ENTRY;
import static com.android.server.wm.proto.WindowManagerTraceFileProto.MAGIC_NUMBER;
import static com.android.server.wm.proto.WindowManagerTraceFileProto.MAGIC_NUMBER_H;
import static com.android.server.wm.proto.WindowManagerTraceFileProto.MAGIC_NUMBER_L;
import static com.android.server.wm.proto.WindowManagerTraceProto.ELAPSED_REALTIME_NANOS;
import static com.android.server.wm.proto.WindowManagerTraceProto.WHERE;
import static com.android.server.wm.proto.WindowManagerTraceProto.WINDOW_MANAGER_SERVICE;

import android.content.Context;
import android.os.ShellCommand;
import android.os.SystemClock;
import android.os.Trace;
import android.annotation.Nullable;
import android.util.Log;
import android.util.proto.ProtoOutputStream;

import com.android.internal.annotations.VisibleForTesting;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

/**
 * A class that allows window manager to dump its state continuously to a trace file, such that a
 * time series of window manager state can be analyzed after the fact.
 */
class WindowTracing {

    private static final String TAG = "WindowTracing";
    private static final long MAGIC_NUMBER_VALUE = ((long) MAGIC_NUMBER_H << 32) | MAGIC_NUMBER_L;

    private final Object mLock = new Object();
    private final File mTraceFile;
    private final BlockingQueue<ProtoOutputStream> mWriteQueue = new ArrayBlockingQueue<>(200);

    private boolean mEnabled;
    private volatile boolean mEnabledLockFree;

    WindowTracing(File file) {
        mTraceFile = file;
    }

    void startTrace(@Nullable PrintWriter pw) throws IOException {
        if (IS_USER){
            logAndPrintln(pw, "Error: Tracing is not supported on user builds.");
            return;
        }
        synchronized (mLock) {
            logAndPrintln(pw, "Start tracing to " + mTraceFile + ".");
            mWriteQueue.clear();
            mTraceFile.delete();
            try (OutputStream os = new FileOutputStream(mTraceFile)) {
                mTraceFile.setReadable(true, false);
                ProtoOutputStream proto = new ProtoOutputStream(os);
                proto.write(MAGIC_NUMBER, MAGIC_NUMBER_VALUE);
                proto.flush();
            }
            mEnabled = mEnabledLockFree = true;
        }
    }

    private void logAndPrintln(@Nullable PrintWriter pw, String msg) {
        Log.i(TAG, msg);
        if (pw != null) {
            pw.println(msg);
            pw.flush();
        }
    }

    void stopTrace(@Nullable PrintWriter pw) {
        if (IS_USER){
            logAndPrintln(pw, "Error: Tracing is not supported on user builds.");
            return;
        }
        synchronized (mLock) {
            logAndPrintln(pw, "Stop tracing to " + mTraceFile + ". Waiting for traces to flush.");
            mEnabled = mEnabledLockFree = false;
            while (!mWriteQueue.isEmpty()) {
                if (mEnabled) {
                    logAndPrintln(pw, "ERROR: tracing was re-enabled while waiting for flush.");
                    throw new IllegalStateException("tracing enabled while waiting for flush.");
                }
                try {
                    mLock.wait();
                    mLock.notify();
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
            logAndPrintln(pw, "Trace written to " + mTraceFile + ".");
        }
    }

    void appendTraceEntry(ProtoOutputStream proto) {
        if (!mEnabledLockFree) {
            return;
        }

        if (!mWriteQueue.offer(proto)) {
            Log.e(TAG, "Dropping window trace entry, queue full");
        }
    }

    void loop() {
        for (;;) {
            loopOnce();
        }
    }

    @VisibleForTesting
    void loopOnce() {
        ProtoOutputStream proto;
        try {
            proto = mWriteQueue.take();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return;
        }

        synchronized (mLock) {
            try {
                Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "writeToFile");
                try (OutputStream os = new FileOutputStream(mTraceFile, true /* append */)) {
                    os.write(proto.getBytes());
                }
            } catch (IOException e) {
                Log.e(TAG, "Failed to write file " + mTraceFile, e);
            } finally {
                Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
            }
            mLock.notify();
        }
    }

    boolean isEnabled() {
        return mEnabledLockFree;
    }

    static WindowTracing createDefaultAndStartLooper(Context context) {
        File file = new File("/data/misc/wmtrace/wm_trace.pb");
        WindowTracing windowTracing = new WindowTracing(file);
        if (!IS_USER){
            new Thread(windowTracing::loop, "window_tracing").start();
        }
        return windowTracing;
    }

    int onShellCommand(ShellCommand shell, String cmd) {
        PrintWriter pw = shell.getOutPrintWriter();
        try {
            switch (cmd) {
                case "start":
                    startTrace(pw);
                    return 0;
                case "stop":
                    stopTrace(pw);
                    return 0;
                default:
                    pw.println("Unknown command: " + cmd);
                    return -1;
            }
        } catch (IOException e) {
            logAndPrintln(pw, e.toString());
            throw new RuntimeException(e);
        }
    }

    void traceStateLocked(String where, WindowManagerService service) {
        if (!isEnabled()) {
            return;
        }
        ProtoOutputStream os = new ProtoOutputStream();
        long tokenOuter = os.start(ENTRY);
        os.write(ELAPSED_REALTIME_NANOS, SystemClock.elapsedRealtimeNanos());
        os.write(WHERE, where);

        Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "writeToProtoLocked");
        try {
            long tokenInner = os.start(WINDOW_MANAGER_SERVICE);
            service.writeToProtoLocked(os, true /* trim */);
            os.end(tokenInner);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
        }
        os.end(tokenOuter);
        appendTraceEntry(os);
        Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
    }
}
