//   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.lang.reflect.Field;

import de.ugoe.cs.autoquest.androidmonitor.AndroidMonitorCompositeOnClickListener;
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.os.Build;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

/**
 * <p>
 * Monitor an android activity. Call this class via
 * AndroidMonitor.getInstanceOfAndroidMonitor().startMonitor(this); at the end of the
 * onCreate(Bundle savedInstanceState) method.
 * </p>
 * 
 * @author Florian Unger
 * @version 1.0
 */
public class AndroidMonitor {

    /**
     * <p>
     * Name of the Activity that starts the monitor.
     * </p>
     */
    String activityName;

    /**
     * <p>
     * Represents the operations to the log file.
     * </p>
     */
    private AndroidMonitorLogFile logFile;

    /**
     * Constructor method to get a monitor object.
     * 
     * @return monitor
     */
    public static AndroidMonitor getInstanceOfAndroidMonitor() {
        AndroidMonitor monitor = new AndroidMonitor();
        return monitor;
    }

    /**
     * starts tracking an activity
     * 
     * @param activity
     */
    public void startMonitor(Activity activity) {
        activityName = activity.getClass().getSimpleName();

        logFile = new AndroidMonitorLogFile(getAppLable(activity), activity.getFilesDir());

        addLogListenerToView(getRootView(activity));

        // TODO
        // listen to changes and update own listener
        // find out if it is possible to directly call addLogListenerToView
        // again
        // activity.onContentChanged();

        // write backPresss as event to xml file
        // activity.onBackPressed();

        // handle onStop() method of the activity
        // add a function that end up tracking if onStop() is given otherwise
        // create onStop()
        // http://developer.android.com/training/basics/activity-lifecycle/stopping.html
    }

    /**
     * get the root view of an activity
     * 
     * @param activity
     * @return
     */
    public View getRootView(Activity activity) {
        // get root view of the activity as start point
        View view = activity.getWindow().getDecorView().getRootView();
        // try out if the given node is the upper one in the tree and return the
        // first node of the tree
        // The root of the decorView could be embedded into another layout
        // element.
        return findFirstView(view);
    }

    /**
     * returns first view element of the tree
     * 
     * @param view
     * @return
     */
    private View findFirstView(View view) {
        if (view.getParent() != null && (view.getParent() instanceof ViewGroup)) {
            return findFirstView((View) view.getParent());
        }
        else {
            return view;
        }
    }

    /**
     * Replace the listener of each view with a composite listener which collects several listeners
     * for one view.
     * 
     * @param view
     *            to be monitored
     */
    public void addLogListenerToView(View view) {
        addLogListenerToView(view, 0);
    }

    /**
     * Replace the listener of each view with a composite listener which collects several listeners
     * for one view.
     * 
     * @param view
     *            to be monitored
     * @param parentHash
     *            hash of the parent view element
     */
    private void addLogListenerToView(View view, int parentHash) {

        if (!logFile.isComponentLogged(view.hashCode())) {
            logFile.addComponent(view, parentHash, activityName);
        }

        // hash code of the actual view element. Problem in using
        // view.getParent().hashCode() is described in
        // de.ugoe.cs.autoquest.androidmonitor#addComponent()
        parentHash = view.hashCode();
        // traverse all views of the activity
        if (view instanceof ViewGroup) {
            ViewGroup group = (ViewGroup) view;
            for (int i = 0; i < group.getChildCount(); i++) {
                View child = group.getChildAt(i);
                addLogListenerToView(child, parentHash);
            }
        }

        // save original listener to add it later on to the groupLisatener
        View.OnClickListener onClickListener = getOnClickListener(view);

        if (onClickListener != null) {
            // create new compositeOnClickListener to handle multiple listeners
            // for one view
            AndroidMonitorCompositeOnClickListener groupListener =
                new AndroidMonitorCompositeOnClickListener();
            // replace the original onClickListener with the
            // compositeOnClickListener
            view.setOnClickListener(groupListener);
            // add the tracking part as a several listener
            groupListener.addOnClickListener(new View.OnClickListener() {
                public void onClick(View v) {

                    logFile.addEvent(v, "onClick");
                }
            });

            groupListener.addOnClickListener(onClickListener);
        }
        // if view is a TextView add a addTextChangedListener to this view
        if (view instanceof TextView) {
            final int hashOfView = view.hashCode();
            TextView textView = (TextView) view;
            textView.addTextChangedListener(new TextWatcher() {
                public void beforeTextChanged(CharSequence s, int start, int count, int after) {
                    // do nothing
                }

                public void onTextChanged(CharSequence s, int start, int before, int count) {
                    // do nothing
                }

                public void afterTextChanged(Editable s) {
                    logFile.addEvent(hashOfView, "text", s.toString());
                }

            });
        }
    }

    /**
     * finds out if a listener exists
     * 
     * @param view
     * @return the listener of the view or null if no listener exists
     */
    public View.OnClickListener getOnClickListener(View view) {
        // http://stackoverflow.com/questions/11186960/getonclicklistener-in-android-views
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
            return getOnClickListenerV14(view);
        }
        else {
            return getOnClickListenerV(view);
        }
    }

    // Used for APIs lower than ICS (API 14)
    private View.OnClickListener getOnClickListenerV(View view) {
        View.OnClickListener retrievedListener = null;
        String viewStr = "android.view.View";
        Field field;

        try {
            field = Class.forName(viewStr).getDeclaredField("mOnClickListener");
            retrievedListener = (View.OnClickListener) field.get(view);
        }
        catch (NoSuchFieldException ex) {
            Log.e("Reflection", "No Such Field.");
        }
        catch (IllegalAccessException ex) {
            Log.e("Reflection", "Illegal Access.");
        }
        catch (ClassNotFoundException ex) {
            Log.e("Reflection", "Class Not Found.");
        }

        return retrievedListener;
    }

    // Used for new ListenerInfo class structure used beginning with API 14
    // (ICS)
    private View.OnClickListener getOnClickListenerV14(View view) {
        View.OnClickListener retrievedListener = null;
        String viewStr = "android.view.View";
        String lInfoStr = "android.view.View$ListenerInfo";

        try {
            Field listenerField = Class.forName(viewStr).getDeclaredField("mListenerInfo");
            Object listenerInfo = null;

            if (listenerField != null) {
                listenerField.setAccessible(true);
                listenerInfo = listenerField.get(view);
            }

            Field clickListenerField = Class.forName(lInfoStr).getDeclaredField("mOnClickListener");

            if (clickListenerField != null && listenerInfo != null) {
                retrievedListener = (View.OnClickListener) clickListenerField.get(listenerInfo);
            }
        }
        catch (NoSuchFieldException ex) {
            Log.e("Reflection", "No Such Field.");
        }
        catch (IllegalAccessException ex) {
            Log.e("Reflection", "Illegal Access.");
        }
        catch (ClassNotFoundException ex) {
            Log.e("Reflection", "Class Not Found.");
        }

        return retrievedListener;
    }

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