package com.todo.client;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.event.shared.EventBus;
import com.google.gwt.json.client.JSONArray;
import com.google.gwt.json.client.JSONBoolean;
import com.google.gwt.json.client.JSONObject;
import com.google.gwt.json.client.JSONParser;
import com.google.gwt.json.client.JSONString;
import com.google.gwt.storage.client.Storage;
import com.google.gwt.user.client.History;
import com.google.gwt.view.client.AbstractDataProvider;
import com.google.gwt.view.client.ListDataProvider;
import com.todo.client.ToDoItem;
import com.todo.client.ToDoRouting;
import com.todo.client.events.ToDoEvent;
import com.todo.client.events.ToDoRemovedEvent;
import com.todo.client.events.ToDoUpdatedEvent;

/**
 * The presenter for the ToDo application. This class is responsible for the lifecycle of the
 * {@link ToDoItem} instances.
 *
 * @author ceberhardt
 * @author dprotti
 *
 */
public class ToDoPresenter {

	private static final String STORAGE_KEY = "todo-gwt";

	/**
	 * The interface that a view for this presenter must implement.
	 */
	public interface View {

		/**
		 * Gets the text that the user has input for the creation of new tasks.
		 */
		String getTaskText();

		/**
		 * Clears the user input field where new tasks are added.
		 */
		void clearTaskText();

		/**
		 * Sets the current task statistics.
		 */
		void setTaskStatistics(int totalTasks, int completedTasks);

		/**
		 * Sets the data provider that acts as a source of {@link ToDoItem} instances.
		 */
		void setDataProvider(AbstractDataProvider<ToDoItem> data);

		/**
		 * Adds the handler to the events raised by the view.
		 */
		void addhandler(ViewEventHandler handler);

		/**
		 * Informs the view of the current routing state.
		 */
		void setRouting(ToDoRouting routing);
	}

	/**
	 * The interface that handles interactions from the view.
	 */
	public interface ViewEventHandler {
		/**
		 * Invoked when a user adds a new task.
		 */
		void addTask();

		/**
		 * Invoked when a user wishes to clear completed tasks.
		 */
		void clearCompletedTasks();

		/**
		 * Sets the completed state of all tasks to the given state.
		 */
		void markAllCompleted(boolean completed);
	}

	/**
	 * Handler for view events, defers to private presenter methods.
	 */
	private final ViewEventHandler viewHandler = new ViewEventHandler() {
		@Override
		public void addTask() {
			ToDoPresenter.this.addTask();
		}

		@Override
		public void clearCompletedTasks() {
			ToDoPresenter.this.clearCompletedTasks();
		}

		@Override
		public void markAllCompleted(boolean completed) {
			ToDoPresenter.this.markAllCompleted(completed);
		}
	};

	private final List<ToDoItem> todos = new ArrayList<ToDoItem>();

	private final ListDataProvider<ToDoItem> filteredTodos = new ListDataProvider<ToDoItem>();

	private final View view;

	private ToDoRouting routing = ToDoRouting.ALL;

	private EventBus eventBus;

	public ToDoPresenter(View view) {
		this.view = view;

		loadState();

		String initialToken = History.getToken();
		routing = parseRoutingToken(initialToken);

		view.addhandler(viewHandler);
		view.setDataProvider(filteredTodos);
		view.setRouting(routing);

		updateTaskStatistics();
		setupHistoryHandler();
		eventBus = ToDoEvent.getGlobalEventBus();
		// listen to edits on individual items
		eventBus.addHandler(ToDoUpdatedEvent.TYPE, new ToDoUpdatedEvent.Handler() {

			@Override
			public void onEvent(ToDoUpdatedEvent event) {
				itemStateChanged(event.getToDo());
			}

		});
		// listen to removals
		eventBus.addHandler(ToDoRemovedEvent.TYPE, new ToDoRemovedEvent.Handler() {

			@Override
			public void onEvent(ToDoRemovedEvent event) {
				deleteTask(event.getToDo());
			}

		});
	}

	/**
	 * Set up the history changed handler, which provides routing.
	 */
	private void setupHistoryHandler() {
		History.addValueChangeHandler(new ValueChangeHandler<String>() {
			public void onValueChange(ValueChangeEvent<String> event) {
				String historyToken = event.getValue();
				routing = parseRoutingToken(historyToken);
				view.setRouting(routing);
				updateFilteredList();
			}
		});
	}

	/**
	 * Converts the string routing token into the equivalent enum value.
	 */
	private ToDoRouting parseRoutingToken(String token ) {
		if (token.equals("/active")) {
			return ToDoRouting.ACTIVE;
		} else if (token.equals("/completed")) {
			return ToDoRouting.COMPLETED;
		} else {
			return ToDoRouting.ALL;
		}
	}

	/**
	 * Updates the filtered list, which is rendered in the UI.
	 */
	private void updateFilteredList() {
		filteredTodos.getList().clear();
		for (ToDoItem task : todos) {
			if (routing.getMatcher().matches(task)) {
				filteredTodos.getList().add(task);
			}
		}
	}

	/**
	 * Computes the tasks statistics and updates the view.
	 */
	private void updateTaskStatistics() {
		int totalTasks = todos.size();

		int completeTask = 0;
		for (ToDoItem task : todos) {
			if (task.isCompleted()) {
				completeTask++;
			}
		}

		view.setTaskStatistics(totalTasks, completeTask);
	}

	/**
	 * Deletes the given task and updates statistics.
	 */
	protected void deleteTask(ToDoItem toDoItem) {
		todos.remove(toDoItem);
		taskStateChanged();
	}

	/**
	 * Invoked by a task when its state changes so that we can update the view statistics and persist.
	 */
	protected void itemStateChanged(ToDoItem toDoItem) {

		toDoItem.setTitle(toDoItem.getTitle().trim());

		if (toDoItem.getTitle().isEmpty()) {
			todos.remove(toDoItem);
		}

		taskStateChanged();
	}

	/**
	 * When the task state has changed, this method will update the UI and persist.
	 */
	private void taskStateChanged() {
		updateFilteredList();
		updateTaskStatistics();
		saveState();
	}

	/**
	 * Sets the completed state of all tasks.
	 */
	private void markAllCompleted(boolean completed) {

		for (ToDoItem task : todos) {
			task.setCompleted(completed);
		}

		taskStateChanged();
	}

	/**
	 * Adds a new task based on the user input field.
	 */
	private void addTask() {
		String taskTitle = view.getTaskText().trim();

		// if white-space only, do not add a todo
		if (taskTitle.equals(""))
			return;

		ToDoItem toDoItem = new ToDoItem(taskTitle);
		view.clearTaskText();
		todos.add(toDoItem);

		taskStateChanged();
	}

	/**
	 * Clears completed tasks and updates the view.
	 */
	private void clearCompletedTasks() {
		Iterator<ToDoItem> iterator = todos.iterator();
		while (iterator.hasNext()) {
			ToDoItem item = iterator.next();
			if (item.isCompleted()) {
				iterator.remove();
			}
		}

		taskStateChanged();
	}

	/**
	 * Saves the current to-do items to local storage.
	 */
	private void saveState() {
		Storage storage = Storage.getLocalStorageIfSupported();
		if (storage != null) {

			// JSON encode the items
			JSONArray todoItems = new JSONArray();
			for (int i = 0; i < todos.size(); i++) {
				ToDoItem toDoItem = todos.get(i);
				JSONObject jsonObject = new JSONObject();
				jsonObject.put("task", new JSONString(toDoItem.getTitle()));
				jsonObject.put("complete", JSONBoolean.getInstance(toDoItem.isCompleted()));
				todoItems.set(i, jsonObject);
			}

			// save to local storage
			storage.setItem(STORAGE_KEY, todoItems.toString());
		}
	}

	private void loadState() {
		Storage storage = Storage.getLocalStorageIfSupported();
		if (storage != null) {
			try {
				// get state
				String state = storage.getItem(STORAGE_KEY);

				// parse the JSON array
				JSONArray todoItems = JSONParser.parseStrict(state).isArray();
				for (int i = 0; i < todoItems.size(); i++) {
					// extract the to-do item values
					JSONObject jsonObject = todoItems.get(i).isObject();
					String task = jsonObject.get("task").isString().stringValue();
					boolean completed = jsonObject.get("complete").isBoolean().booleanValue();
					// add a new item to our list
					todos.add(new ToDoItem(task, completed));
				}
			} catch (Exception e) {

			}
		}

		updateFilteredList();
	}

}
