/*
 * 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.wifi;


import android.util.ArrayMap;

import com.android.internal.annotations.VisibleForTesting;
import com.android.server.wifi.util.FileUtils;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Map;

/**
 * Provides a facility for capturing kernel trace events related to Wifi control and data paths.
 */
public class LastMileLogger {
    public LastMileLogger(WifiInjector injector) {
        File tracefsEnablePath = new File(WIFI_EVENT_ENABLE_PATH);
        if (tracefsEnablePath.exists()) {
            initLastMileLogger(injector, WIFI_EVENT_BUFFER_PATH, WIFI_EVENT_ENABLE_PATH,
                    WIFI_EVENT_RELEASE_PATH);
        } else {
            initLastMileLogger(injector, WIFI_EVENT_BUFFER_PATH_DEBUGFS,
                    WIFI_EVENT_ENABLE_PATH_DEBUGFS, WIFI_EVENT_RELEASE_PATH_DEBUGFS);
        }
    }

    @VisibleForTesting
    public LastMileLogger(WifiInjector injector, String bufferPath, String enablePath,
                          String releasePath) {
        initLastMileLogger(injector, bufferPath, enablePath, releasePath);
    }

    /**
     * Informs LastMileLogger that a connection event has occurred.
     * @param event an event defined in WifiDiagnostics
     */
    public void reportConnectionEvent(String ifaceName, byte event) {
        boolean wasTracingEnabled = anyConnectionInProgress();

        mIfaceToConnectionStatus.put(ifaceName, event);

        boolean shouldTracingBeEnabled = anyConnectionInProgress();

        if (!wasTracingEnabled && shouldTracingBeEnabled) {
            enableTracing();
        } else if (wasTracingEnabled && !shouldTracingBeEnabled) {
            disableTracing();
        }

        if (event == WifiDiagnostics.CONNECTION_EVENT_FAILED
                || event == WifiDiagnostics.CONNECTION_EVENT_TIMEOUT) {
            mLastMileLogForLastFailure = readTrace();
        }
    }

    private boolean anyConnectionInProgress() {
        for (byte status : mIfaceToConnectionStatus.values()) {
            if (status == WifiDiagnostics.CONNECTION_EVENT_STARTED) {
                return true;
            }
        }
        return false;
    }

    /**
     * Dumps the contents of the log.
     * @param pw the PrintWriter that will receive the dump
     */
    public void dump(PrintWriter pw) {
        dumpInternal(pw, "Last failed last-mile log", mLastMileLogForLastFailure);
        dumpInternal(pw, "Latest last-mile log", readTrace());
    }

    private static final String TAG = "LastMileLogger";
    private static final String WIFI_EVENT_BUFFER_PATH =
            "/sys/kernel/tracing/instances/wifi/trace";
    private static final String WIFI_EVENT_ENABLE_PATH =
            "/sys/kernel/tracing/instances/wifi/tracing_on";
    private static final String WIFI_EVENT_RELEASE_PATH =
            "/sys/kernel/tracing/instances/wifi/free_buffer";
    private static final String WIFI_EVENT_BUFFER_PATH_DEBUGFS =
            "/sys/kernel/debug/tracing/instances/wifi/trace";
    private static final String WIFI_EVENT_ENABLE_PATH_DEBUGFS =
            "/sys/kernel/debug/tracing/instances/wifi/tracing_on";
    private static final String WIFI_EVENT_RELEASE_PATH_DEBUGFS =
            "/sys/kernel/debug/tracing/instances/wifi/free_buffer";

    private String mEventBufferPath;
    private String mEventEnablePath;
    private String mEventReleasePath;
    private WifiLog mLog;
    private byte[] mLastMileLogForLastFailure;
    private FileInputStream mLastMileTraceHandle;
    /**
     * String key: iface name
     * byte value: Connection status, one of WifiDiagnostics.CONNECTION_EVENT_*
     */
    private final Map<String, Byte> mIfaceToConnectionStatus = new ArrayMap<>();

    private void initLastMileLogger(WifiInjector injector, String bufferPath, String enablePath,
                          String releasePath) {
        mLog = injector.makeLog(TAG);
        mEventBufferPath = bufferPath;
        mEventEnablePath = enablePath;
        mEventReleasePath = releasePath;
    }

    private void enableTracing() {
        if (!ensureFailSafeIsArmed()) {
            mLog.wC("Failed to arm fail-safe.");
            return;
        }

        try {
            FileUtils.stringToFile(mEventEnablePath, "1");
        } catch (IOException e) {
            mLog.warn("Failed to start event tracing: %").r(e.getMessage()).flush();
        }
    }

    private void disableTracing() {
        try {
            FileUtils.stringToFile(mEventEnablePath, "0");
        } catch (IOException e) {
            mLog.warn("Failed to stop event tracing: %").r(e.getMessage()).flush();
        }
    }

    private byte[] readTrace() {
        try {
            return Files.readAllBytes(Paths.get(mEventBufferPath));
        } catch (IOException e) {
            mLog.warn("Failed to read event trace: %").r(e.getMessage()).flush();
            return new byte[0];
        }
    }

    private boolean ensureFailSafeIsArmed() {
        if (mLastMileTraceHandle != null) {
            return true;
        }

        try {
            // This file provides fail-safe behavior for Last-Mile logging. Given that we:
            // 1. Set the disable_on_free option in the trace_options pseudo-file
            //    (see wifi-events.rc), and
            // 2. Hold the WIFI_EVENT_RELEASE_PATH open,
            //
            // Then, when this process dies, the kernel will automatically disable any
            // tracing in the wifi trace instance.
            //
            // Note that, despite Studio's suggestion that |mLastMileTraceHandle| could be demoted
            // to a local variable, we need to stick with a field. Otherwise, the handle could be
            // garbage collected.
            mLastMileTraceHandle = new FileInputStream(mEventReleasePath);
            return true;
        } catch (IOException e) {
            mLog.warn("Failed to open free_buffer pseudo-file: %").r(e.getMessage()).flush();
            return false;
        }
    }

    private static void dumpInternal(PrintWriter pw, String description, byte[] lastMileLog) {
        if (lastMileLog == null || lastMileLog.length < 1) {
            pw.format("No last mile log for \"%s\"\n", description);
            return;
        }

        pw.format("-------------------------- %s ---------------------------\n", description);
        pw.print(new String(lastMileLog));
        pw.println("--------------------------------------------------------------------");
    }
}
