package com.vpedak.testsrecorder.core;

import android.app.Activity;
import android.app.Instrumentation;
import android.content.res.Resources;
import android.os.Build;
import android.os.Handler;
import android.support.annotation.Nullable;
import android.support.v4.view.ViewPager;
import android.support.v7.widget.RecyclerView;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.webkit.WebView;
import android.widget.*;

import com.vpedak.testsrecorder.core.events.*;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.concurrent.ConcurrentHashMap;

public class ActivityProcessor {
    private long uniqueId;
    private final Instrumentation instrumentation;
    public static final String ANDRIOD_TEST_RECORDER = "Android Test Recorder";
    private ConcurrentHashMap<View, View> allViews = new ConcurrentHashMap<View, View>();
    private EventWriter eventWriter;
    private Activity activity;
    private MenuProcessor menuProcessor;
    private CheckableProcessor checkableProcessor;
    private AdapterViewProcessor adapterViewProcessor;

    private static final int SWIPE_THRESHOLD = 100;
    private static final int SWIPE_VELOCITY_THRESHOLD = 100;

    public ActivityProcessor(long uniqueId, Instrumentation instrumentation, EventWriter eventWriter) {
        this.uniqueId = uniqueId;
        this.instrumentation = instrumentation;
        this.eventWriter = eventWriter;
        menuProcessor = new MenuProcessor(this);
        checkableProcessor = new CheckableProcessor(this);
        adapterViewProcessor = new AdapterViewProcessor(this);
    }

    public EventWriter getEventWriter() {
        return eventWriter;
    }

    public void processAllViews() {
        View[] views = getWMViews();

        for (int i = 0; i < views.length; i++) {
            final View view = views[i];

            if (!allViews.contains(view)) {
                activity.runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        processView(view);
                    }
                });
            }
        }
    }

    public void processActivity(final Activity activity) {
        this.activity = activity;

        activity.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                menuProcessor.processActivity(activity);

                View view = activity.getWindow().getDecorView();
                processView(view);
            }
        });
    }

    private void processView(View view) {
        if (view instanceof WebView) {
            // skip web view processing since it is not supported yet
            return;
        }

        View tst = allViews.putIfAbsent(view, view);

        if (tst != null) {
            return;
        }

        menuProcessor.processView(view);

        processClick(view);

        processTouch(view);


        try {
            if (view instanceof ViewPager) {
                ViewPager viewPager = (ViewPager) view;
                processViewPager(viewPager);
            }
        } catch (NoClassDefFoundError e) {
            // ViewPager is not used in this project, ignore this
        }


        if (view instanceof EditText) {
            processTextView((TextView) view);
        } else if (view instanceof AdapterView) {
            adapterViewProcessor.processView((AdapterView) view);
        }

        if (view instanceof ViewGroup) {
            ViewGroup viewGroup = (ViewGroup) view;

            for (int i = 0; i < viewGroup.getChildCount(); i++) {
                View child = viewGroup.getChildAt(i);

                processView(child);
            }

            ViewGroup.OnHierarchyChangeListener listener = null;
            try {
                Field f = ViewGroup.class.getDeclaredField("mOnHierarchyChangeListener");
                f.setAccessible(true);
                listener = (ViewGroup.OnHierarchyChangeListener) f.get(view);
            } catch (IllegalAccessException e) {
                Log.e(ANDRIOD_TEST_RECORDER, "IllegalAccessException", e);
            } catch (NoSuchFieldException e) {
                Log.e(ANDRIOD_TEST_RECORDER, "NoSuchFieldException", e);
            }
            final ViewGroup.OnHierarchyChangeListener finalListener = listener;
            viewGroup.setOnHierarchyChangeListener(new ViewGroup.OnHierarchyChangeListener() {
                @Override
                public void onChildViewAdded(View parent, final View child) {

                    // make a delay in new view processing to make sure that all listeners will be already attached during Activity.OnCreate, etc
                    // not sure how to make this better (to wait for listeners attachment)
                    activity.runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            final Handler handler = new Handler();
                            handler.postDelayed(new Runnable() {
                                @Override
                                public void run() {
                                    processView(child);
                                }
                            }, 200);
                        }
                    });

                    if (finalListener != null) {
                        finalListener.onChildViewAdded(parent, child);
                    }
                }

                @Override
                public void onChildViewRemoved(View parent, View child) {
                    if (finalListener != null) {
                        finalListener.onChildViewRemoved(parent, child);
                    }
                }
            });
        }
    }

    private void processViewPager(final ViewPager viewPager) {
        viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
            @Override
            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
            }

            @Override
            public void onPageSelected(int position) {
                ResolveSubjectResult result = resolveSubject(viewPager);
                if (result != null) {
                    eventWriter.writeEvent(new RecordingEvent(result.getSubject(), new SelectViewPagerPageAction(position),
                            "Select page " + position + " in " + getWidgetName(viewPager) + generateSubjectDescription(result.getSubject())));
                }
            }

            @Override
            public void onPageScrollStateChanged(int state) {
            }
        });
    }

    private void processTouch(final View view) {
        View.OnTouchListener listener = null;
        try {
            Field f = View.class.getDeclaredField("mListenerInfo");
            f.setAccessible(true);
            Object li = f.get(view);

            if (li != null) {
                Field f2 = li.getClass().getDeclaredField("mOnTouchListener");
                f2.setAccessible(true);
                listener = (View.OnTouchListener) f2.get(li);

            }
        } catch (IllegalAccessException e) {
            Log.e(ANDRIOD_TEST_RECORDER, "IllegalAccessException", e);
        } catch (NoSuchFieldException e) {
            Log.e(ANDRIOD_TEST_RECORDER, "NoSuchFieldException", e);
        }

        final GestureDetector gestureDetector = new GestureDetector(instrumentation.getTargetContext(), new GestureDetector.OnGestureListener() {
            @Override
            public boolean onDown(MotionEvent e) {
                return false;
            }

            @Override
            public void onShowPress(MotionEvent e) {
            }

            @Override
            public boolean onSingleTapUp(MotionEvent e) {
                return false;
            }

            @Override
            public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
                return false;
            }

            @Override
            public void onLongPress(MotionEvent e) {
            }

            @Override
            public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
                if (e1 == null || e2 == null) {
                    return false;
                }

                Action action = null;
                String str = null;

                float diffY = e2.getY() - e1.getY();
                float diffX = e2.getX() - e1.getX();
                if (Math.abs(diffX) > Math.abs(diffY)) {
                    if (Math.abs(diffX) > SWIPE_THRESHOLD && Math.abs(velocityX) > SWIPE_VELOCITY_THRESHOLD) {
                        if (diffX > 0) {
                            // swipe right
                            action = new SwipeRightAction();
                            str = "Swipe right at ";
                        } else {
                            // swipe left
                            action = new SwipeLeftAction();
                            str = "Swipe left at ";
                        }
                    }
                } else if (Math.abs(diffY) > SWIPE_THRESHOLD && Math.abs(velocityY) > SWIPE_VELOCITY_THRESHOLD) {
                    if (diffY > 0) {
                        // swipe down
                        action = new SwipeDownAction();
                        str = "Swipe down at ";
                    } else {
                        // swipe up
                        action = new SwipeUpAction();
                        str = "Swipe up at ";
                    }
                }

                if (action != null) {
                    AdapterView adapterView = getAdaptedView(view);

                    if (adapterView != null) {
                        // view is inside adapter view
                        int pos = adapterView.getPositionForView(view);
                        AdapterViewProcessor.generateEvent(ActivityProcessor.this, pos, adapterView, str + "item ", action);
                    } else {
                        ResolveSubjectResult result = resolveSubject(view);
                        if (result != null) {
                            String descr = str + getWidgetName(view) + generateSubjectDescription(result.getSubject());
                            if (result.getScrollToEvent() != null) {
                                result.getScrollToEvent().setDescription(descr);
                                eventWriter.writeEvent(result.getScrollToEvent());
                                eventWriter.writeEvent(new RecordingEvent(result.getScrollToEvent().getGroup(),result.getSubject(), action));
                            } else {
                                eventWriter.writeEvent(new RecordingEvent(result.getSubject(), action, descr));
                            }
                        }
                    }
                }

                return false;
            }
        });

        final View.OnTouchListener finalListener = listener;
        view.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                boolean result = false;

                if (finalListener != null) {
                    result = finalListener.onTouch(v, event);
                } else {
                    result = view.onTouchEvent(event);
                }

                if (result) {
                    gestureDetector.onTouchEvent(event);
                }

                return result;
            }
        });
    }

    private String generateSubjectDescription(Subject subject) {
        if (subject instanceof com.vpedak.testsrecorder.core.events.View) {
            com.vpedak.testsrecorder.core.events.View view = (com.vpedak.testsrecorder.core.events.View) subject;
            return " with id " + view.getId();
        } else if (subject instanceof DisplayedView) {
            DisplayedView view = (DisplayedView) subject;
            return " with id "+view.getId();
        } else if (subject instanceof  ParentView) {
            ParentView view = (ParentView) subject;
            return " with child index "+view.getChildIndex()+" of parent with id "+view.getParentId();
        } else if (subject instanceof  ParentIdView) {
            ParentIdView view = (ParentIdView) subject;
            return " with child id "+view.getChildId()+" of parent with id "+view.getParentId();
        }
        return "";
    }

    private void processTextView(final TextView view) {
        view.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
            }

            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {
                ResolveSubjectResult result = resolveSubject(view);
                if (result != null) {
                    String text = view.getText().toString();
                    String descr = "Set text to '" + text + "' in " + getWidgetName(view) + generateSubjectDescription(result.getSubject());
                    String delayedId = result.getSubject().toString();
                    if (result.getScrollToEvent() != null) {
                        result.getScrollToEvent().setDescription(descr);
                        eventWriter.addDelayedEvent(delayedId+"_scroll", result.getScrollToEvent());
                        eventWriter.addDelayedEvent(delayedId, new RecordingEvent(result.getScrollToEvent().getGroup(),
                                result.getSubject(), new ReplaceTextAction(text)));
                    } else {
                        eventWriter.addDelayedEvent(delayedId, new RecordingEvent(result.getSubject(), new ReplaceTextAction
                                (text), descr));
                    }
                }
            }

            @Override
            public void afterTextChanged(Editable s) {
            }
        });
    }

    private void processClick(final View view) {
        if (view instanceof Spinner) {
            return;
        }

        View.OnClickListener listener = null;
        View.OnLongClickListener longListener = null;
        try {
            Field f = View.class.getDeclaredField("mListenerInfo");
            f.setAccessible(true);
            Object li = f.get(view);

            if (li != null) {
                Field f2 = li.getClass().getDeclaredField("mOnClickListener");
                f2.setAccessible(true);
                listener = (View.OnClickListener) f2.get(li);

                Field f3 = li.getClass().getDeclaredField("mOnLongClickListener");
                f3.setAccessible(true);
                longListener = (View.OnLongClickListener) f3.get(li);
            }
        } catch (IllegalAccessException e) {
            Log.e(ANDRIOD_TEST_RECORDER, "IllegalAccessException", e);
        } catch (NoSuchFieldException e) {
            Log.e(ANDRIOD_TEST_RECORDER, "NoSuchFieldException", e);
        }

        if (view.isClickable() && listener != null) {
            final View.OnClickListener finalListener = listener;
            view.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    AdapterView adapterView = getAdaptedView(view);

                    if (adapterView != null) {
                        // view is inside adapter view
                        int pos = adapterView.getPositionForView(view);
                        AdapterViewProcessor.generateClickEvent(ActivityProcessor.this, pos, adapterView);
                    } else {
                        ResolveSubjectResult result = resolveSubject(view);
                        if (result != null) {
                            String descr = "Click at " + getWidgetName(view) + generateSubjectDescription(result.getSubject());
                            if (result.getScrollToEvent() != null) {
                                result.getScrollToEvent().setDescription(descr);
                                eventWriter.writeEvent(result.getScrollToEvent());
                                eventWriter.writeEvent(new RecordingEvent(
                                        result.getScrollToEvent().getGroup(),
                                        result.getSubject(),
                                        new ClickAction()));
                            } else {
                                eventWriter.writeEvent(new RecordingEvent(result.getSubject(), new ClickAction(), descr));
                            }
                        }
                    }
                    finalListener.onClick(v);
                }
            });
        }

        if (view.isLongClickable()) {
            final View.OnLongClickListener finalLongListener = longListener;
            view.setOnLongClickListener(new View.OnLongClickListener() {
                @Override
                public boolean onLongClick(View v) {
                    AdapterView adapterView = getAdaptedView(view);

                    if (adapterView != null) {
                        // view is inside adapter view
                        int pos = adapterView.getPositionForView(view);
                        AdapterViewProcessor.generateLongClickEvent(ActivityProcessor.this, pos, adapterView);
                    } else {
                        ResolveSubjectResult result = resolveSubject(view);
                        if (result != null) {
                            String descr = "Long click at " + getWidgetName(view) + generateSubjectDescription(result.getSubject());
                            if (result.getScrollToEvent() != null) {
                                result.getScrollToEvent().setDescription(descr);
                                eventWriter.writeEvent(result.getScrollToEvent());
                                eventWriter.writeEvent(new RecordingEvent(
                                        result.getScrollToEvent().getGroup(),
                                        result.getSubject(), new LongClickAction()));
                            } else {
                                eventWriter.writeEvent(new RecordingEvent(result.getSubject(), new LongClickAction(), descr));
                            }
                        }

                    }

                    if (finalLongListener != null) {
                        return finalLongListener.onLongClick(v);
                    } else {
                        return false;
                    }
                }
            });
        }

        if (view instanceof CompoundButton) {
            checkableProcessor.processClick((CompoundButton) view);
        }
    }

    private AdapterView getAdaptedView(View view) {
        ViewParent parent = view.getParent();
        while (parent != null) {
            if (parent instanceof AdapterView) {
                return (AdapterView) parent;
            }
            parent = parent.getParent();
        }
        return null;
    }

    private boolean isInsideScrollView(String viewId, View view) {
        ViewParent parent = view.getParent();
        while (parent != null) {
            if (parent instanceof ScrollView || parent instanceof HorizontalScrollView) {
                return true;
            }
            parent = parent.getParent();
        }
        return false;
    }

    @Nullable
    public ResolveSubjectResult resolveSubject(View view) {
        String viewId = resolveId(view.getId());
        if (viewId != null) {
            try {
                if (view.getParent() instanceof RecyclerView) {
                    RecyclerView parentView = (RecyclerView) view.getParent();

                    String parentId = resolveId(parentView.getId());
                    if (parentId != null) {
                        for (int i = 0; i < parentView.getChildCount(); i++) {
                            if (view.equals(parentView.getChildAt(i))) {
                                RecordingEvent scrollToEvent =
                                        new RecordingEvent(String.valueOf(System.currentTimeMillis()),
                                                new com.vpedak.testsrecorder.core.events.View(parentId),
                                                new ScrollToPositionAction(i));
                                return new ResolveSubjectResult(new ParentView(parentId, i), scrollToEvent);
                            }
                        }
                    }
                }
            } catch (NoClassDefFoundError e) {
                // RecyclerView is not used in this project, ignore this
            }

            Subject subject = checkIdDuplication(viewId, view);

            if (isInsideScrollView(viewId, view)) {
                RecordingEvent scrollToEvent = new RecordingEvent(String.valueOf(System.currentTimeMillis()),
                        subject,
                        new ScrollToAction());
                return new ResolveSubjectResult(subject, scrollToEvent);
            } else {
                return new ResolveSubjectResult(subject);
            }
        } else if (view.getParent() != null && view.getParent() instanceof ViewGroup) {
            ViewGroup parentView = (ViewGroup) view.getParent();
            String parentId = resolveId(parentView.getId());

            if (parentId != null) {
                for (int i = 0; i < parentView.getChildCount(); i++) {
                    if (view.equals(parentView.getChildAt(i))) {
                        RecordingEvent scrollToEvent = null;
                        try {
                            if (parentView instanceof RecyclerView) {
                                scrollToEvent =
                                        new RecordingEvent(String.valueOf(System.currentTimeMillis()),
                                                new com.vpedak.testsrecorder.core.events.View(parentId),
                                                new ScrollToPositionAction(i));
                            }
                        } catch (NoClassDefFoundError e) {
                            // RecyclerView is not used in this project, ignore this
                        }
                        return new ResolveSubjectResult(new ParentView(parentId, i), scrollToEvent);
                    }
                }
            }
        }

        return null;
    }

    // See issue #18
    private Subject checkIdDuplication(String viewId, View view) {
        if (view.getParent() != null && view.getParent() instanceof View &&
                view.getParent().getParent() != null && view.getParent().getParent() instanceof ViewGroup) {
            ViewGroup grandParentView = (ViewGroup) view.getParent().getParent();

            if (isIdDuplicated(grandParentView, view.getId(), new IntegerHolder())) {
                String parentId = resolveId(((View) view.getParent()).getId());
                if (parentId != null) {
                    return new ParentIdView(parentId, viewId);
                } else if (isInsideViewPager(view)) {
                    return new DisplayedView(viewId);
                }
            }
        }

        return new com.vpedak.testsrecorder.core.events.View(viewId);
    }

    private boolean isIdDuplicated(ViewGroup view, int viewId, IntegerHolder num) {
        for (int i = 0; i < view.getChildCount(); i++) {
            View child = view.getChildAt(i);

            if (child instanceof ViewGroup) {
                boolean tst = isIdDuplicated((ViewGroup) child, viewId, num);
                if (tst) {
                    return true;
                }
            } else {
                if (child.getId() == viewId) {
                    num.value++;
                    if (num.value > 1) {
                        return true;
                    }
                }
            }
        }

        if (view.getId() == viewId) {
            num.value++;
        }

        return num.value > 1;
    }

    private boolean isInsideViewPager(View view) {
        try {
            ViewParent parent = view.getParent();
            while (parent != null) {
                if (parent instanceof ViewPager) {
                    return true;
                }
                parent = parent.getParent();
            }
        } catch (NoClassDefFoundError e) {
            // ViewPager is not used in this project, ignore this
        }
        return false;
    }


    @Nullable
    public String resolveId(int id) {
        if (id < 0) {
            return null;
        }

        try {
            String viewId = activity.getResources().getResourceEntryName(id);
            String pkg = activity.getResources().getResourcePackageName(id);
            String type = activity.getResources().getResourceTypeName(id);
            if (pkg.equals("android")) {
                viewId = "android.R." + type + "." + viewId;
            } else {
                viewId = "R." + type + "." + viewId;
            }
            return viewId;
        } catch (Resources.NotFoundException e) {
            return String.valueOf(id);
        }
    }

    public String getWidgetName(View view) {
        return view.getClass().getSimpleName();
    }

    static String wmFieldName;
    static Class wmClass;

    static {
        try {
            if (Build.VERSION.SDK_INT >= 17) {
                wmFieldName = "sDefaultWindowManager";
                wmClass = Class.forName("android.view.WindowManagerGlobal");
            } else if (Build.VERSION.SDK_INT >= 13) {
                wmFieldName = "sWindowManager";
                wmClass = Class.forName("android.view.WindowManagerImpl");
            } else {
                wmFieldName = "mWindowManager";
                wmClass = Class.forName("android.view.WindowManagerImpl");
            }
        } catch (ClassNotFoundException localClassNotFoundException) {
            Log.e(ANDRIOD_TEST_RECORDER, "Couldn't find android.view.WindowManagerImpl - fatal!", localClassNotFoundException);
        } catch (SecurityException localSecurityException) {
            Log.e(ANDRIOD_TEST_RECORDER, "Couldn't get android.view.WindowManagerImpl", localSecurityException);
        }
    }

    private static View[] getWMViews() {
        try {
            Field localField = wmClass.getDeclaredField("mViews");
            Object localObject = wmClass.getDeclaredField(wmFieldName);
            localField.setAccessible(true);
            ((Field) localObject).setAccessible(true);
            localObject = ((Field) localObject).get(null);
            if (Build.VERSION.SDK_INT < 19) {
                return (View[]) localField.get(localObject);
            }
            return (View[]) ((ArrayList) localField.get(localObject)).toArray(new View[0]);
        } catch (SecurityException localSecurityException) {
            Log.e(ANDRIOD_TEST_RECORDER, "Couldn't get decor views!", localSecurityException);
        } catch (NoSuchFieldException localNoSuchFieldException) {
            Log.e(ANDRIOD_TEST_RECORDER, "Couldn't get decor views!", localNoSuchFieldException);
        } catch (IllegalArgumentException localIllegalArgumentException) {
            Log.e(ANDRIOD_TEST_RECORDER, "Couldn't get decor views!", localIllegalArgumentException);
        } catch (IllegalAccessException localIllegalAccessException) {
            Log.e(ANDRIOD_TEST_RECORDER, "Couldn't get decor views!", localIllegalAccessException);
        }

        return null;
    }

    public static class ResolveSubjectResult {
        private Subject subject;
        private RecordingEvent scrollToEvent;

        public ResolveSubjectResult(Subject subject) {
            this.subject = subject;
        }

        public ResolveSubjectResult(Subject subject, RecordingEvent scrollToEvent) {
            this.subject = subject;
            this.scrollToEvent = scrollToEvent;
        }

        public Subject getSubject() {
            return subject;
        }

        public RecordingEvent getScrollToEvent() {
            return scrollToEvent;
        }
    }

    public static class IntegerHolder {
        public int value = 0;
    }
}