//   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.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>
     */
    private String activityName;

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

    /**
     * <p>
     * Represents the position of the current view element in the tree structure of an activity.
     * </p>
     */
    private int elementPosition = 0;

    /**
     * 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) {
        // create logFile only once.
        if (AndroidMonitor.logFile == null) {
            AndroidMonitor.logFile = new AndroidMonitorLogFile(activity);
            Log.i("logfile", "file created");
        }
        activityName = activity.getClass().getSimpleName();
        addLogListenerToView(getRootView(activity));

        /*
         * TODO listen to changes and update own listener. - Listen to known elements is implemented
         * and trigger startMonitot(View).
         */
        /*
         * TODO write backPresss as event to xml file activity.onBackPressed();
         */
        /*
         * TODO handle onStop() method of the activity add a function that end up tracking if
         * onStop() is given otherwise. Maybe: create onStop()
         * http://developer.android.com/training/basics/activity-lifecycle/stopping.html
         */
    }

    public void startMonitor(View view) {
        addLogListenerToView(findFirstView(view));
    }

    /**
     * Get the root view of an activity.
     * 
     * @param activity
     * @return
     */
    private 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) {
        elementPosition = 0;
        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. Add a TextWatcher to all TexView views.
     * 
     * @param view
     *            to be monitored
     */
    private 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 (!AndroidMonitorLogFile.isComponentLogged(view.hashCode())) {
            AndroidMonitor.logFile.addComponent(view, parentHash, activityName, getElementPosition());

            // Log.i("object type", "");

            // 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) {
                        AndroidMonitor.logFile.addEvent(v, "onClick");
                        // check if something changed in the activity
                        startMonitor(v);
                    }
                });

                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) {
                        AndroidMonitor.logFile.addEvent(hashOfView, "text", s.toString());
                    }

                });
            }
        }

        // hash code of the actual view element. Problem in using
        // view.getParent().hashCode() is described in
        // de.ugoe.cs.autoquest.androidmonitor.AndroidMonitorLogFile#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);
            }
        }

    }

    /**
     * finds out if a listener exists
     * 
     * @param view
     * @return the listener of the view or null if no listener exists
     */
    // source: http://stackoverflow.com/questions/11186960/getonclicklistener-in-android-views
    private View.OnClickListener getOnClickListener(View view) {
        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)
    // source: http://stackoverflow.com/questions/11186960/getonclicklistener-in-android-views
    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)
    // source: http://stackoverflow.com/questions/11186960/getonclicklistener-in-android-views
    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;
    }

    /**
     * 
     * <p>
     * Return the AndroidMonitorLogFile that is used in the AndroidMonitor.
     * </p>
     * 
     * @return AndroidMonitorLogFile that is used in the AndroidMonitor.
     */
    public AndroidMonitorLogFile getLogFile() {
        return logFile;
    }
    
    private int getElementPosition(){
        int element = elementPosition;
        elementPosition ++;
        return element;
    }
}
