/*
 * Copyright (C) 2015 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.messaging.util;

import android.app.Activity;
import android.app.AlertDialog;
import android.app.FragmentManager;
import android.app.FragmentTransaction;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.Environment;
import android.telephony.SmsMessage;
import android.text.TextUtils;
import android.widget.ArrayAdapter;

import com.android.messaging.Factory;
import com.android.messaging.R;
import com.android.messaging.datamodel.SyncManager;
import com.android.messaging.datamodel.action.DumpDatabaseAction;
import com.android.messaging.datamodel.action.LogTelephonyDatabaseAction;
import com.android.messaging.sms.MmsUtils;
import com.android.messaging.ui.UIIntents;
import com.android.messaging.ui.debug.DebugSmsMmsFromDumpFileDialogFragment;
import com.google.common.io.ByteStreams;

import java.io.BufferedInputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.StreamCorruptedException;

public class DebugUtils {
    private static final String TAG = "bugle.util.DebugUtils";

    private static boolean sDebugNoise;
    private static boolean sDebugClassZeroSms;
    private static MediaPlayer [] sMediaPlayer;
    private static final Object sLock = new Object();

    public static final int DEBUG_SOUND_SERVER_REQUEST = 0;
    public static final int DEBUG_SOUND_DB_OP = 1;

    public static void maybePlayDebugNoise(final Context context, final int sound) {
        if (sDebugNoise) {
            synchronized (sLock) {
                try {
                    if (sMediaPlayer == null) {
                        sMediaPlayer = new MediaPlayer[2];
                        sMediaPlayer[DEBUG_SOUND_SERVER_REQUEST] =
                                MediaPlayer.create(context, R.raw.server_request_debug);
                        sMediaPlayer[DEBUG_SOUND_DB_OP] =
                                MediaPlayer.create(context, R.raw.db_op_debug);
                        sMediaPlayer[DEBUG_SOUND_DB_OP].setVolume(1.0F, 1.0F);
                        sMediaPlayer[DEBUG_SOUND_SERVER_REQUEST].setVolume(0.3F, 0.3F);
                    }
                    if (sMediaPlayer[sound] != null) {
                        sMediaPlayer[sound].start();
                    }
                } catch (final IllegalArgumentException e) {
                    LogUtil.e(TAG, "MediaPlayer exception", e);
                } catch (final SecurityException e) {
                    LogUtil.e(TAG, "MediaPlayer exception", e);
                } catch (final IllegalStateException e) {
                    LogUtil.e(TAG, "MediaPlayer exception", e);
                }
            }
        }
    }

    public static boolean isDebugEnabled() {
        return BugleGservices.get().getBoolean(BugleGservicesKeys.ENABLE_DEBUGGING_FEATURES,
                BugleGservicesKeys.ENABLE_DEBUGGING_FEATURES_DEFAULT);
    }

    public abstract static class DebugAction {
        String mTitle;
        public DebugAction(final String title) {
            mTitle = title;
        }

        @Override
        public String toString() {
            return mTitle;
        }

        public abstract void run();
    }

    public static void showDebugOptions(final Activity host) {
        final AlertDialog.Builder builder = new AlertDialog.Builder(host);

        final ArrayAdapter<DebugAction> arrayAdapter = new ArrayAdapter<DebugAction>(
                host, android.R.layout.simple_list_item_1);

        arrayAdapter.add(new DebugAction("Dump Database") {
            @Override
            public void run() {
                DumpDatabaseAction.dumpDatabase();
            }
        });

        arrayAdapter.add(new DebugAction("Log Telephony Data") {
            @Override
            public void run() {
                LogTelephonyDatabaseAction.dumpDatabase();
            }
        });

        arrayAdapter.add(new DebugAction("Toggle Noise") {
            @Override
            public void run() {
                sDebugNoise = !sDebugNoise;
            }
        });

        arrayAdapter.add(new DebugAction("Force sync SMS") {
            @Override
            public void run() {
                final BuglePrefs prefs = BuglePrefs.getApplicationPrefs();
                prefs.putLong(BuglePrefsKeys.LAST_FULL_SYNC_TIME, -1);
                SyncManager.forceSync();
            }
        });

        arrayAdapter.add(new DebugAction("Sync SMS") {
            @Override
            public void run() {
                SyncManager.sync();
            }
        });

        arrayAdapter.add(new DebugAction("Load SMS/MMS from dump file") {
            @Override
            public void run() {
                new DebugSmsMmsDumpTask(host,
                        DebugSmsMmsFromDumpFileDialogFragment.ACTION_LOAD).executeOnThreadPool();
            }
        });

        arrayAdapter.add(new DebugAction("Email SMS/MMS dump file") {
            @Override
            public void run() {
                new DebugSmsMmsDumpTask(host,
                        DebugSmsMmsFromDumpFileDialogFragment.ACTION_EMAIL).executeOnThreadPool();
            }
        });

        arrayAdapter.add(new DebugAction("MMS Config...") {
            @Override
            public void run() {
                UIIntents.get().launchDebugMmsConfigActivity(host);
            }
        });

        arrayAdapter.add(new DebugAction(sDebugClassZeroSms ? "Turn off Class 0 sms test" :
                "Turn on Class Zero test") {
            @Override
            public void run() {
                sDebugClassZeroSms = !sDebugClassZeroSms;
            }
        });

        arrayAdapter.add(new DebugAction("Test sharing a file URI") {
            @Override
            public void run() {
                shareFileUri();
            }
        });

        builder.setAdapter(arrayAdapter,
                new android.content.DialogInterface.OnClickListener() {
            @Override
            public void onClick(final DialogInterface arg0, final int pos) {
                arrayAdapter.getItem(pos).run();
            }
        });

        builder.create().show();
    }

    /**
     * Task to list all the dump files and perform an action on it
     */
    private static class DebugSmsMmsDumpTask extends SafeAsyncTask<Void, Void, String[]> {
        private final String mAction;
        private final Activity mHost;

        public DebugSmsMmsDumpTask(final Activity host, final String action) {
            mHost = host;
            mAction = action;
        }

        @Override
        protected void onPostExecute(final String[] result) {
            if (result == null || result.length < 1) {
                return;
            }
            final FragmentManager fragmentManager = mHost.getFragmentManager();
            final FragmentTransaction ft = fragmentManager.beginTransaction();
            final DebugSmsMmsFromDumpFileDialogFragment dialog =
                    DebugSmsMmsFromDumpFileDialogFragment.newInstance(result, mAction);
            dialog.show(fragmentManager, ""/*tag*/);
        }

        @Override
        protected String[] doInBackgroundTimed(final Void... params) {
            final File dir = DebugUtils.getDebugFilesDir();
            return dir.list(new FilenameFilter() {
                @Override
                public boolean accept(final File dir, final String filename) {
                    return filename != null
                            && ((mAction == DebugSmsMmsFromDumpFileDialogFragment.ACTION_EMAIL
                            && filename.equals(DumpDatabaseAction.DUMP_NAME))
                            || filename.startsWith(MmsUtils.MMS_DUMP_PREFIX)
                            || filename.startsWith(MmsUtils.SMS_DUMP_PREFIX));
                }
            });
        }
    }

    /**
     * Dump the received raw SMS data into a file on external storage
     *
     * @param id The ID to use as part of the dump file name
     * @param messages The raw SMS data
     */
    public static void dumpSms(final long id, final android.telephony.SmsMessage[] messages,
            final String format) {
        try {
            final String dumpFileName = MmsUtils.SMS_DUMP_PREFIX + Long.toString(id);
            final File dumpFile = DebugUtils.getDebugFile(dumpFileName, true);
            if (dumpFile != null) {
                final FileOutputStream fos = new FileOutputStream(dumpFile);
                final DataOutputStream dos = new DataOutputStream(fos);
                try {
                    final int chars = (TextUtils.isEmpty(format) ? 0 : format.length());
                    dos.writeInt(chars);
                    if (chars > 0) {
                        dos.writeUTF(format);
                    }
                    dos.writeInt(messages.length);
                    for (final android.telephony.SmsMessage message : messages) {
                        final byte[] pdu = message.getPdu();
                        dos.writeInt(pdu.length);
                        dos.write(pdu, 0, pdu.length);
                    }
                    dos.flush();
                } finally {
                    dos.close();
                    ensureReadable(dumpFile);
                }
            }
        } catch (final IOException e) {
            LogUtil.e(LogUtil.BUGLE_TAG, "dumpSms: " + e, e);
        }
    }

    /**
     * Load MMS/SMS from the dump file
     */
    public static SmsMessage[] retreiveSmsFromDumpFile(final String dumpFileName) {
        SmsMessage[] messages = null;
        final File inputFile = DebugUtils.getDebugFile(dumpFileName, false);
        if (inputFile != null) {
            FileInputStream fis = null;
            DataInputStream dis = null;
            try {
                fis = new FileInputStream(inputFile);
                dis = new DataInputStream(fis);

                // SMS dump
                String format = null;
                final int chars = dis.readInt();
                if (chars > 0) {
                    format = dis.readUTF();
                }
                final int count = dis.readInt();
                final SmsMessage[] messagesTemp = new SmsMessage[count];
                for (int i = 0; i < count; i++) {
                    final int length = dis.readInt();
                    final byte[] pdu = new byte[length];
                    dis.read(pdu, 0, length);
                    messagesTemp[i] =
                            OsUtil.isAtLeastM()
                                    ? SmsMessage.createFromPdu(pdu, format)
                                    : SmsMessage.createFromPdu(pdu);
                }
                messages = messagesTemp;
            } catch (final FileNotFoundException e) {
                // Nothing to do
            } catch (final StreamCorruptedException e) {
                // Nothing to do
            } catch (final IOException e) {
                // Nothing to do
            } finally {
                if (dis != null) {
                    try {
                        dis.close();
                    } catch (final IOException e) {
                        // Nothing to do
                    }
                }
            }
        }
        return messages;
    }

    public static File getDebugFile(final String fileName, final boolean create) {
        final File dir = getDebugFilesDir();
        final File file = new File(dir, fileName);
        if (create && file.exists()) {
            file.delete();
        }
        return file;
    }

    public static File getDebugFilesDir() {
        final File dir = Environment.getExternalStorageDirectory();
        return dir;
    }

    /**
     * Load MMS/SMS from the dump file
     */
    public static byte[] receiveFromDumpFile(final String dumpFileName) {
        byte[] data = null;
        try {
            final File inputFile = getDebugFile(dumpFileName, false);
            if (inputFile != null) {
                final FileInputStream fis = new FileInputStream(inputFile);
                final BufferedInputStream bis = new BufferedInputStream(fis);
                try {
                    // dump file
                    data = ByteStreams.toByteArray(bis);
                    if (data == null || data.length < 1) {
                        LogUtil.e(LogUtil.BUGLE_TAG, "receiveFromDumpFile: empty data");
                    }
                } finally {
                    bis.close();
                }
            }
        } catch (final IOException e) {
            LogUtil.e(LogUtil.BUGLE_TAG, "receiveFromDumpFile: " + e, e);
        }
        return data;
    }

    public static void ensureReadable(final File file) {
        if (file.exists()){
            file.setReadable(true, false);
        }
    }

    /**
     * Logs the name of the method that is currently executing, e.g. "MyActivity.onCreate". This is
     * useful for surgically adding logs for tracing execution while debugging.
     * <p>
     * NOTE: This method retrieves the current thread's stack trace, which adds runtime overhead.
     * However, this method is only executed on eng builds if DEBUG logs are loggable.
     */
    public static void logCurrentMethod(String tag) {
        if (!LogUtil.isLoggable(tag, LogUtil.DEBUG)) {
            return;
        }
        StackTraceElement caller = getCaller(1);
        if (caller == null) {
            return;
        }
        String className = caller.getClassName();
        // Strip off the package name
        int lastDot = className.lastIndexOf('.');
        if (lastDot > -1) {
            className = className.substring(lastDot + 1);
        }
        LogUtil.d(tag, className + "." + caller.getMethodName());
    }

    /**
     * Returns info about the calling method. The {@code depth} parameter controls how far back to
     * go. For example, if foo() calls bar(), and bar() calls getCaller(0), it returns info about
     * bar(). If bar() instead called getCaller(1), it would return info about foo(). And so on.
     * <p>
     * NOTE: This method retrieves the current thread's stack trace, which adds runtime overhead.
     * It should only be used in production where necessary to gather context about an error or
     * unexpected event (e.g. the {@link Assert} class uses it).
     *
     * @return stack frame information for the caller (if found); otherwise {@code null}.
     */
    public static StackTraceElement getCaller(int depth) {
        // If the signature of this method is changed, proguard.flags must be updated!
        if (depth < 0) {
            throw new IllegalArgumentException("depth cannot be negative");
        }
        StackTraceElement[] trace = Thread.currentThread().getStackTrace();
        if (trace == null || trace.length < (depth + 2)) {
            return null;
        }
        // The stack trace includes some methods we don't care about (e.g. this method).
        // Walk down until we find this method, and then back up to the caller we're looking for.
        for (int i = 0; i < trace.length - 1; i++) {
            String methodName = trace[i].getMethodName();
            if ("getCaller".equals(methodName)) {
                return trace[i + depth + 1];
            }
        }
        // Never found ourself in the stack?!
        return null;
    }

    /**
     * Returns a boolean indicating whether ClassZero debugging is enabled. If enabled, any received
     * sms is treated as if it were a class zero message and displayed by the ClassZeroActivity.
     */
    public static boolean debugClassZeroSmsEnabled() {
        return sDebugClassZeroSms;
    }

    /** Shares a ringtone file via file URI. */
    private static void shareFileUri() {
        final String packageName = "com.android.messaging";
        final String fileName = "/system/media/audio/ringtones/Andromeda.ogg";

        Intent intent = new Intent(Intent.ACTION_SEND);
        intent.setPackage(packageName);
        intent.putExtra(Intent.EXTRA_STREAM, Uri.parse("file://" + fileName));
        intent.setType("image/*");
        Factory.get().getApplicationContext().startActivity(intent);
    }
}
