/**
 * Copyright (c) 2013, Redsolution LTD. All rights reserved.
 * 
 * This file is part of Xabber project; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License, Version 3.
 * 
 * Xabber 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 GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License,
 * along with this program. If not, see http://www.gnu.org/licenses/.
 */
package com.xabber.android.ui.widget;

import android.content.Context;
import android.database.DataSetObserver;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.widget.Adapter;
import android.widget.ListView;
import android.widget.Scroller;

import com.xabber.android.data.LogManager;
import com.xabber.android.ui.adapter.SaveStateAdapter;
import com.xabber.androiddev.R;

/**
 * Widget to switch between chat pages.
 * 
 * Warning: This class is to be replaced.
 * 
 * @author alexander.ivanov
 * 
 */
public class PageSwitcher extends ViewGroup {

	public static final boolean LOG = false;

	/**
	 * Distance a touch can wander before we think the user is scrolling.
	 */
	private final int touchSlop;

	private final DataSetObserver dataSetObserver = new DataSetObserver() {
		@Override
		public void onChanged() {
			dataChanged = true;
			requestLayout();
		}

		@Override
		public void onInvalidated() {
			dataChanged = true;
			requestLayout();
		}
	};

	private boolean dataChanged;

	private SaveStateAdapter adapter;

	private int widthMeasureSpec;

	private int heightMeasureSpec;

	/**
	 * User is currently dragging this view.
	 */
	private boolean isBeingDragged;

	/**
	 * Drag was interrupted.
	 */
	private boolean dragWasCanceled;

	/**
	 * Position of down touch.
	 */
	private float touchX;

	/**
	 * Initial scroll position.
	 */
	private int initialScrollX;

	/**
	 * {@link Scroller#isFinished()} is <code>false</code> when being flung.
	 */
	private final Scroller scroller;

	/**
	 * The listener that receives notifications when an item is selected,
	 * unselected or removed.
	 */
	private OnSelectListener onSelectListener;

	/**
	 * Selected position in adapter. Can be incorrect while {@link #dataChanged}
	 * is <code>true</code> .
	 */
	private int selectedPosition;

	private View selectedView;

	/**
	 * Previous selected object (before data changes).
	 */
	private Object previousSelectedObject;

	/**
	 * Visible but not selected position.
	 */
	private int visiblePosition;

	/**
	 * Visible but not selected view.
	 */
	private View visibleView;

	/**
	 * Previous visible object (before data changes).
	 */
	private Object previousVisibleObject;

	public PageSwitcher(Context context, AttributeSet attrs) {
		super(context, attrs);
		touchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
		dataChanged = true;
		adapter = null;
		widthMeasureSpec = 0;
		heightMeasureSpec = 0;
		isBeingDragged = false;
		dragWasCanceled = false;
		touchX = 0;
		initialScrollX = 0;
		scroller = new Scroller(getContext());
		onSelectListener = null;

		selectedPosition = 0;
		selectedView = null;
		previousSelectedObject = null;
		visiblePosition = 0;
		visibleView = null;
		previousVisibleObject = null;
	}

	/**
	 * Sets adapter. Request update and set initial selected position.
	 * 
	 * @param adapter
	 */
	public void setAdapter(SaveStateAdapter adapter) {
		if (adapter == null)
			throw new IllegalStateException();
		if (this.adapter != null)
			this.adapter.unregisterDataSetObserver(dataSetObserver);
		this.adapter = adapter;
		this.adapter.registerDataSetObserver(dataSetObserver);
		// dataChanged will be set in setSelection().
		setSelection(0);
	}

	/**
	 * @return Assigned adapter.
	 */
	public Adapter getAdapter() {
		return adapter;
	}

	/**
	 * Register a callback to be invoked when an item in this View has been
	 * selected, unselected or removed.
	 * 
	 * @param listener
	 *            The callback that will run
	 */
	public void setOnSelectListener(OnSelectListener listener) {
		onSelectListener = listener;
	}

	public OnSelectListener getOnSelectListener() {
		return onSelectListener;
	}

	/**
	 * @return Selected position in adapter.
	 */
	public int getSelectedItemPosition() {
		if (adapter == null)
			throw new IllegalStateException();
		return selectedPosition;
	}

	/**
	 * @return Selected item in adapter or <code>null</code> if there is no
	 *         elements.
	 */
	public Object getSelectedItem() {
		if (adapter == null)
			throw new IllegalStateException();
		if (selectedPosition < 0 || selectedPosition >= adapter.getCount())
			return null;
		else
			return adapter.getItem(selectedPosition);
	}

	/**
	 * @return Visible item from adapter or <code>null</code> if there is no
	 *         visible elements (selected element still can exists).
	 */
	public Object getVisibleItem() {
		if (adapter == null)
			throw new IllegalStateException();
		if (visiblePosition < 0 || visiblePosition >= adapter.getCount())
			return null;
		else
			return adapter.getItem(visiblePosition);
	}

	/**
	 * @return Selected view.
	 */
	public View getSelectedView() {
		if (adapter == null)
			throw new IllegalStateException();
		return selectedView;
	}

	/**
	 * @return Visible view.
	 */
	public View getVisibleView() {
		if (adapter == null)
			throw new IllegalStateException();
		return visibleView;
	}

	/**
	 * @return number of items or zero if adapter is <code>null</code> .
	 */
	private int getCount() {
		return adapter == null ? 0 : adapter.getCount();
	}

	/**
	 * Returns correct position.
	 * 
	 * @param position
	 * @return value between <code>0</code> and <code>count - 1</code>.
	 */
	private int correntPosition(int position) {
		final int count = getCount();
		if (position >= count)
			return 0;
		if (position < 0)
			return count - 1;
		return position;
	}

	/**
	 * Gets view.
	 * 
	 * @param position
	 *            in adapter.
	 * @param x
	 *            position in layout.
	 * @param convertView
	 *            previous view.
	 * @param update
	 *            whether we need force update underlying view.
	 * @param layout
	 *            whether we need to update layout (remeasure).
	 * @return
	 */
	private View getView(int position, int x, View convertView, boolean update,
			boolean layout) {
		final View view;
		if (convertView == null) {
			if (LOG)
				LogManager.i(this, "new view");
			view = adapter.getView(position, null, this);
		} else if (update) {
			if (LOG)
				LogManager.i(this, "update view");
			view = adapter.getView(position, convertView, this);
		} else {
			view = convertView;
		}
		if (view != convertView) {
			if (LOG)
				LogManager.i(this, "init view");
			LayoutParams layoutParams = view.getLayoutParams();
			if (layoutParams == null)
				layoutParams = new LayoutParams(LayoutParams.FILL_PARENT,
						LayoutParams.FILL_PARENT);
			addViewInLayout(view, 0, layoutParams, true);
		}
		if (update || layout || view.getLeft() != x) {
			if (LOG)
				LogManager.i(this, "layout view");
			// We must measure ListView after update to show items.
			measureChild(view, widthMeasureSpec, heightMeasureSpec);
			view.layout(x, 0, x + view.getMeasuredWidth(),
					view.getMeasuredHeight());
		}
		return view;
	}

	/**
	 * Updates scrolling, creates views if necessary .
	 * 
	 * @param layout
	 *            whether we need to update layout (remeasure).
	 */
	private void update(boolean layout) {
		// Process data change.
		final int count = getCount();
		if (dataChanged) {
			if (previousSelectedObject != null)
				for (int position = 0; position < count; position++)
					if (adapter.getItem(position)
							.equals(previousSelectedObject)) {
						selectedPosition = position;
						if (LOG)
							LogManager.i(this, "Found selected position: "
									+ selectedPosition);
						break;
					}
			selectedPosition = correntPosition(selectedPosition);
		}

		// Process scrolling.
		final int width = getWidth();
		int scrollX = getScrollX();
		if (width != 0) {
			while (scrollX >= width) {
				scrollX -= width;
				initialScrollX -= width;
				selectedPosition = correntPosition(selectedPosition + 1);
				if (LOG)
					LogManager.i(this, "scrollX >= width: " + selectedPosition);
			}
			while (scrollX <= -width) {
				scrollX += width;
				initialScrollX += width;
				selectedPosition = correntPosition(selectedPosition - 1);
				if (LOG)
					LogManager
							.i(this, "scrollX <= -width: " + selectedPosition);
			}
		}

		// Process low count.
		if (count < 2) {
			if (LOG)
				LogManager.i(this, "count < 2");
			dragWasCanceled = true;
			isBeingDragged = false;
			if (!scroller.isFinished())
				scroller.abortAnimation();
			if (scrollX != 0)
				scrollX = 0;
		}

		// Store focus.
		final View focus;
		if (selectedView != null)
			focus = selectedView.findFocus();
		else
			focus = null;

		// Process selected view.
		if (count == 0) {
			if (LOG)
				LogManager.i(this, "count == 0");
			selectedPosition = -1;
			if (selectedView != null) {
				if (onSelectListener != null)
					onSelectListener.onUnselect();
				adapter.saveState(selectedView);
				removeViewInLayout(selectedView);
				selectedView = null;
				// We must invalidate to update view.
				invalidate();
			}
		} else {
			if (LOG)
				LogManager.i(this, "count > 0");

			// Exchange visible and selected views and previous objects.
			final Object selectedObject = adapter.getItem(selectedPosition);
			final boolean exchange = previousSelectedObject != null
					&& previousVisibleObject != null
					&& !previousSelectedObject.equals(selectedObject)
					&& previousVisibleObject.equals(selectedObject);
			if (exchange) {
				Object tempObject = previousSelectedObject;
				previousSelectedObject = previousVisibleObject;
				previousVisibleObject = tempObject;
				View view = selectedView;
				selectedView = visibleView;
				visibleView = view;
			}

			// Update view.
			final boolean update = dataChanged
					|| previousSelectedObject == null
					|| !previousSelectedObject.equals(selectedObject);
			selectedView = getView(selectedPosition, 0, selectedView, update,
					layout);
			previousSelectedObject = selectedObject;
			if (update || exchange)
				if (onSelectListener != null)
					onSelectListener.onSelect();

			// Enable focusable.
			if (selectedView instanceof ViewGroup)
				((ViewGroup) selectedView)
						.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
			else
				selectedView.setFocusable(true);
		}

		// Process visible (not selected) view.
		if (count < 2) {
			if (LOG)
				LogManager.i(this, "count < 2 || scrollX == 0");
			visiblePosition = -1;
			if (visibleView != null) {
				adapter.saveState(visibleView);
				removeViewInLayout(visibleView);
				visibleView = null;
			}
		} else {
			// Calculate position.
			final int visibleX;
			if (scrollX > 0) {
				if (LOG)
					LogManager.i(this, "scrollX > 0");
				visiblePosition = correntPosition(selectedPosition + 1);
				visibleX = width;
			} else {
				if (LOG)
					LogManager.i(this, "scrollX < 0");
				visiblePosition = correntPosition(selectedPosition - 1);
				visibleX = -width;
			}

			// Update view.
			final Object visibleObject = adapter.getItem(visiblePosition);
			final boolean update = dataChanged || previousVisibleObject == null
					|| !previousVisibleObject.equals(visibleObject);
			visibleView = getView(visiblePosition, visibleX, visibleView,
					update, layout);
			previousVisibleObject = visibleObject;

			// Disable focusable.
			if (visibleView instanceof ViewGroup)
				((ViewGroup) visibleView)
						.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
			else
				visibleView.setFocusable(false);
		}

		// Restore focus by ID.
		if (selectedView != null) {
			View target;
			if (focus == null || focus.getId() == View.NO_ID)
				target = null;
			else
				target = selectedView.findViewById(focus.getId());
			if (target == null)
				target = selectedView.findViewById(R.id.chat_input);
			target.requestFocus();
		}

		if (scrollX == 0) {
			if (LOG)
				LogManager.i(this, "Scroll X == 0");
			if (visibleView != null)
				adapter.hidePages(visibleView);
			if (selectedView != null)
				adapter.hidePages(selectedView);
		} else {
			if (LOG)
				LogManager.i(this, "Scroll X != 0");
			if (visibleView != null)
				adapter.showPages(visibleView);
			if (selectedView != null)
				adapter.showPages(selectedView);
		}

		super.scrollTo(scrollX, 0);

		dataChanged = false;
	}

	/**
	 * Save state of views. Must be called on activity pause.
	 */
	public void saveState() {
		if (visibleView != null)
			adapter.saveState(visibleView);
		if (selectedView != null)
			adapter.saveState(selectedView);
	}

	/**
	 * Selects an item. Immediately scroll to this position.
	 * 
	 * @param position
	 */
	public void setSelection(int position) {
		if (adapter == null)
			throw new IllegalStateException();
		dataChanged = true;
		isBeingDragged = false;
		dragWasCanceled = true;
		if (!scroller.isFinished())
			scroller.abortAnimation();
		if (getScrollX() != 0 || getScrollY() != 0)
			super.scrollTo(0, 0);
		previousSelectedObject = null;
		selectedPosition = position;
		if (LOG)
			LogManager.i(this, "setSelection: " + selectedPosition);
		update(false);
		if (selectedView == null)
			return;
		ListView listView = (ListView) selectedView
				.findViewById(android.R.id.list);
		listView.setAdapter(listView.getAdapter());
	}

	/**
	 * Stop any movements.
	 */
	public void stopMovement() {
		isBeingDragged = false;
		dragWasCanceled = true;
		if (!scroller.isFinished())
			scroller.abortAnimation();
		if (getScrollX() != 0 || getScrollY() != 0)
			super.scrollTo(0, 0);
		update(false);
	}

	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		super.onMeasure(widthMeasureSpec, heightMeasureSpec);
		this.widthMeasureSpec = widthMeasureSpec;
		this.heightMeasureSpec = heightMeasureSpec;
	}

	@Override
	protected void onLayout(boolean changed, int left, int top, int right,
			int bottom) {
		if (LOG)
			LogManager.i(this, "onLayout");
		update(true);
	}

	@Override
	public boolean onInterceptTouchEvent(MotionEvent event) {
		if (getCount() <= 1) {
			isBeingDragged = false;
			dragWasCanceled = true;
			return false;
		}

		final int action = event.getAction();
		if (action == MotionEvent.ACTION_DOWN)
			dragWasCanceled = false;
		if (dragWasCanceled)
			return false;

		final float x = event.getX();
		switch (action) {
		case MotionEvent.ACTION_DOWN:
			touchX = x;
			initialScrollX = getScrollX();
			if (!scroller.isFinished()) {
				scroller.abortAnimation();
				isBeingDragged = true;
			} else {
				isBeingDragged = false;
			}
			break;
		case MotionEvent.ACTION_MOVE:
			if (Math.abs(x - touchX) > touchSlop)
				isBeingDragged = true;
			// requestDisallowInterceptTouchEvent(true);
			break;
		case MotionEvent.ACTION_CANCEL:
		case MotionEvent.ACTION_UP:
			break;
		}
		return isBeingDragged;
	}

	@Override
	public boolean onTouchEvent(MotionEvent event) {
		if (getCount() <= 1)
			dragWasCanceled = true;
		if (dragWasCanceled)
			return false;

		final int action = event.getAction();
		final float x = event.getX();
		switch (action) {
		case MotionEvent.ACTION_DOWN:
			break;
		case MotionEvent.ACTION_MOVE:
			if (Math.abs(x - touchX) > touchSlop)
				isBeingDragged = true;
			if (isBeingDragged) {
				if (LOG)
					LogManager.i(this, "onTouchEvent - MOVE");
				scrollTo((int) (touchX - x) + initialScrollX, 0);
			}
			break;
		case MotionEvent.ACTION_CANCEL:
		case MotionEvent.ACTION_UP:
			if (isBeingDragged) {
				final int scroll = getScrollX();
				final int width = getWidth();
				final int center = width / 2;
				final int target;
				if (scroll > center) {
					target = width;
				} else if (scroll < -center) {
					target = -width;
				} else {
					target = 0;
				}
				scroller.startScroll(scroll, 0, target - scroll, 0);
				invalidate();
			}
			isBeingDragged = false;
			break;
		}
		return true;
	}

	@Override
	public void computeScroll() {
		if (scroller.computeScrollOffset()) {
			if (scroller.getCurrX() == scroller.getFinalX()
					&& scroller.getCurrY() == scroller.getFinalY())
				scroller.abortAnimation();
			if (LOG)
				LogManager.i(this, "computeScroll");
			scrollTo(scroller.getCurrX(), scroller.getCurrY());
			postInvalidate();
		}
	}

	@Override
	public void scrollTo(int x, int y) {
		if (LOG)
			LogManager.i(this, "scrollTo: " + x + "," + y);
		super.scrollTo(x, y);
		update(false);
	}

	public interface OnSelectListener {

		/**
		 * Callback method to be invoked when an item has been selected.
		 */
		void onSelect();

		/**
		 * Callback method to be invoked when an item has been unselected.
		 */
		void onUnselect();

	}
}
