/* Copyright 2009 Palm, Inc.  All rights reserved. */

var TasksAssistant = Class.create({
	
    initialize: function()
	{
        Mojo.Log.info('TasksAssistant::initialize %s', curTicks());
    },
    
    setup: function()
	{
        Mojo.Log.info('TasksAssistant::setup %j %s', State.taskList, curTicks());
		
		// Handlers
		this.handleTap = this.handleTap.bindAsEventListener(this);
		this.handleTapCapture = this.handleTapCapture.bindAsEventListener(this);
		this.handleHoldCapture = this.handleHoldCapture.bindAsEventListener(this);
		this.handleDelete = this.handleDelete.bindAsEventListener(this);
		this.handleReorder = this.handleReorder.bindAsEventListener(this);
		this.onTaskChanged = this.onTaskChanged.bindAsEventListener(this);
		this.handleFilter = this.handleFilter.bindAsEventListener(this);
		this.taskListNameChanged = this.taskListNameChanged.bindAsEventListener(this);
		this.showOrderChanged = this.showOrderChanged.bindAsEventListener(this);
		this.onKey = this.onKey.bindAsEventListener(this);
		this.onTaskBlur = this.onTaskBlur.bindAsEventListener(this);
		this.onTaskListName = this.onTaskListName.bindAsEventListener(this);
		this.onTaskListEnterKey = this.onTaskListEnterKey.bindAsEventListener(this);
		this._onSceneDeactivate = this._onSceneDeactivate.bindAsEventListener(this);
		this._stopScrolling = this._stopScrolling.bindAsEventListener(this);
        
        // Whole scene
        this.sceneElement = this.controller.get('tasks-list');
		                		
        // Task list
        var taskList = State.taskList;
		this.taskList = taskList;
		this.modifiedTaskList = 
		{ 
			name: taskList.name, 
			show: taskList.show ? taskList.show : "all", 
			order: taskList.order ? taskList.order : "position",
			_disabled: (TaskList.isDefault(taskList) || TaskList.isAllTasks(taskList)) 
		};

        // Add the EAS icon if necessary
        if (TaskList.isEAS(taskList)) 
		{
            this.controller.get('taskListEasIcon').show();
        }
        
        this.controller.setupWidget('taskListName',
		{
            hintText: $L('List name'),
            textFieldMode: 'sentence-case',
            requiresEnterKey: true,
			changeOnKeyPress: true,
			modelProperty: 'name',
			disabledProperty: '_disabled',
			focus: TaskList.isNew(taskList),
			focusMode: Mojo.Widget.focusAppendMode,
        }, this.modifiedTaskList);
        this.taskListNameElement = this.controller.get('taskListName');
        this.taskListNameElement.observe(Mojo.Event.propertyChanged, this.taskListNameChanged);
        this.taskListNameElement.observe('keyup', this.onTaskListEnterKey);
		if (!this.modifiedTaskList._disabled) 
		{
			this.taskListNameElement.observe(Mojo.Event.tap, this.onTaskListName);
		}     

        this.listAttr =
		{
            itemTemplate: Mojo.View.loadTemplate('tasks/task-entry'),
			dividerTemplate: Mojo.View.loadTemplate('tasks/tasks-divider'),
            swipeToDelete: true,
            focusMode: Mojo.Widget.focusInsertMode,
            reorderable: true,
			uniquenessProperty: 'id',
            formatters: {
                dummy: this.formatItem.bind(this)
            },
			fixedHeightItems: true,
            itemsCallback: this.itemsCallback.bind(this),
			dividerFunction: this._itemsDividers.bind(this),
			lookahead: 50,
			renderLimit: 200,
			scrollThreshold: 52 * 200 / 4,
        };
		this.controller.setupWidget('taskList', this.listAttr, {});

		this.controller.setupWidget('tasks_scroller');
        
        this.listElement = this.controller.get('taskList');
        this.listElement.observe(Mojo.Event.listTap, this.handleTap);
		this.listElement.addEventListener(Mojo.Event.tap, this.handleTapCapture, true);
		this.listElement.addEventListener(Mojo.Event.hold, this.handleHoldCapture, true);
        this.listElement.observe(Mojo.Event.listDelete, this.handleDelete);
        this.listElement.observe(Mojo.Event.listReorder, this.handleReorder);
        this.listElement.observe(Mojo.Event.propertyChange, this.onTaskChanged);
		this.listElement.observe('keyup', this.onKey);
		this.sceneElement.observe('DOMFocusOut', this.onTaskBlur);
		
        this.controller.setupWidget('completed', { modelProperty: 'completed' });

        // Filter
		this._filterState = { disabled: true };
        this.controller.setupWidget('filterField', {}, this._filterState);
        this.filterFieldElement = this.controller.get('filterField');
        this.filterFieldElement.observe(Mojo.Event.filter, this.handleFilter);
		
        // Subject
        this.controller.setupWidget('subject',
		{
            hintText: $L('Task name...'),
            modelProperty: 'titleFormatted',
            textFieldMode: 'sentence-case',
            requiresEnterKey: true,
			changeOnKeyPress: true,
			focusMode: Mojo.Widget.focusAppendMode,
        });
		
		// Set up a command menu
        this.controller.setupWidget(Mojo.Menu.commandMenu, {menuClass: "no-fade"}, 
		{
            items: 
			[
				{
					icon: 'new-task-button',
	                command: 'add'
	            },
			]
        });
       
		// App Menu
		var self = this;
		this._appmenu = new Weave.Utilities.AppMenu(this).addEdit()
			.addItem($L('Set due date for all...'), this.handleSetDueDateCmd.bind(this))
			.addSubMenu($L('Mark All'))
				.addItem($L('Mark all completed'), this.handleMarkCompletedCmd.bind(this))
				.addItem($L('Mark all incomplete'), this.handleMarkIncompleteCmd.bind(this))
			.popSubMenu()
			.addItem(this.modifiedTaskList.show == 'all' ? $L('Hide Completed') : $L('Show Completed'), this.handleHideShowCompletedCmd.bind(this))
			.addItem($L('Delete completed tasks'), this.handlePurgeCompletedCmd.bind(this))
			.addItem($L('Delete list'), this.handleDeleteListCmd.bind(this), function(event)
			{
				// Disable Delete list command for special lists
				return !(TaskList.isSpecial(self.taskList) || TaskList.isAllTasks(self.taskList));
			})
			.addHelp();
        
        /*
         * Ordering
         */
        this.controller.setupWidget('showFilter', 
		{
			modelProperty: "order",
			choices: [
			{
				label: $L('My Order'),
				value: 'position'
			}, 
			{
				label: $L('Due Date'),
				value: 'duedate'
			}, 
			{
				label: $L('Priority'),
				value: 'priority'
			}]
		}, this.modifiedTaskList);
        this.controller.listen('showFilter', Mojo.Event.propertyChange, this.showOrderChanged);
		
		// Stop scrolling whenever we type
		this.controller.sceneElement.addEventListener('keydown', this._stopScrolling, true);
		
		// Initialize state machine
		if (TaskList.isNew(taskList))
		{
			// For new lists we start in rename state (since the first thing we do is give it a name)
			this.state = this.states.rename;
			// We make a new list as untouched until someone does something to it.  If it remains untouched we'll never save it
			this._untouched = true;
		}
		else if (State.task)
		{
			this.state = this.states.push;
			this.setCurrentTask(State.task);
		}
		else
		{
			this.state = this.states.idle;
			this._enableFilter(true);
		}
		
		// Watch for external list changes
		var self = this;
		taskList.addExternalUpdateListener(this, function(list)
		{
			Mojo.Log.info("Sync: %s", self.state._name);
			Mojo.Log.info("%d %d %d", self._ignoresync, (new Date()).getTime(), self._ignoresync - (new Date()).getTime());
			if (self._ignoresync && self._ignoresync > (new Date()).getTime()) 
			{
				self._ignoresync = null;
			}
			else 
			{
				self._ignoresync = null;
				self.state.sync.call(self);
			}
		});
    },
	
	cleanup: function(params)
	{
	    Mojo.Log.info('TasksAssistant::cleanup');
		
		this._onSceneDeactivate();
		
		// Cleanup models (if we didnt delete it already)
		if (this.task) 
		{
			this.task.removeUpdateListener(this);
		}
		this.taskList.removeExternalUpdateListener(this);
	},

	activate: function(arg)
	{
	    Mojo.Log.info('TasksAssistant::activate');
		
		// Watch for when we go into card view
		this.controller.document.addEventListener(Mojo.Event.deactivate, this._onSceneDeactivate);
		
		// Blur outself so filtering will work immediately
		if (this.state != this.states.rename) 
		{
			this._blurCurrent();
		}
		
		this.state.activate.call(this);
	},
	
	deactivate: function()
	{
		this.controller.document.removeEventListener(Mojo.Event.deactivate, this._onSceneDeactivate);
	},
	
	/**************************************************************/
	/* State machine */
	
	state: null,
	
	states: 
	{		
		idle: 
		{
			_name: 'idle',
			
			reset: function()
			{
			},
			
			add: function()
			{
				this.state = this.states.adding;
				this.addItem(null);
			},
			
			edit: function()
			{
			},
			
			back: function()
			{
				// Pop the scene.
				this.state = this.states.pop;
				//this._enableFilter(false);
				this._popScene();
			},
			
			enter: function()
			{
			},
			
			blur: function()
			{
			},
			
			select: function(node, item)
			{
				Mojo.assert(this.task == null, 'Attempting to assign to this.task which isnt null');
				this.setCurrentTask(new Task(item));
				this.addWidgets(node, item);
				//this._enableFilter(false);
				this.state = this.states.editing;
			},
			
			sync: function()
			{
				this.checkSync(true);
			},
			
			selectname: function()
			{
				this.state = this.states.rename;
				//this._enableFilter(false);
			},
			
			listloaded: function()
			{
			},
			
			activate: function()
			{
			}
		},
		
		adding:
		{
			_name: 'adding',
			
			reset: function()
			{
				if (!this.saveCurrentTask()) 
				{
					// If there we no changes to commit, then we remove the task
					this.listElement.mojo.noticeRemovedItems(this.getListIndexByItem(this.task), 1);
				}
				else
				{
					this._fadeTaskIfComplete(this.task, this.modifiedTask);
				}
				this.setCurrentTask(null);
				this.state = this.states.idle;
				this.checkSync();
			},
			
			add: function()
			{
				this.state.enter.call(this);
			},
			
			back: function()
			{
				// If the current task is saved (ie. worth saving) then we pop back to the main screen.
				// Otherwise we just we 'reset' which will remove the current entry and stay on this screen.
				if (this.saveCurrentTask())
				{
					// Pop the scene.
					this.state = this.states.pop;
					this._popScene();
				}
				else
				{
					this.state.reset.call(this);
				}
			},
			
			enter: function()
			{
				this._fadeTaskIfComplete(this.task, this.modifiedTask);
				var currentTask = this.task;
				if (this.saveCurrentTask())
				{
					// If we commit changes, then we create a new task and automatically.
					this.setCurrentTask(null);
					this.addItem(currentTask);
				}
				else
				{
					// Otherwise we just stay with the current task and ignore this.
					var index = this.getListIndexByItem(currentTask);
					this.listElement.mojo.revealItem(index);
					this.listElement.mojo.focusItem(currentTask);
					Mojo.Log.info("NOSAVE %j %d", currentTask, index);
				}
			},
			
			blur: function()
			{
				this.state.reset.call(this);
			},
			
			select: function(node, item)
			{
				if (this.task != item) 
				{
					this.state.reset.call(this);
				}
			},
			
			edit: function()
			{
				// If you push keys and tap fast enough, you can sometimes edit a task
				// before its finished saving - so currentTask is null.  We ignore these
				// attempted edits.
				if (this.task) 
				{
					this.state = this.states.push;
					this.editCurrentTask();
				}
			},
			
			setsubject: function(subject)
			{
				this.modifiedTask.subject = subject;
			},
			
			setcompleted: function(completed)
			{
				this.modifiedTask.completed = completed;
				// Put the focus back on the text field
				this.listElement.mojo.focusItem(this.task);
			},
			
			sync: function()
			{
				this._needsync = true;
			},
			
			selectname: function()
			{
			},
			
			listloaded: function()
			{
				Mojo.Log.info("LISTLOADED - adding item");
				this.addItem(null);
			},
		},
		
		editing:
		{
			_name: 'editing',
			
			reset: function()
			{
				this._fadeTaskIfComplete(this.task, this.modifiedTask);
				this.saveCurrentTask();
				this.setCurrentTask(null);
				this.state = this.states.idle;
				this.checkSync();
				this._blurCurrent();
			},
			
			add: function()
			{
				this.state.enter.call(this);
			},
			
			back: function()
			{
				this.state.reset.call(this);
			},
			
			enter: function()
			{
				this._fadeTaskIfComplete(this.task, this.modifiedTask);
				var currentTask = this.task;
				this.saveCurrentTask();
				this.setCurrentTask(null);
				this.addItem(currentTask);
				this.state = this.states.adding;
			},
			
			blur: function()
			{
				this.state.reset.call(this);
			},
			
			select: function(node, item)
			{
				if (this.task.id != item.id) 
				{
					this.saveCurrentTask();
					// Select the new task
					this.setCurrentTask(new Task(item));
					this.addWidgets(node, item);
					this.listElement.mojo.focusItem(item);
				}
			},
			
			edit: function()
			{
				this.saveCurrentTask();
				this.state = this.states.push;
				this.editCurrentTask();
			},
			
			setsubject: function(subject)
			{
				this.modifiedTask.subject = subject;
			},
			
			setcompleted: function(completed, target)
			{
				this.modifiedTask.completed = completed;
				// Give up the focus and go idle
				this.state.reset.call(this);
			},
			
			sync: function()
			{
				this._needsync = true;
			},
			
			selectname: function()
			{
				this.state = this.states.rename;
			},
			
			listloaded: function()
			{
/*
				var index = this.getListIndexByItem(this.task);
				if (index !== undefined) 
				{
					this.listElement.mojo.noticeUpdatedItems(index, [this.task]);
					this.listElement.mojo.revealItem(index);
				}
				this.state = this.states.idle;
				this._enableFilter(true);
				this.checkSync();
*/
				var index = this.getListIndexByItem(this.task);
				if (index !== undefined) 
				{
					this.listElement.mojo.noticeUpdatedItems(index, [this.task]);
					this.listElement.mojo.revealItem(index);
					this.addWidgets(this.listElement.mojo.getNodeByIndex(index).querySelector('[name="subject"]'), this.task);
					this.listElement.mojo.focusItem(this.task);
				}
				else 
				{
					this.state = this.states.idle;
					//this._enableFilter(true);
					this.checkSync();
				}

			},
		},
		
		rename:
		{
			_name: 'rename',
			
			reset: function()
			{
				this.state = this.states.idle;
				//this._enableFilter(true);
				this._blurCurrent();
			},
			
			add: function()
			{
				this._untouched = false; // If we add an item from an untouched, we become touched
				this.addItem(null);
				this.state = this.states.adding;
			},
			
			back: function()
			{
				// Pop the scene.
				this.state = this.states.pop;
				this._popScene();
			},
			
			enter: function()
			{
				this.state.add.call(this);
			},
			
			blur: function()
			{
				this.state.reset.call(this);
			},
			
			select: function(node, item)
			{
				Mojo.assert(this.task == null, 'Attempting to assign to this.task which isnt null');
				this.setCurrentTask(new Task(item));
				this.addWidgets(node, item);
				this.state = this.states.editing;
			},
			
			edit: function()
			{
			},
			
			sync: function()
			{
				this.checkSync(true);
			},
			
			selectname: function()
			{
			},
			
			listloaded: function()
			{
			},
			
			activate: function()
			{
			},
		},
		
		push:
		{
			_name: 'push',
			
			reset: function()
			{
			},
			
			activate: function()
			{
				this.setCurrentTask(null);
				this.state = this.states.idle;
				//this._enableFilter(true);
				this.checkSync();
			},
			
			select: function()
			{
			},
			
			blur: function()
			{
			},
			
			edit: function()
			{
			},
			
			sync: function()
			{
				this._needsync = true;
			},
			
			selectname: function()
			{
			},
			
			listloaded: function()
			{
			},
		},
		
		pop:
		{
			_name: 'pop',
			
			reset: function()
			{
				if (this.task) 
				{
					this.saveCurrentTask();
					this.setCurrentTask(null);
				}
			},
			
			blur: function()
			{
			},
			
			sync: function()
			{
			},
			
			listloaded: function()
			{
			},
		},
	},
	
	/**************************************************************/
	/* Task */
	
	currentTask: null,
	
	setCurrentTask: function(task)
	{
		if (this.task)
		{
			this.task.removeUpdateListener(this);
		}
		State.task = task;
		this.task = task;
		this.modifiedTask = {};
		if (task) 
		{
			var self = this;
			this.task.addUpdateListener(this, function(model, diff, scene)
			{
				Mojo.Log.info("****************** ON CURRENT TASK UPDATED ******");
				
				// If the 'duedate' or 'completed' has changed, we need to fixup the dueItemCount in the tasklist
				if ('dueDate' in diff || 'completed' in diff)
				{
					self.taskList.update(this, { dueItemCount: null });
				}
				
				// The remaining changes are only necessary if this scene didnt make them (otherwise we've done them already)
				if (scene != self) 
				{
					Mojo.Log.info("--model %j diff %j list", model, diff);
					
					// If we are marked deleted, or completed and we shouldnt be shown, remove from list
					if (model.deleted || (model.completed && self.modifiedTaskList.show == "remaining"))
					{
						Mojo.Log.info("-cancelAddedItem");
						self.listElement.mojo.noticeRemovedItems(self.getListIndexByItem(model), 1);
					}
					// If the task's tasklist has changed and we're not the all-tasks list, we must remove it from the list
					else if ('taskListId' in diff && !TaskList.isAllTasks(State.taskList)) 
					{
						Mojo.Log.info("-cancelAddedItem");
						self.listElement.mojo.noticeRemovedItems(self.getListIndexByItem(model), 1);
						// Force a list-of-list update
						State.listOfLists.update();
					}
					// If we change the duedate and we're ordering by due date, we must update the entire list (re-sort)
					else if (self.modifiedTaskList.order == "duedate" && "dueDate" in diff)
					{
						self.invalidateList();
					}
					// If we change the priority and we're ordering by priority, we must update the entire list (re-sort)
					else if (self.modifiedTaskList.order == "priority" && "priority" in diff)
					{
						self.invalidateList();
					}
					// Otherwise, just update us in the list
					else 
					{
						Mojo.Log.info("-updateItem");
						self.listElement.mojo.noticeUpdatedItems(self.getListIndexByItem(model), [ model ]);
					}
				}
			});
		}
	},
	
	/**
	 * Event fired when a task looses focus.
	 * ƒadd
	 * @param {Object} event
	 */
	onTaskBlur: function(event)
	{
		// Only fire if its the current task .. and defer it since we actually want this to come
		// after we've processed any other events relevant to what's being blurred.
		var self = this;
		(function()
		{
			var item = self.listElement.mojo.getItemByNode(event.target);
			if (item != null && item == self.task) 
			{
				Mojo.Log.info("Blur %j %j", item, self.task);
				self.state.blur.call(self);
			}
		}).defer();
	},
    
	/**
	 * Event fired when we change the task.
	 * This event is not passed to the state machine.
	 * 
	 * @param {Object} event
	 */
    onTaskChanged: function(event)
	{
		if (event.property == 'deleted' && event.value == false)
		{
			// Aborted a delete
			Mojo.Log.info("Reset");
			this.state.reset.call(this);
		}
        else if (event.target.id)
		{
            // We get 2 events when entering high ascii characters
            // causing the event.target.id to have value of either the div or the text area
            // in the div. Not reliable but we know that it contains either checkbox- or subject-
            var item = this.listElement.mojo.getItemByNode(event.target);
            Mojo.Log.info("New value: %s (id: %s) - item %j", event.value, event.target.id, item);
            
            if (item) 
			{
                if (event.target.id.indexOf('completed-') != -1) 
				{
					Mojo.Log.info("setcompleted");
					this.state.setcompleted.call(this, event.value, event.target);
                }
                else if (event.target.id.indexOf('subject-') != -1) 
				{
					Mojo.Log.info("setsubject");
					this.state.setsubject.call(this, event.value);
                }
            }
        }
    },
	
	/**
	 * Event fired when we hit the a key.
	 * 
	 * @param {Object} event
	 */
	onKey: function(event)
	{
		if (event.keyCode === Mojo.Char.enter && event.target.id && event.target.id.indexOf('subject-') != -1)
		{
			Mojo.Log.info("Enter");
			this.state.enter.call(this);
		}
	},
	
	/**
	 * Event fired when we tap a list entry.
	 * 
	 * @param {Object} event
	 */
	handleTapCapture: function(event)
	{
		var item = this.listElement.mojo.getItemByNode(event.target);
		if (item)
		{
			Mojo.Log.info("Select %j", item);
			this._stopScrolling();
			this.state.select.call(this, event.target, item);
		}
	},
	
	/**
	 * We cannot reorder a new item (one without an id).  If we detech this we stop
	 * the reorder from starting.
	 * 
	 * @param {Object} event
	 */
	handleHoldCapture: function(event)
	{
		 var item = this.listElement.mojo.getItemByNode(event.target);
		 if (item && !item.id)
		 {
		 	Event.stop(event);
		 }
	},
    
    handleTap: function(event)
	{
		if (event.item)
		{
			Mojo.Log.info("Edit %j", event.item);
			this.state.edit.call(this, event.item);
		}
	},
	
	editCurrentTask: function()
	{
		// Save the task list if necessary (name may have changed)
		this.saveTaskList();
		
		Mojo.Log.info("Pushing scene task %j", this.task);
	    this.controller.stageController.pushScene('task', this.modifiedTask);
	},

	addItem: function(previousTask)
	{
		Mojo.Log.info("onAddTask", curTicks());

	    // Save the list so that we have a valid taskList.id
		// The list will be empty, so no need to refresh it
		if (!this.modifiedTaskList.name)
		{
			this.modifiedTaskList.name = TaskList.getNameWithDefault(this.taskList);
		}
		var self = this;
		this.saveTaskList(function()
		{
			self.setCurrentTask(self._newTask(previousTask));
		});
	},
	
	_nextNewId: 1,

	/**
	 * Called when tapping on the + button
	 *
	 * handle nicely
	 * - when creating a new task
	 * - when creating a new task while one is already created (but not modified yet)
	 * - when creating a new task while one is already created and modified and to be saved (arg)
	 *
	 */
	_newTask: function(previousTask)
	{
		var aboveOrBelow = 'below';
	    Mojo.Log.info('_newTask %s %j', aboveOrBelow, previousTask);
		
		var task = new Task({ subject: '', completed: false });
		task._isnew = true;
	    
	    // Increment the newId so that we never get 2 items with the same id
		task._newId = this._nextNewId++;
	    task.taskListId = this.taskList.id;
    
	    // Special case where we are currently creating an item
	    // No need to query the database for the position
		if (previousTask) 
		{
			Mojo.Log.info("--position %s", previousTask.position);
			
			task.priority = previousTask.priority; // Inherit the priority
			if (this.modifiedTaskList.order == "duedate")
			{
				task.dueDate = previousTask.dueDate; // Inhert duedate is we're ordering by duedate
			}
			
			var index = this.getListIndexByItem(previousTask) + 1;
			// Insert the element at the same index as the previousTask and bump the position by 1000
			task.position = previousTask.position + 1000;
			this.listElement.mojo.insertItemsAt(index, [task]);
			this.listElement.mojo.revealItem(index);
			this.addWidgets(this.listElement.mojo.getNodeByIndex(index).querySelector('[name="subject"]'), task);
			this.listElement.mojo.focusItem(task);
			
			Mojo.Log.info("New task %j %j", task, this.task);
		}
		else 
		{
			// Find the position first to scroll to it first
			var self = this;
			task.priority = 2; // Default priority
			task.getPositionIndex(this, function(status, position)
			{
				var index = self.listElement.mojo.getLength();
				Mojo.Log.info('task position: %d %d', position, index);
				// 1000 is the default space between task to allow fast moving around
				task.position = position + 1000;
				self.listElement.mojo.insertItemsAt(index, [task]);
				self.listElement.mojo.revealItem(index);
				var node = self.listElement.mojo.getNodeByIndex(index);
				if (node) 
				{
					// If we find the node we just added, focus on it.  We might not find the node however if the list is being refreshed
					self.addWidgets(node.querySelector('[name="subject"]'), task);
					self.listElement.mojo.focusItem(task);
				}
				
				Mojo.Log.info("New task %j %j", task, self.task);
			});
		}
		
		return task;
	},

	/**
	 * Saved any pending changes (e.g. a newly updated task)
	 */
	saveCurrentTask: function()
	{
		var task = this.task;
		if (task)
		{
			if (task.update(this, this.modifiedTask))
			{
				// Fixup the subject (if necessary)
				task.subject = Task.nameFromItem(task);
				
				// Make sure the task changes are refected in the list (incase it gets redrawn)
				var idx = this.getListIndexByItem(task);
				if (idx !== undefined && !(task.completed && this.modifiedTaskList.show == "remaining")) 
				{
					this.listElement.mojo.noticeUpdatedItems(idx, [task]);
				}
				
				Mojo.Log.info("Saving task %j %j", task, this.modifiedTask);
				task.save(this);
				// If we modified the 'completed' state, we must update the tasklist dueItemCount
				if ('completed' in this.modifiedTask) 
				{
					this.taskList.update(this, { dueItemCount: null });
				}
				// If we dont have a taskListId then this will go under 'unfiled' which may in turn make that list appear
				if (!task.getTaskListId()) 
				{
					State.listOfLists.update();
				}
				
				// If we've modifed the task in the dashboard, we must force it to update
				if (task.id !== undefined && task.id == State.dashboard.taskId)
				{
					State.dashboard.update(this, { taskName: task.subject });
				}
				return true;
			}
		}
		return false;
	},
	
	addWidgets: function(node, model)
	{
		var common = Element.extend(node).up('.title');
		if (common) 
		{
			var subject = common.down('[name="subject"]');
			var completed = common.down('[name="completed"]');
			Mojo.Log.info("addWidgets", subject.id, completed.id);
			
			if (subject.getAttribute('x-mojo-element') != 'TextField') 
			{
				subject.setAttribute('x-mojo-element', 'TextField');
				model.titleFormatted = model.subject;
				this.controller.setWidgetModel(subject, model);
			}
			if (completed.getAttribute('x-mojo-element') != 'CheckBox') 
			{
				completed.setAttribute('x-mojo-element', 'CheckBox');
				this.controller.setWidgetModel(completed, model);
			}
			this.controller.newContent(common);
		}
	},
	
	_fadeTaskIfComplete: function(task, mtask)
	{
		if (this.modifiedTaskList.show == "remaining" && mtask.completed) 
		{
			var target = this.listElement.mojo.getNodeByIndex(this.getListIndexByItem(task));
			this._fadeAndRemoveTask(target.down(".row-fader"), task);
		}
	},
	
	_fadeAndRemoveTask: function(fader, task)
	{
		var self = this;
		(function()
		{
			fader.style.display = "block";
			Mojo.Animation.animateStyle(fader, "opacity", "bezier", 
			{
				curve: "over-easy",
				from: 0,
				to: 1,
				duration: 0.3,
				styleSetter: function(value)
				{
					fader.style.opacity = value;
				},
				onComplete: function()
				{
					self.listElement.mojo.noticeRemovedItems(self.getListIndexByItem(task), 1);
				}
			});
		}).delay(0.2);
	},
	
 	/**************************************************************/
	/* Sync */
	
	checkSync: function(force)
	{
		if (this._needsync || force)
		{
			this.invalidateList();
			this._needsync = false;
		}
	},
	
	_ignoreNextSync: function()
	{
		// Ignore the next sync, so long as it turn up within 10s
		this._ignoresync = (new Date()).getTime() + 10000;
	},
		
 	/**************************************************************/
	/* TaskList */
	
	onTaskListName: function(event)
	{
		Mojo.Log.info("Selectname");
		this.state.selectname.call(this);
	},
	
	onTaskListEnterKey: function(event)
	{
		if (event.keyCode != Mojo.Char.escape) 
		{
			this._untouched = false; // When we type in a listname it becomes untouched
		}
		if (event.keyCode === Mojo.Char.enter)
		{
			Mojo.Log.info("Enter");
			this.state.enter.call(this);
		}
	},
    
    saveTaskList: function(callback)
	{
		Mojo.Log.info("saveTaskList");
		var taskList = this.taskList;
		if (!this._untouched && taskList && taskList.update(this, this.modifiedTaskList) && !TaskList.isAllTasks(taskList)) 
		{
			// Fixup the name (if necessary)
			taskList.name = TaskList.getNameWithDefault(taskList);
				
			Mojo.Log.info("** Saving task list");
			taskList.save(this, function()
			{
				// Update dashboard if necessary
				if (State.dashboard.taskListId == taskList.id) 
				{
					State.dashboard.update(this, { taskListName: taskList.name });
				}
				callback && callback();
			});
		}
		else 
		{
			callback && callback();
		}
	},
    
    taskListNameChanged: function(event)
	{
        this.modifiedTaskList.name = event.value;
    },
	
	/**************************************************************/
    
    handleCommand: function(event)
	{
		Mojo.Log.info("handleCommand", event.type, event.command);
		if (event.type == Mojo.Event.back) 
		{
			Mojo.Log.info("Back");
			this.state.back.call(this);
			Event.stop(event);
        }
        else if (event.type == Mojo.Event.command) 
		{
			if (event.command == 'add') 
			{
				Mojo.Log.info("Add", this.state._name);
				this._stopScrolling();
				this.state.add.call(this);
			}
        }
    },
    
    handleFilter: function(event)
	{
        this.filterString = event.filterString;
        
        if (this.filterString) 
		{
			this.filterStringRegExp = new RegExp('(' + Weave.Utilities.RegExp.escape(this.filterString) + ')', 'i');
            this.sceneElement.addClassName('filtering');
            this.controller.select('.palm-filterfield-spacer')[0].addClassName('filtering');
        }
        else 
		{
			this.filterStringRegExp = null;
            this.sceneElement.removeClassName('filtering');
			this.controller.select('.palm-filterfield-spacer')[0].removeClassName('filtering');
        }
		
		// Make sure we're not editing/selected/etc...
		this.state.reset.call(this);
        
        this.invalidateList();
    },
	
	_enableFilter: function(enable)
	{
		this._filterState.disabled = !enable;
		this.controller.modelChanged(this._filterState);
	},
    
    handleDelete: function(event)
	{
        var item = event.item;
        if (item) 
		{
			if (item.id) 
			{
				var self = this;
				new Task(item).remove(this, function()
				{
					self._ignoreNextSync(); // The update may generate a sync - which we ignore
				});
			}
			if (item.id == this.task.id) 
			{
				this.setCurrentTask(null);
			}
			// If we've modifed the task in the dashboard, we must force it to update
			if (item.id == State.dashboard.taskId)
			{
				State.dashboard.update(this, { taskId: item.id }, true);
			}
			// Deleting a task may effect the due count
			this.taskList.update(this, { dueItemCount: null });
			// If list count goes to zero or we're the alltasks list, we may have to change how the list is displayed
			if (this.listElement.mojo.getLength() == 0 || TaskList.isAllTasks(this.taskList)) 
			{
				// Force a list-of-list update
				State.listOfLists.update();
			}
		}
		this.state.reset.call(this);
    },
    
    handleReorder: function(event)
	{
        var item = event.item;
        if (item && item.id) 
		{
            Mojo.Log.info('Moving from %s to %s', event.fromIndex, event.toIndex);
            var self = this;
			new Task(item).move(this, this.modifiedTaskList.show, this.filterString, event.toIndex, event.fromIndex, function()
			{
				self._ignoreNextSync(); // The update may generate a sync - which we ignore
			});
        }
		this.state.reset.call(this);
    },
    
    itemsCallback: function(widget, offset, limit)
	{
		Mojo.Log.info("itemsCallback", offset, limit, curTicks());
		
		var self = this;
		this.taskList.getTasks(this, this.modifiedTaskList.show, this.filterString, this.modifiedTaskList.order, offset, limit, function(status, list, count)
		{
			//Mojo.Log.info("itemsCallback .. calledback %d %d %d", offset, count, limit);
			
            self.formatItems(offset, list, count);
			
			// Handle filtering count
			if (self.filterString)
			{
				self.filterFieldElement.mojo.setCount(count);
				// If we're filtering a list, put the focus back at the top
				if (offset == 0) 
				{
					self.listElement.mojo.revealItem(0);
				}
			}
			// Tell the state machine we're done
			if (count <= offset + limit) 
			{
				self.state.listloaded.call(self);
			}
        });
    },
    
    // Weird actually we have a dummy propery that we don't use
    // Instead we simply update the existing item.
    formatItem: function(dummy, item)
	{
		var due = item.dueDate;
        if (due) 
		{
            item.infoClassFormatted = 'label due-date info';
            item.dueDateFormatted = Mojo.Format.formatDate(new Date(due), {
				date: 'short'
			});
        }
        else 
		{
            item.infoClassFormatted = 'icon tasks-icon info';
			item.dueDateFormatted = '';
        }
        
        item.classFormatted = Task.priorityName(item.priority);
        
        // Special trick for the newly created task
        if (item._newId) 
		{
            // No name yet
            item.titleFormatted = item.subject.split('\n')[0];
			item.idFormatted = 'new' + item._newId;
        }
        else 
		{
            item.titleFormatted = Task.nameFromItem(item).split('\n')[0];
			item.idFormatted = item.id;
        }
		
		item.titleFormatted = item.titleFormatted.escapeHTML();
		if (this.filterStringRegExp)
		{
            item.titleFormatted = item.titleFormatted.replace(this.filterStringRegExp, '<span class="list-highlight">$1</span>');
		}
		
		//Mojo.Log.info("Item %j", item);
    },
       
    formatItems: function(offset, list, count)
	{
        // Mojo.Log.info("formatItems", this.filterString, response.offset, this.filterFieldIsDisable);
        // Disable the filter field is no data
        //if (!this.filterString && offset == 0 && count == 0) 
		//{
        //    this.filterFieldIsDisable = true;
        //}
        //else 
		//{
        //    this.filterFieldIsDisable = false;
        //}
		
		Mojo.Log.info("TasksAssistant::time before formatItems", curTicks());
		Mojo.Log.info('-- data update at %s/%s total %s', offset, list.length, count);

/* NOT YET - makes the screen flash if we've scrolled down ----			
		// The idea here is to get something on the screen, and then dump the rest into the list after this
		var mojo = this.listElement.mojo;
		if (offset == 0 && count > 8) 
		{
			var slice = list.slice(0, 8);
			var len = slice.length;
			mojo.updateItemsAndTotalLength(offset, slice, len);
			if (list.length > len) 
			{
				mojo.updateItemsAndTotalLength.bind(mojo).defer(offset + len, list.slice(len), count);
			}
		}
		else
		{
			mojo.updateItemsAndTotalLength(offset, list, count);
		}
*/
		this.listElement.mojo.updateItemsAndTotalLength(offset, list, count);

        Mojo.Log.info('TasksAssistant::time in formatItems', curTicks());
    },
	
	_itemsDividers: function(model)
	{
		Mojo.Log.info("itemsDividers", this.modifiedTaskList.order);
		switch (this.modifiedTaskList.order)
		{
			case "priority":
				// Explicit localization
				switch (Task.priorityName(model.priority))
				{
					case "low":
						return $L("Low");
					case "high":
						return $L("High");
					case "normal":
					default:
						return $L("Normal");
				}
			case "duedate":
			case "myorder":
			default:
				return undefined;
        };
	},

	handleDeleteListCmd: function()
	{
		var taskList = this.taskList;
		var self = this;
		this.controller.showAlertDialog(
		{
	        onChoose: function(value) 
			{
				if (value == 'delete')
				{
				    if (taskList.id) 
					{
				        Mojo.Log.info("Delete: %s", taskList.id);
						taskList.remove(self);
						taskList.update(self, { id: undefined });
						// Force a dashboard update because the system doesnt always do this (and I'm not going to try to fix it right now).
						State.dashboard.update(self, { taskListId: null, taskId: null }, true);
				    }
					self.modifiedTaskList = {}; // Prevent it from being saved by deleting any modifications
				    self._popScene();
				}
			},
	        title: $L("Delete list"),
	        message: $L("Are you sure you want to delete this list?"),
	        choices:
			[
	             {label:$L('Delete'), value:'delete', type:'negative'},
	             {label:$L("Cancel"), value:'cancel'},
	        ]
        });
		Mojo.Log.info("Reset");
		this.state.reset.call(this);
	},
	
	_popScene: function()
	{
		this.controller.stageController.popScene();
	},
	
	_onSceneDeactivate: function()
	{
		this.saveTaskList();
		this.state.reset.call(this);
	},
	
	_stopScrolling: function()
	{
		var scroller = Mojo.View.getScrollerForElement(this.controller.sceneElement);
		var pos = scroller.mojo.getScrollPosition();
		scroller.mojo.scrollTo(pos.left, pos.top, false, true);
	},

	invalidateList: function()
	{
		this._needsync = false;
		this.listElement.mojo.reloadAll();
	},

	handleMarkCompletedCmd: function()
	{
		var self = this;
		this.taskList.setAllComplete(this, true, function()
		{
			self._ignoreNextSync(); // The update may generate a sync - which we ignore
			// We dont know the due count now and cannot calculate it - so we'll have to refetch it form the DB
			self.taskList.update(self, { dueItemCount: null });
		});
		if (this.modifiedTaskList.show == 'all') 
		{
			this._forAllTasks.bind(this, 'completed', 'true').defer();
		}
		else
		{
			this.invalidateList();
		}
		Mojo.Log.info("Reset");
		this.state.reset.call(this);
	},

	handleMarkIncompleteCmd: function()
	{
		var self = this;
		this.taskList.setAllComplete(this, false, function()
		{
			self._ignoreNextSync(); // The update may generate a sync - which we ignore
			// We dont know he due count now and cannot calculate it - so we'll have to refetch it form the DB
			self.taskList.update(self, { dueItemCount: null });
		});
		if (this.modifiedTaskList.show == 'all') 
		{
			this._forAllTasks.bind(this, 'completed', false).defer();
		}
		else
		{
			this.invalidateList();
		}
		Mojo.Log.info("Reset");
		this.state.reset.call(this);
	},

	handleSetDueDateCmd: function()
	{
	    var self = this;
	    this.controller.showDialog({
	        template: 'datepicker/datepicker-scene',
	        assistant: new DatePickerAssistant({
				sceneAssistant: this,
		        title: $L('Set due date for all'),
		        callback: function(dueDate)
				{
					self.taskList.setAllDueDate(self, dueDate, function()
					{
						self._ignoreNextSync(); // The update may generate a sync - which we ignore
						// We dont know the due count now and cannot calculate it - so we'll have to refetch it from the DB
						self.taskList.update(this, { dueItemCount: null });
					});
					self._forAllTasks.bind(self, 'dueDate', dueDate).defer();
		        }
		    }),
		});
		Mojo.Log.info("Reset");
		this.state.reset.call(this);
	},

	handlePurgeCompletedCmd: function()
	{
	    var self = this;
		this.controller.showAlertDialog(
		{
	        onChoose: function(value) 
			{
				if (value == "delete") 
				{
					var scrim = new Weave.Utilities.Scrim(this, "scrim", $L("Deleting completed tasks..."), true);
					self.taskList.purgeAllComplete(this, function()
					{
						scrim.stop();
						self.invalidateList();
						// If the unfile or alltasks lists, we must update the list-of-lists after a purge because it
						// might make the unfiled list disappear
						if (TaskList.isDefault(self.taskList) || TaskList.isAllTasks(self.taskList)) 
						{
							State.listOfLists.update();
						}
					});
				}
			},
	        title: $L("Delete Completed Tasks"),
	        message: $L("Are you sure you want to delete all completed tasks?"),
	        choices:
			[
	             {label:$L('Delete Completed'), value:'delete', type:'negative'},
	             {label:$L("Cancel"), value:'cancel'},
	        ]
        });
		Mojo.Log.info("Reset");
		this.state.reset.call(this);
	},
	
	handleHideShowCompletedCmd: function(event, item)
	{
		var nstate = this.modifiedTaskList.show == 'all' ? 'remaining' : 'all';
		this._setShow(nstate);
		if (nstate == "all")
		{
			item.label = $L("Hide Completed");
		}
		else
		{
			item.label = $L("Show Completed");
		}
		this.controller.modelChanged(this._appmenu.model);
	},
	
	_forAllTasks: function(propname, propvalue)
	{
		var models = this.listElement.mojo.getModels();
		for (var i = models.length - 1; i >= 0; i--)
		{
			models[i][propname] = propvalue;
		}
		this.listElement.mojo.rerenderAll();
	},

	_setShow: function(what)
	{
	    if (this.modifiedTaskList.show != what) 
		{
			this._untouched = false;
			this.modifiedTaskList.show = what;
			this.saveCurrentTask();
			this.invalidateList();
			this.listElement.mojo.revealItem(0);
		}
	},

	showOrderChanged: function(event)
	{
	    Mojo.Log.info('show order %s', event.value);
		this._untouched = false;
		if (event.value == "position")
		{
			this.listAttr.reorderable = true;
		}
		else
		{
			this.listAttr.reorderable = false;
		}
		this.saveCurrentTask();
		this.invalidateList();
		// Remove old dividers
		var divs = this.listElement.querySelectorAll('table.palm-list-section-label');
		var len = divs.length;
		for (var i = 0; i < len; i++)
		{
			divs[i].parentNode.removeChild(divs[i]);
		}
		this.state.reset.call(this);
	},
	
	_blurCurrent: function()
	{
		var focus = this.sceneElement.querySelector(':focus');
		if (focus)
		{
			focus.blur();
		}
	},
	
	// ----------------- List utilities ------------------
	
	/**
	 * Find the index of an item in the list.
	 * 
	 * @param {Object} item
	 */
	getListIndexByItem: function(item)
	{
		var idx = undefined;
		if (item) 
		{
			if (item._newId !== undefined)
			{
				idx = this.listElement.mojo.getItemIndexByProperty(item._newId, '_newId');
			}
			else if (item.id !== undefined) 
			{
				idx = this.listElement.mojo.getItemIndexByProperty(item.id, 'id');
			}
		}
		//if (idx === undefined) Mojo.Log.error("MISSING ID %j", item);
		return idx;
	},
});
