/* 
 * Copyright 2015 by AVM GmbH <info@avm.de>
 *
 * This software contains free software; you can redistribute it and/or modify 
 * it under the terms of the GNU General Public License ("License") as 
 * published by the Free Software Foundation  (version 3 of the License). 
 * This software is distributed in the hope that it will be useful, but 
 * WITHOUT ANY WARRANTY; without even the implied warranty of 
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the copy of the 
 * License you received along with this software for more details.
 */

package de.avm.fundamentals.logger;

import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.support.v4.app.ShareCompat;
import android.support.v4.content.FileProvider;
import android.util.Log;
import android.widget.ArrayAdapter;

import de.avm.fundamentals.R;
import de.avm.fundamentals.activities.LogActivity;

import java.io.*;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;

/**
 * FileLog is used for logging outputs into a file of the app environment.
 */
public class FileLog {

    /**
     * Default TAG for logging
     */
    private final static String TAG = FileLog.class.getSimpleName();

    private static final String TMP_LOG_FILE_NAME = "protokoll.txt";
    private static final String FILENAME_PREFIX = "protokoll_";
    private static final String FILE_TYPE = ".txt";
    public static final int KILOBYTE = 1024;

    private static Context mContext;
    private static SimpleDateFormat mLogTimestampFormatter;
    private static SimpleDateFormat mFileNameFormatter;

    private enum LogType {
        DEBUG {
            @Override
            public String getTypeString() {
                return "D/";
            }
        }, ERROR {
            @Override
            public String getTypeString() {
                return "E/";
            }
        }, INFO {
            @Override
            public String getTypeString() {
                return "I/";
            }
        }, VERBOSE {
            @Override
            public String getTypeString() {
                return "V/";
            }
        }, WARN {
            @Override
            public String getTypeString() {
                return "W/";
            }
        }, WTF {
            @Override
            public String getTypeString() {
                return "WTF/";
            }
        };

        public abstract String getTypeString();
    }

    private static String getFileSuffix() {
        return mFileNameFormatter.format(System.currentTimeMillis());
    }

    private FileLog() {}

    /**
     * Instantiate the FileLog and makes it available from everywhere. This should be instantiated in you Application-Class.
     *
     * @param context Context of your application class
     */
    public static void instantiate(final Context context) {
        if (mContext == null) {
            mContext = context;
            mLogTimestampFormatter = new SimpleDateFormat("MM-dd HH:mm:ss.SSS: ");
            mFileNameFormatter = new SimpleDateFormat("yyyyMMddHHmmss");
        }
    }

    public static boolean isInstantiated() {
        return mContext != null;
    }

    /**
     * same as {@link Log#d(String, String)} <br/>
     * Using "{@link FileLog}" as TAG for logging
     */
    public static void d(final String message) {
        Log.d(TAG, message);
        writeLog(LogType.DEBUG, TAG, message);
    }

    /**
     * @inheritDoc Log#d(String, String)
     */
    public static void d(final String tag, final String message) {
        Log.d(tag, message);
        writeLog(LogType.DEBUG, tag, message);
    }

    /**
     * same as {@link Log#d(String, String, Throwable)}
     */
    public static void d(final String tag, final String message, final Throwable exception) {
        Log.d(tag, message, exception);
        writeLog(LogType.DEBUG, tag, message, exception);
    }

    /**
     * same as {@link Log#e(String, String)}
     * Using "{@link FileLog}" as TAG for logging
     */
    public static void e(final String message) {
        Log.e(TAG, message);
        writeLog(LogType.ERROR, TAG, message);
    }

    /**
     * same as {@link Log#e(String, String)}
     */
    public static void e(final String tag, final String message) {
        Log.e(tag, message);
        writeLog(LogType.ERROR, tag, message);
    }

    /**
     * same as {@link Log#e(String, String, Throwable)}
     */
    public static void e(final String tag, final String message, final Throwable exception) {
        Log.e(tag, message, exception);
        writeLog(LogType.ERROR, tag, message, exception);
    }

    /**
     * same as {@link Log#i(String, String)}
     * Using "{@link FileLog}" as TAG for logging
     */
    public static void i(final String message) {
        Log.i(TAG, message);
        writeLog(LogType.INFO, TAG, message);
    }

    /**
     * same as {@link Log#i(String, String)}
     */
    public static void i(final String tag, final String message) {
        Log.i(tag, message);
        writeLog(LogType.INFO, tag, message);
    }

    /**
     * same as {@link Log#i(String, String, Throwable)}
     */
    public static void i(final String tag, final String message, final Throwable exception) {
        Log.i(tag, message, exception);
        writeLog(LogType.INFO, tag, message, exception);
    }

    /**
     * same as {@link Log#v(String, String)}
     * Using "{@link FileLog}" as TAG for logging
     */
    public static void v(final String message) {
        Log.v(TAG, message);
        writeLog(LogType.VERBOSE, TAG, message);
    }

    /**
     * same as {@link Log#v(String, String)}
     */
    public static void v(final String tag, final String message) {
        Log.v(tag, message);
        writeLog(LogType.VERBOSE, tag, message);
    }

    /**
     * same as {@link Log#v(String, String, Throwable)}
     */
    public static void v(final String tag, final String message, final Throwable exception) {
        Log.v(tag, message, exception);
        writeLog(LogType.VERBOSE, tag, message, exception);
    }

    /**
     * same as {@link Log#w(String, String)}
     * Using "{@link FileLog}" as TAG for logging
     */
    public static void w(final String message) {
        Log.w(TAG, message);
        writeLog(LogType.WARN, TAG, message);
    }

    /**
     * same as {@link Log#w(String, String)}
     */
    public static void w(final String tag, final String message) {
        Log.w(tag, message);
        writeLog(LogType.WARN, tag, message);
    }

    /**
     * same as {@link Log#w(String, String, Throwable)}
     */
    public static void w(final String tag, final String message, final Throwable exception) {
        Log.w(tag, message, exception);
        writeLog(LogType.WARN, tag, message, exception);
    }

    /**
     * same as {@link Log#wtf(String, String)}
     * Using "{@link FileLog}" as TAG for logging
     */
    public static void wtf(final String message) {
        Log.d(TAG, message);
        writeLog(LogType.WTF, TAG, message);
    }

    /**
     * same as {@link Log#wtf(String, String)}
     */
    public static void wtf(final String tag, final String message) {
        Log.d(tag, message);
        writeLog(LogType.WTF, tag, message);
    }

    /**
     * same as {@link Log#wtf(String, String, Throwable)}
     */
    public static void wtf(final String tag, final String message, final Throwable exception) {
        Log.wtf(tag, message, exception);
        writeLog(LogType.WTF, tag, message);
    }

    private static void writeLog(LogType type, String tag, String message, Throwable exception) {
        checkIfIsInstantiated();
        new LogWriterTask().execute(getLogLine(type, tag, message, exception));
    }

    private static void writeLog(LogType type, String tag, String message) {
        checkIfIsInstantiated();
        new LogWriterTask().execute(getLogLine(type, tag, message, null));
    }

    private static String getLogLine(LogType type, String tag, String message, Throwable exception) {
        StringBuilder builder = new StringBuilder();
        builder.append(mLogTimestampFormatter.format(System.currentTimeMillis()));
        builder.append(type.getTypeString()).append(tag).append(": ");
        builder.append(message);
        appendStackTrace(builder, exception);
        return builder.toString();
    }

    private static void appendStackTrace(final StringBuilder builder, final Throwable exception) {
        for (Throwable exp = exception; exp != null; exp = exp.getCause()) {
            builder.append(LogWriter.NEW_LINE);
            if (exp != exception) {
                builder.append("caused by: ");
            }
            builder.append(exp.getClass().getName()).append(": ").append(exp.getMessage());
            for (StackTraceElement element : exception.getStackTrace()) {
                builder.append(LogWriter.NEW_LINE);
                builder.append("\t at ").append(element.toString());
            }
        }
    }

    private static void checkIfIsInstantiated() {
        if (mContext == null) {
            throw new IllegalStateException(FileLog.class.getSimpleName() + " is not instantiated, call " + FileLog.class.getSimpleName() + ".instantiate(context) in your Application class");
        }
    }

    /**
     * @return the log file content as a {@link java.util.ArrayList}
     */
    public static ArrayList<String> getLogAsList() {
        try {
            ArrayList<String> log = new ArrayList<String>();
            File[] sortedFiles = getSortedFiles();
            BufferedReader reader;
            String line;
            for (File file : sortedFiles) {
                reader = getBufferedReader(file);
                while ((line = reader.readLine()) != null) {
                    log.add(line);
                }
                reader.close();
            }
            return log;
        } catch (IOException e) {
            return new ArrayList<String>();
        }
    }

    private static BufferedReader getBufferedReader(final File file) throws FileNotFoundException {
        FileInputStream fileInputStream = new FileInputStream(file);
        return new BufferedReader(new InputStreamReader(fileInputStream));
    }

    private static void deleteOldLogFilesIfNeeded() {
        new LogRemoverTask().execute();
    }

    protected static File getRecentLogFile() {
        checkIfIsInstantiated();
        File[] files = getSortedFiles();
        if (files.length >= 2) {
            deleteOldLogFilesIfNeeded();
        }

        for (File file : files) {
            if ((file.length() / KILOBYTE) < 100) {
                return file;
            }
        }

        return new File(mContext.getFilesDir(), getFileName());
    }

    /**
     * @return an Array of all log files sorted by name
     */
    protected static File[] getSortedFiles() {
        File[] files = mContext.getFilesDir().listFiles(createFileFilter());
        sortFilesByName(files);
        return files;
    }

    private static FilenameFilter createFileFilter() {
        return new FilenameFilter() {
            @Override
            public boolean accept(File dir, String filename) {
                return filename.startsWith(FILENAME_PREFIX);
            }
        };
    }

    private static void sortFilesByName(File[] files) {
        Arrays.sort(files, new Comparator<File>() {
            @Override
            public int compare(File lhs, File rhs) {
                return lhs.getName().compareTo(rhs.getName());
            }
        });
    }

    private static String getFileName() {
        return FILENAME_PREFIX + getFileSuffix() + FILE_TYPE;
    }

    /**
     * Returns the {@link android.net.Uri} to send the log file (if it exists) as a attachment of an E-Mail.
     * To use this Uri, you need to add a provider to your AndroidManifest.xml as following: <br/>
     * <pre><code>
     * &#60;provider android:name="android.support.v4.content.FileProvider"
     *      android:authorities="your.package.name"
     *      android:exported="false"
     *      android:grantUriPermissions="true"&#62;
     *      &#60;meta-data
     *          android:name="android.support.FILE_PROVIDER_PATHS"
     *          android:resource="@xml/file_path" /&#62;
     *  &#60;/provider&#62;</code></pre>
     *
     * @return the {@link android.net.Uri} to send the log file as a attachment of an E-Mail
     */
    public static Uri getLogFileUri() throws IOException {
        checkIfIsInstantiated();
        try {
            File tmpFile = concatenateLogFiles();

            if (tmpFile != null) {
                return FileProvider.getUriForFile(mContext, mContext.getPackageName(), tmpFile);
            }
        } catch (NullPointerException e) {
            e.printStackTrace();
            throw new IllegalStateException("You need to add the <provider> tag to your AndroidManifest. See: https://developer.android.com/reference/android/support/v4/content/FileProvider.html");
        }

        return null;
    }

    private static File concatenateLogFiles() throws IOException {
        File tmpFile = new File(mContext.getFilesDir(), TMP_LOG_FILE_NAME);
        tmpFile.setReadable(true, false);
        FileWriter fileWriter = new FileWriter(tmpFile, false);
        File[] sortedFiles = getSortedFiles();

        if (sortedFiles.length > 0) {
            BufferedReader reader;
            String line;
            for (File file : sortedFiles) {
                reader = getBufferedReader(file);
                while ((line = reader.readLine()) != null) {
                    fileWriter.append(line);
                    fileWriter.append(LogWriter.NEW_LINE);
                }
                reader.close();
            }
            fileWriter.close();
            return tmpFile;
        } else {
            return null;
        }
    }

    /**
     * Shows a {@link android.app.AlertDialog} with the log file as a scrollable list
     *
     * @param context
     */
    public static void showLogFileDialog(final Context context) {
        AlertDialog.Builder builder = new AlertDialog.Builder(context);
        builder.setTitle(R.string.log_title);
        builder.setCancelable(true);
        final ArrayAdapter<String> arrayAdapter = new ArrayAdapter<String>(context, R.layout.log_list_entry);
        arrayAdapter.addAll(FileLog.getLogAsList());
        builder.setAdapter(arrayAdapter, null);
        builder.setPositiveButton(R.string.ok, null);
        builder.show();
    }

    /**
     * Shows an {@link android.app.Activity} with the log file as a scrollable list.<br/>
     * You need to add
     * <pre><code>&#60;activity android:name="de.avm.fundamentals.activities.LogActivity"/&#62;</code></pre>
     * to your AndroidManifest.xml
     *
     * @param context
     */
    public static void showLogFileActivity(final Context context) {
        context.startActivity(new Intent(context, LogActivity.class));
    }
}
