//   Copyright 2012 Georg-August-Universität Göttingen, Germany
//
//   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 de.ugoe.cs.autoquest.androidmonitor;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.List;

import org.xmlpull.v1.XmlSerializer;

import android.app.Activity;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.util.Log;
import android.util.Xml;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;

/**
 * <p>
 * Writes informations about the device, the application, the components of the activity and the
 * events in a log file. File is stored in the space of the application. Path is normally internal
 * storage: data/data/<package name of the application using the AndroidMonitor>/files/ e.g.
 * data/data/de.ugoe.cs.autoquest.androidmonitor.testApp/files/
 * </p>
 * 
 * @author Florian Unger
 * @version 1.0
 */
public class AndroidMonitorLogFile {

    /**
     * <p>
     * Name of the log file which is stored in the internal space of the device.
     * </p>
     */
    private static String name;

    /**
     * <p>
     * Name of the Application which is monitored.
     * </p>
     */
    private static String appName;

    /**
     * <p>
     * File representation to store monitored information.
     * </p>
     */
    private static File file;

    /**
     * <p>
     * List representing all components which was written to log file before.
     * </p>
     */
    private static List<Integer> currentLoggedComponents;

    /**
     * 
     * <p>
     * Constructor. Creates a new AndroidmonitorLogFile.
     * </p>
     * 
     * @param appName
     *            Name of the calling application.
     * @param dir
     *            Folder to store the log file.
     */
    public AndroidMonitorLogFile(Activity activity) {
        createFile(activity);
    }

    /**
     * 
     * <p>
     * Get file name which is in use.
     * </p>
     * 
     * @return filename
     */
    public static String getFileName() {
        return AndroidMonitorLogFile.name;
    }

    /**
     * Get application name as defined in Package Name and write it to appName.
     * 
     * @param pContext
     *            package context; could also be an activity
     * @return app name
     */
    private String getAppLable(Context pContext) {
        // source:
        // http://stackoverflow.com/questions/11229219/android-get-application-name-not-package-name
        // (last call 2014-09-04)
        String appLabel;
        PackageManager lPackageManager = pContext.getPackageManager();
        ApplicationInfo lApplicationInfo = null;
        try {
            lApplicationInfo =
                lPackageManager.getApplicationInfo(pContext.getApplicationInfo().packageName, 0);
        }
        catch (final NameNotFoundException e) {
            
        }
        appLabel = (String) (lApplicationInfo != null ? lPackageManager
            .getApplicationLabel(lApplicationInfo) : "Unknown");
        return appLabel;
    }

    /**
     * 
     * <p>
     * Get package Name.
     * </p>
     * 
     * @param pContext
     *            package context; could also be an activity
     * @return package name
     */
    private String getAppPackageName(Context pContext) {
        return pContext.getPackageName();
    }

    /**
     * 
     * <p>
     * Writes information about the application to the log file.
     * </p>
     * 
     * @param activity
     * 
     */
    private void setAppInformation(Activity activity) {

        XmlSerializer serializer = Xml.newSerializer();
        StringWriter writer = new StringWriter();
        try {
            serializer.setOutput(writer);

            serializer.startTag("", "application");

            serializer.startTag("", "param");
            serializer.attribute("", "value", getAppPackageName(activity));
            serializer.attribute("", "name", "package");
            serializer.endTag("", "param");

            serializer.startTag("", "param");
            serializer.attribute("", "value", getAppLable(activity));
            serializer.attribute("", "name", "name");
            serializer.endTag("", "param");

            serializer.endTag("", "application");
            serializer.endDocument();

            writeToFile(writer.toString());
        }
        catch (IllegalArgumentException e) {
            e.printStackTrace();
            Log.e("file", "outputstream: " + e.getMessage());
        }
        catch (IllegalStateException e) {
            e.printStackTrace();
            Log.e("file", "outputstream: " + e.getMessage());
        }
        catch (IOException e) {
            e.printStackTrace();
            Log.e("file", "outputstream: " + e.getMessage());
        }
    }

    /**
     * <p>
     * Query device information and store it to log file.
     * </p>
     * 
     */
    private void setDeviceInformation() {

        XmlSerializer serializer = Xml.newSerializer();
        StringWriter writer = new StringWriter();
        try {
            serializer.setOutput(writer);
            serializer.startTag("", "device");
            serializer.startTag("", "param");
            serializer.attribute("", "value", "" + android.os.Build.VERSION.SDK_INT);
            serializer.attribute("", "name", "sdk_version");
            serializer.endTag("", "param");

            serializer.startTag("", "param");
            serializer.attribute("", "value", android.os.Build.DEVICE);
            serializer.attribute("", "name", "device");
            serializer.endTag("", "param");

            serializer.startTag("", "param");
            serializer.attribute("", "value", android.os.Build.MANUFACTURER);
            serializer.attribute("", "name", "manufacturer");
            serializer.endTag("", "param");

            serializer.startTag("", "param");
            serializer.attribute("", "value", android.os.Build.MODEL);
            serializer.attribute("", "name", "model");
            serializer.endTag("", "param");

            // TODO get resolution ...

            serializer.endTag("", "device");
            serializer.endDocument();

            writeToFile(writer.toString());

        }
        catch (IllegalArgumentException e) {
            e.printStackTrace();
            Log.e("file", "outputstream: " + e.getMessage());
        }
        catch (IllegalStateException e) {
            e.printStackTrace();
            Log.e("file", "outputstream: " + e.getMessage());
        }
        catch (IOException e) {
            e.printStackTrace();
            Log.e("file", "outputstream: " + e.getMessage());
        }

    }

    /**
     * <p>
     * Adds some information of an component of an activity (view) to the file.
     * </p>
     * 
     * @param view
     *            view to be logged
     * @param parentHash
     *            hash of the parent view
     * @param activityName
     *            name of the activity that is analyzed
     */
    public void addComponent(View view, int parentHash, String activityName, int position) {
        XmlSerializer serializer = Xml.newSerializer();
        StringWriter writer = new StringWriter();

        try {
            serializer.setOutput(writer);
            serializer.startTag("", "component");
            /*
             * (non-Javadoc) 
             * TODO find a way in that the hash code is unique over time and target
             * 
             * view.getId() seems to be unique over time and targets but there is a problem. In some
             * cases there is no ID (value: -1).
             * 
             * view.getId() returns different values in case of some identical components. 
             * E.g. in the application timerdroid the id of the list elements 
             * changes when calling home button.
             */
            serializer.attribute("", "hash", "" + view.hashCode());
            currentLoggedComponents.add(view.hashCode());

            /*
             * (non-Javadoc)
             * http://developer.android.com/reference/android/view/View.html#getId()
             * Returns this view's identifier.
             */
            serializer.startTag("", "param");
            serializer.attribute("", "name", "id");
            serializer.attribute("", "value", "" + view.getId());
            serializer.endTag("", "param");
            
            serializer.startTag("", "param");
            serializer.attribute("", "name", "position");
            serializer.attribute("", "value", "" + position);
            serializer.endTag("", "param");

            if (view instanceof TextView) {
                serializer.startTag("", "param");
                serializer.attribute("", "name", "title");
                TextView textView = (TextView) view;
                serializer.attribute("", "value", "" + textView.getText());
                serializer.endTag("", "param");
            }
            //TODO in case of an image add file name
            if (view instanceof ImageView) {
                serializer.startTag("", "param");
                serializer.attribute("", "name", "title");
                serializer.attribute("", "value", "image:" );
                serializer.endTag("", "param");
            }

            serializer.startTag("", "param");
            serializer.attribute("", "name", "path");
            serializer.attribute("", "value", activityName + "/" + getViewPath(view) +
                view.getClass().getSimpleName());
            serializer.endTag("", "param");

            serializer.startTag("", "param");
            serializer.attribute("", "name", "class");
            serializer.attribute("", "value", view.getClass().getName());
            serializer.endTag("", "param");

            serializer.startTag("", "param");
            serializer.attribute("", "name", "parent");
            /*
             * (non-Javadoc) 
             * Problem in using view.getParent().hashCode():
             * http://developer.android.com/reference/android/view/View.html#getParent() tells:
             * "... parent is a ViewParent and not necessarily a View." ViewParent does not have a
             * method hashCode(). Solution is done add parentHash as parameter to method
             * addComponent() and Androidmonitor-> addLogListenerToView.
             */
            serializer.attribute("", "value", "" + parentHash);
            serializer.endTag("", "param");

            serializer.startTag("", "ancestors");

            Class<? extends Object> classobject = view.getClass();

            while ((classobject != null)) {
                serializer.startTag("", "ancestor");
                serializer.attribute("", "name", classobject.getName());
                serializer.endTag("", "ancestor");
                classobject = classobject.getSuperclass();
            }
            serializer.endTag("", "ancestors");

            serializer.endTag("", "component");
            serializer.endDocument();

            writeToFile(writer.toString());

        }
        catch (IllegalArgumentException e) {
            e.printStackTrace();
            Log.e("file", "outputstream: " + e.getMessage());
        }
        catch (IllegalStateException e) {
            e.printStackTrace();
            Log.e("file", "outputstream: " + e.getMessage());
        }
        catch (IOException e) {
            e.printStackTrace();
            Log.e("file", "outputstream: " + e.getMessage());
        }

    }

    /**
     * <p>
     * Add an event to the log file
     * </p>
     * 
     * @param hash
     *            hash value of the calling view of the listener
     * @param type
     *            the type of listener e.g. textView ...
     * @param message
     *            message typed in
     */
    public void addEvent(int hash, String type, String message) {
        XmlSerializer serializer = Xml.newSerializer();
        StringWriter writer = new StringWriter();

        try {
            serializer.setOutput(writer);

            serializer.startTag("", "event");
            serializer.attribute("", "id", type);

            serializer.startTag("", "param");
            serializer.attribute("", "value", "" + hash);
            serializer.attribute("", "name", "source");
            serializer.endTag("", "param");

            serializer.startTag("", "param");
            serializer.attribute("", "value", message);
            serializer.attribute("", "name", "message");
            serializer.endTag("", "param");

            serializer.startTag("", "param");
            serializer.attribute("", "value", "" + System.currentTimeMillis());
            serializer.attribute("", "name", "timestamp");
            serializer.endTag("", "param");

            serializer.endTag("", "event");
            serializer.endDocument();

            writeToFile(writer.toString());
        }
        catch (IllegalArgumentException e) {
            e.printStackTrace();
            Log.e("file", "outputstream: " + e.getMessage());
        }
        catch (IllegalStateException e) {
            e.printStackTrace();
            Log.e("file", "outputstream: " + e.getMessage());
        }
        catch (IOException e) {
            e.printStackTrace();
            Log.e("file", "outputstream: " + e.getMessage());
        }
    }

    /**
     * <p>
     * Add an event to the log file
     * </p>
     * 
     * @param view
     *            the calling view of the listener
     * @param type
     *            the type of listener e.g. onClick ...
     */
    public void addEvent(View view, String type) {

        String x = "" + view.getX();
        String y = "" + view.getY();

        XmlSerializer serializer = Xml.newSerializer();
        StringWriter writer = new StringWriter();

        try {
            serializer.setOutput(writer);

            serializer.startTag("", "event");
            serializer.attribute("", "id", type);

            serializer.startTag("", "param");
            serializer.attribute("", "value", x);
            serializer.attribute("", "name", "X");
            serializer.endTag("", "param");

            serializer.startTag("", "param");
            serializer.attribute("", "value", y);
            serializer.attribute("", "name", "Y");
            serializer.endTag("", "param");

            serializer.startTag("", "param");
            serializer.attribute("", "value", "" + view.hashCode());
            serializer.attribute("", "name", "source");
            serializer.endTag("", "param");

            serializer.startTag("", "param");
            serializer.attribute("", "value", "" + System.currentTimeMillis());
            serializer.attribute("", "name", "timestamp");
            serializer.endTag("", "param");

            serializer.endTag("", "event");
            serializer.endDocument();

            writeToFile(writer.toString());
        }
        catch (IllegalArgumentException e) {
            e.printStackTrace();
            Log.e("file", "outputstream: " + e.getMessage());
        }
        catch (IllegalStateException e) {
            e.printStackTrace();
            Log.e("file", "outputstream: " + e.getMessage());
        }
        catch (IOException e) {
            e.printStackTrace();
            Log.e("file", "outputstream: " + e.getMessage());
        }
    }

    /**
     * 
     * <p>
     * Creates a new file to store information.
     * </p>
     * 
     * @param activity
     *            Calling application.
     */
    private void createFile(Activity activity) {
        
        AndroidMonitorLogFile.appName = getAppLable(activity);
        AndroidMonitorLogFile.name =
            "androidLogFile_" + AndroidMonitorLogFile.appName + System.currentTimeMillis() + ".log";
        try {
            AndroidMonitorLogFile.file =
                new File(activity.getFilesDir(), AndroidMonitorLogFile.name);
            if (AndroidMonitorLogFile.file.exists() && !AndroidMonitorLogFile.file.isDirectory()) {
                createFile(activity, 0);                
            }
            else {
                AndroidMonitorLogFile.currentLoggedComponents = new ArrayList<Integer>();
                writeHeaderToFile(activity);
                
            }
        }
        catch (Exception e) {
            e.printStackTrace();
            Log.e("AndroidMonitorLogFile.file", "file: " + e.getMessage());
        }
    }

    /**
     * 
     * <p>
     * Creates a new file to store information. Counts up if file exists.
     * </p>
     * 
     * @param activity
     *            Calling application.
     * @param count
     *            File number.
     */
    private void createFile(Activity activity, int count) {
        AndroidMonitorLogFile.name =
            "androidLogFile_" + count + "_" + AndroidMonitorLogFile.appName +
                System.currentTimeMillis() + ".log";
        
        try {
            AndroidMonitorLogFile.file =
                new File(activity.getFilesDir(), AndroidMonitorLogFile.name);
            if (AndroidMonitorLogFile.file.exists() && !AndroidMonitorLogFile.file.isDirectory()) {
                count++;
                createFile(activity, count);
            }
            else {
                writeHeaderToFile(activity);
            }
        }
        catch (Exception e) {
            e.printStackTrace();
            Log.e("AndroidMonitorLogFile.file", "file: " + e.getMessage());
        }
    }

    /**
     * 
     * <p>
     * Writes XML head, device and application information to file.
     * </p>
     * 
     * @param activity
     *            Calling application.
     */
    private void writeHeaderToFile(Activity activity) {
        writeToFile("<?xml version='1.0' encoding='UTF-8' standalone='yes' ?><sessions>");
        setDeviceInformation();
        setAppInformation(activity);
    }

    /**
     * <p>
     * Writes given information to the file. e.g. previous produced XML statements.
     * </p>
     * 
     * @param data
     *            content to add to the file
     */
    private void writeToFile(String data) {

        FileOutputStream outputStream;
        try {
            outputStream = new FileOutputStream(AndroidMonitorLogFile.file, true);
            outputStream.write(data.getBytes());
            outputStream.close();
        }
        catch (FileNotFoundException e) {
            e.printStackTrace();
            Log.e("file", "outputstream: " + e.getMessage());
        }
        catch (IOException e) {
            e.printStackTrace();
            Log.e("file", "outputstream: " + e.getMessage());
        }

    }

    /**
     * <p>
     * Generates the path of an view element.
     * </p>
     * 
     * @param view
     * @return path path to the element
     */
    private String getViewPath(View view) {
        return getViewPath(view, null);
    }

    /**
     * <p>
     * Generates the path of an view element.
     * </p>
     * 
     * @param view
     * @param path
     * @return path path to the element
     */
    private String getViewPath(View view, String path) {
        if (path == null) {
            path = "";
        }
        else {
            path = view.getClass().getSimpleName() + "/" + path;
        }
        if (view.getParent() != null && (view.getParent() instanceof ViewGroup)) {
            return getViewPath((View) view.getParent(), path);
        }
        else {
            return path;
        }
    }

    /**
     * 
     * <p>
     * Check whether a component is still written to log file.
     * </p>
     * 
     * @param hashCode
     *            hash code of the view
     * @return
     */
    public static Boolean isComponentLogged(Integer hashCode) {
        return AndroidMonitorLogFile.currentLoggedComponents.contains(hashCode);
    }

}
