/*
 * Copyright (C) 2008 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.ddmuilib;

import com.android.ddmlib.AdbCommandRejectedException;
import com.android.ddmlib.Client;
import com.android.ddmlib.IShellOutputReceiver;
import com.android.ddmlib.Log;
import com.android.ddmlib.ShellCommandUnresponsiveException;
import com.android.ddmlib.TimeoutException;

import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.layout.RowLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.FileDialog;
import org.eclipse.swt.widgets.Label;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.JFreeChart;
import org.jfree.data.general.DefaultPieDataset;
import org.jfree.experimental.chart.swt.ChartComposite;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Displays system information graphs obtained from a bugreport file or device.
 */
public class SysinfoPanel extends TablePanel implements IShellOutputReceiver {

    // UI components
    private Label mLabel;
    private Button mFetchButton;
    private Combo mDisplayMode;

    private DefaultPieDataset mDataset;

    // The bugreport file to process
    private File mDataFile;

    // To get output from adb commands
    private FileOutputStream mTempStream;

    // Selects the current display: MODE_CPU, etc.
    private int mMode = 0;

    private static final int MODE_CPU = 0;
    private static final int MODE_ALARM = 1;
    private static final int MODE_WAKELOCK = 2;
    private static final int MODE_MEMINFO = 3;
    private static final int MODE_SYNC = 4;

    // argument to dumpsys; section in the bugreport holding the data
    private static final String BUGREPORT_SECTION[] = {"cpuinfo", "alarm",
            "batteryinfo", "MEMORY INFO", "content"};

    private static final String DUMP_COMMAND[] = {"dumpsys cpuinfo",
            "dumpsys alarm", "dumpsys batteryinfo", "cat /proc/meminfo ; procrank",
            "dumpsys content"};

    private static final String CAPTIONS[] = {"CPU load", "Alarms",
            "Wakelocks", "Memory usage", "Sync"};

    /**
     * Generates the dataset to display.
     *
     * @param file The bugreport file to process.
     */
    public void generateDataset(File file) {
        mDataset.clear();
        mLabel.setText("");
        if (file == null) {
            return;
        }
        try {
            BufferedReader br = getBugreportReader(file);
            if (mMode == MODE_CPU) {
                readCpuDataset(br);
            } else if (mMode == MODE_ALARM) {
                readAlarmDataset(br);
            } else if (mMode == MODE_WAKELOCK) {
                readWakelockDataset(br);
            } else if (mMode == MODE_MEMINFO) {
                readMeminfoDataset(br);
            } else if (mMode == MODE_SYNC) {
                readSyncDataset(br);
            }
        } catch (IOException e) {
            Log.e("DDMS", e);
        }
    }

    /**
     * Sent when a new device is selected. The new device can be accessed with
     * {@link #getCurrentDevice()}
     */
    @Override
    public void deviceSelected() {
        if (getCurrentDevice() != null) {
            mFetchButton.setEnabled(true);
            loadFromDevice();
        } else {
            mFetchButton.setEnabled(false);
        }
    }

    /**
     * Sent when a new client is selected. The new client can be accessed with
     * {@link #getCurrentClient()}.
     */
    @Override
    public void clientSelected() {
    }

    /**
     * Sets the focus to the proper control inside the panel.
     */
    @Override
    public void setFocus() {
        mDisplayMode.setFocus();
    }

    /**
     * Fetches a new bugreport from the device and updates the display.
     * Fetching is asynchronous.  See also addOutput, flush, and isCancelled.
     */
    private void loadFromDevice() {
        try {
            initShellOutputBuffer();
            if (mMode == MODE_MEMINFO) {
                // Hack to add bugreport-style section header for meminfo
                mTempStream.write("------ MEMORY INFO ------\n".getBytes());
            }
            getCurrentDevice().executeShellCommand(
                    DUMP_COMMAND[mMode], this);
        } catch (IOException e) {
            Log.e("DDMS", e);
        } catch (TimeoutException e) {
            Log.e("DDMS", e);
        } catch (AdbCommandRejectedException e) {
            Log.e("DDMS", e);
        } catch (ShellCommandUnresponsiveException e) {
            Log.e("DDMS", e);
        }
    }

    /**
     * Initializes temporary output file for executeShellCommand().
     *
     * @throws IOException on file error
     */
    void initShellOutputBuffer() throws IOException {
        mDataFile = File.createTempFile("ddmsfile", ".txt");
        mDataFile.deleteOnExit();
        mTempStream = new FileOutputStream(mDataFile);
    }

    /**
     * Adds output to the temp file. IShellOutputReceiver method. Called by
     * executeShellCommand().
     */
    public void addOutput(byte[] data, int offset, int length) {
        try {
            mTempStream.write(data, offset, length);
        }
        catch (IOException e) {
            Log.e("DDMS", e);
        }
    }

    /**
     * Processes output from shell command. IShellOutputReceiver method. The
     * output is passed to generateDataset(). Called by executeShellCommand() on
     * completion.
     */
    public void flush() {
        if (mTempStream != null) {
            try {
                mTempStream.close();
                generateDataset(mDataFile);
                mTempStream = null;
                mDataFile = null;
            } catch (IOException e) {
                Log.e("DDMS", e);
            }
        }
    }

    /**
     * IShellOutputReceiver method.
     *
     * @return false - don't cancel
     */
    public boolean isCancelled() {
        return false;
    }

    /**
     * Create our controls for the UI panel.
     */
    @Override
    protected Control createControl(Composite parent) {
        Composite top = new Composite(parent, SWT.NONE);
        top.setLayout(new GridLayout(1, false));
        top.setLayoutData(new GridData(GridData.FILL_BOTH));

        Composite buttons = new Composite(top, SWT.NONE);
        buttons.setLayout(new RowLayout());

        mDisplayMode = new Combo(buttons, SWT.PUSH);
        for (String mode : CAPTIONS) {
            mDisplayMode.add(mode);
        }
        mDisplayMode.select(mMode);
        mDisplayMode.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                mMode = mDisplayMode.getSelectionIndex();
                if (mDataFile != null) {
                    generateDataset(mDataFile);
                } else if (getCurrentDevice() != null) {
                    loadFromDevice();
                }
            }
        });

        final Button loadButton = new Button(buttons, SWT.PUSH);
        loadButton.setText("Load from File");
        loadButton.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                FileDialog fileDialog = new FileDialog(loadButton.getShell(),
                        SWT.OPEN);
                fileDialog.setText("Load bugreport");
                String filename = fileDialog.open();
                if (filename != null) {
                    mDataFile = new File(filename);
                    generateDataset(mDataFile);
                }
            }
        });

        mFetchButton = new Button(buttons, SWT.PUSH);
        mFetchButton.setText("Update from Device");
        mFetchButton.setEnabled(false);
        mFetchButton.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                loadFromDevice();
            }
        });

        mLabel = new Label(top, SWT.NONE);
        mLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));

        mDataset = new DefaultPieDataset();
        JFreeChart chart = ChartFactory.createPieChart("", mDataset, false
                /* legend */, true/* tooltips */, false /* urls */);

        ChartComposite chartComposite = new ChartComposite(top,
                SWT.BORDER, chart,
                ChartComposite.DEFAULT_HEIGHT,
                ChartComposite.DEFAULT_HEIGHT,
                ChartComposite.DEFAULT_MINIMUM_DRAW_WIDTH,
                ChartComposite.DEFAULT_MINIMUM_DRAW_HEIGHT,
                3000,
                // max draw width. We don't want it to zoom, so we put a big number
                3000,
                // max draw height. We don't want it to zoom, so we put a big number
                true,  // off-screen buffer
                true,  // properties
                true,  // save
                true,  // print
                false,  // zoom
                true);
        chartComposite.setLayoutData(new GridData(GridData.FILL_BOTH));
        return top;
    }

    public void clientChanged(final Client client, int changeMask) {
        // Don't care
    }

    /**
     * Helper to open a bugreport and skip to the specified section.
     *
     * @param file File to open
     * @return Reader to bugreport file
     * @throws java.io.IOException on file error
     */
    private BufferedReader getBugreportReader(File file) throws
            IOException {
        BufferedReader br = new BufferedReader(new FileReader(file));
        // Skip over the unwanted bugreport sections
        while (true) {
            String line = br.readLine();
            if (line == null) {
                Log.d("DDMS", "Service not found " + line);
                break;
            }
            if ((line.startsWith("DUMP OF SERVICE ") || line.startsWith("-----")) &&
                    line.indexOf(BUGREPORT_SECTION[mMode]) > 0) {
                break;
            }
        }
        return br;
    }

    /**
     * Parse the time string generated by BatteryStats.
     * A typical new-format string is "11d 13h 45m 39s 999ms".
     * A typical old-format string is "12.3 sec".
     * @return time in ms
     */
    private static long parseTimeMs(String s) {
        long total = 0;
        // Matches a single component e.g. "12.3 sec" or "45ms"
        Pattern p = Pattern.compile("([\\d\\.]+)\\s*([a-z]+)");
        Matcher m = p.matcher(s);
        while (m.find()) {
            String label = m.group(2);
            if ("sec".equals(label)) {
                // Backwards compatibility with old time format
                total += (long) (Double.parseDouble(m.group(1)) * 1000);
                continue;
            }
            long value = Integer.parseInt(m.group(1));
            if ("d".equals(label)) {
                total += value * 24 * 60 * 60 * 1000;
            } else if ("h".equals(label)) {
                total += value * 60 * 60 * 1000;
            } else if ("m".equals(label)) {
                total += value * 60 * 1000;
            } else if ("s".equals(label)) {
                total += value * 1000;
            } else if ("ms".equals(label)) {
                total += value;
            }
        }
        return total;
    }
    /**
     * Processes wakelock information from bugreport. Updates mDataset with the
     * new data.
     *
     * @param br Reader providing the content
     * @throws IOException if error reading file
     */
    void readWakelockDataset(BufferedReader br) throws IOException {
        Pattern lockPattern = Pattern.compile("Wake lock (\\S+): (.+) partial");
        Pattern totalPattern = Pattern.compile("Total: (.+) uptime");
        double total = 0;
        boolean inCurrent = false;

        while (true) {
            String line = br.readLine();
            if (line == null || line.startsWith("DUMP OF SERVICE")) {
                // Done, or moved on to the next service
                break;
            }
            if (line.startsWith("Current Battery Usage Statistics")) {
                inCurrent = true;
            } else if (inCurrent) {
                Matcher m = lockPattern.matcher(line);
                if (m.find()) {
                    double value = parseTimeMs(m.group(2)) / 1000.;
                    mDataset.setValue(m.group(1), value);
                    total -= value;
                } else {
                    m = totalPattern.matcher(line);
                    if (m.find()) {
                        total += parseTimeMs(m.group(1)) / 1000.;
                    }
                }
            }
        }
        if (total > 0) {
            mDataset.setValue("Unlocked", total);
        }
    }

    /**
     * Processes alarm information from bugreport. Updates mDataset with the new
     * data.
     *
     * @param br Reader providing the content
     * @throws IOException if error reading file
     */
    void readAlarmDataset(BufferedReader br) throws IOException {
        Pattern pattern = Pattern
                .compile("(\\d+) alarms: Intent .*\\.([^. ]+) flags");

        while (true) {
            String line = br.readLine();
            if (line == null || line.startsWith("DUMP OF SERVICE")) {
                // Done, or moved on to the next service
                break;
            }
            Matcher m = pattern.matcher(line);
            if (m.find()) {
                long count = Long.parseLong(m.group(1));
                String name = m.group(2);
                mDataset.setValue(name, count);
            }
        }
    }

    /**
     * Processes cpu load information from bugreport. Updates mDataset with the
     * new data.
     *
     * @param br Reader providing the content
     * @throws IOException if error reading file
     */
    void readCpuDataset(BufferedReader br) throws IOException {
        Pattern pattern = Pattern
                .compile("(\\S+): (\\S+)% = (.+)% user . (.+)% kernel");

        while (true) {
            String line = br.readLine();
            if (line == null || line.startsWith("DUMP OF SERVICE")) {
                // Done, or moved on to the next service
                break;
            }
            if (line.startsWith("Load:")) {
                mLabel.setText(line);
                continue;
            }
            Matcher m = pattern.matcher(line);
            if (m.find()) {
                String name = m.group(1);
                long both = Long.parseLong(m.group(2));
                long user = Long.parseLong(m.group(3));
                long kernel = Long.parseLong(m.group(4));
                if ("TOTAL".equals(name)) {
                    if (both < 100) {
                        mDataset.setValue("Idle", (100 - both));
                    }
                } else {
                    // Try to make graphs more useful even with rounding;
                    // log often has 0% user + 0% kernel = 1% total
                    // We arbitrarily give extra to kernel
                    if (user > 0) {
                        mDataset.setValue(name + " (user)", user);
                    }
                    if (kernel > 0) {
                        mDataset.setValue(name + " (kernel)" , both - user);
                    }
                    if (user == 0 && kernel == 0 && both > 0) {
                        mDataset.setValue(name, both);
                    }
                }
            }
        }
    }

    /**
     * Processes meminfo information from bugreport. Updates mDataset with the
     * new data.
     *
     * @param br Reader providing the content
     * @throws IOException if error reading file
     */
    void readMeminfoDataset(BufferedReader br) throws IOException {
        Pattern valuePattern = Pattern.compile("(\\d+) kB");
        long total = 0;
        long other = 0;
        mLabel.setText("PSS in kB");

        // Scan meminfo
        while (true) {
            String line = br.readLine();
            if (line == null) {
                // End of file
                break;
            }
            Matcher m = valuePattern.matcher(line);
            if (m.find()) {
                long kb = Long.parseLong(m.group(1));
                if (line.startsWith("MemTotal")) {
                    total = kb;
                } else if (line.startsWith("MemFree")) {
                    mDataset.setValue("Free", kb);
                    total -= kb;
                } else if (line.startsWith("Slab")) {
                    mDataset.setValue("Slab", kb);
                    total -= kb;
                } else if (line.startsWith("PageTables")) {
                    mDataset.setValue("PageTables", kb);
                    total -= kb;
                } else if (line.startsWith("Buffers") && kb > 0) {
                    mDataset.setValue("Buffers", kb);
                    total -= kb;
                } else if (line.startsWith("Inactive")) {
                    mDataset.setValue("Inactive", kb);
                    total -= kb;
                } else if (line.startsWith("MemFree")) {
                    mDataset.setValue("Free", kb);
                    total -= kb;
                }
            } else {
                break;
            }
        }
        // Scan procrank
        while (true) {
            String line = br.readLine();
            if (line == null) {
                break;
            }
            if (line.indexOf("PROCRANK") >= 0 || line.indexOf("PID") >= 0) {
                // procrank header
                continue;
            }
            if  (line.indexOf("----") >= 0) {
                //end of procrank section
                break;
            }
            // Extract pss field from procrank output
            long pss = Long.parseLong(line.substring(23, 31).trim());
            String cmdline = line.substring(43).trim().replace("/system/bin/", "");
            // Arbitrary minimum size to display
            if (pss > 2000) {
                mDataset.setValue(cmdline, pss);
            } else {
                other += pss;
            }
            total -= pss;
        }
        mDataset.setValue("Other", other);
        mDataset.setValue("Unknown", total);
    }

    /**
     * Processes sync information from bugreport. Updates mDataset with the new
     * data.
     *
     * @param br Reader providing the content
     * @throws IOException if error reading file
     */
    void readSyncDataset(BufferedReader br) throws IOException {
        while (true) {
            String line = br.readLine();
            if (line == null || line.startsWith("DUMP OF SERVICE")) {
                // Done, or moved on to the next service
                break;
            }
            if (line.startsWith(" |") && line.length() > 70) {
                String authority = line.substring(3, 18).trim();
                String duration = line.substring(61, 70).trim();
                // Duration is MM:SS or HH:MM:SS (DateUtils.formatElapsedTime)
                String durParts[] = duration.split(":");
                if (durParts.length == 2) {
                    long dur = Long.parseLong(durParts[0]) * 60 + Long
                            .parseLong(durParts[1]);
                    mDataset.setValue(authority, dur);
                } else if (duration.length() == 3) {
                    long dur = Long.parseLong(durParts[0]) * 3600
                            + Long.parseLong(durParts[1]) * 60 + Long
                            .parseLong(durParts[2]);
                    mDataset.setValue(authority, dur);
                }
            }
        }
    }
}
