text: const Note = require('lib/models/Note.js');
  const Folder = require('lib/models/Folder.js');
  const ArrayUtils = require('lib/ArrayUtils.js');
  
  const defaultState = {
  	notes: [],
  	notesSource: '',
  	notesParentType: null,
  	folders: [],
  	tags: [],
  	masterKeys: [],
  	notLoadedMasterKeys: [],
  	searches: [],
  	selectedNoteIds: [],
  	selectedFolderId: null,
  	selectedTagId: null,
  	selectedSearchId: null,
  	selectedItemType: 'note',
  	showSideMenu: false,
  	screens: {},
  	historyCanGoBack: false,
  	syncStarted: false,
  	syncReport: {},
  	searchQuery: '',
  	settings: {},
  	sharedData: null,
  	appState: 'starting',
  	hasDisabledSyncItems: false,
  	newNote: null,
  	customCss: '',
  	collapsedFolderIds: [],
  	clipperServer: {
  		startState: 'idle',
  		port: null,
  	},
  	decryptionWorker: {
  		state: 'idle',
  		itemIndex: 0,
  		itemCount: 0,
  	},
  	selectedNoteTags: [],
  	resourceFetcher: {
  		toFetchCount: 0,
  	},
  };
  
  const stateUtils = {};
  
  stateUtils.notesOrder = function(stateSettings) {
  	return [{
  		by: stateSettings['notes.sortOrder.field'],
  		dir: stateSettings['notes.sortOrder.reverse'] ? 'DESC' : 'ASC',
  	}];
  }
  
  function arrayHasEncryptedItems(array) {
  	for (let i = 0; i < array.length; i++) {
  		if (!!array[i].encryption_applied) return true;
  	}
  	return false
  }
  
  function stateHasEncryptedItems(state) {
  	if (arrayHasEncryptedItems(state.notes)) return true;
  	if (arrayHasEncryptedItems(state.folders)) return true;
  	if (arrayHasEncryptedItems(state.tags)) return true;
  	return false;
  }
  
  function folderSetCollapsed(state, action) {
  	const collapsedFolderIds = state.collapsedFolderIds.slice();
  	const idx = collapsedFolderIds.indexOf(action.id);
  
  	if (action.collapsed) {
  		if (idx >= 0) return state;
  		collapsedFolderIds.push(action.id);
  	} else {
  		if (idx < 0) return state;
  		collapsedFolderIds.splice(idx, 1);
  	}
  
  	newState = Object.assign({}, state);
  	newState.collapsedFolderIds = collapsedFolderIds;
  	return newState;
  }
  
  // When deleting a note, tag or folder
  function handleItemDelete(state, action) {
  	let newState = Object.assign({}, state);
  
  	const map = {
  		'FOLDER_DELETE': ['folders', 'selectedFolderId'],
  		'NOTE_DELETE': ['notes', 'selectedNoteIds'],
  		'TAG_DELETE': ['tags', 'selectedTagId'],
  		'SEARCH_DELETE': ['searches', 'selectedSearchId'],
  	};
  
  	const listKey = map[action.type][0];
  	const selectedItemKey = map[action.type][1];
  
  	let previousIndex = 0;
  	let newItems = [];
  	const items = state[listKey];
  	for (let i = 0; i < items.length; i++) {
  		let item = items[i];
  		if (item.id == action.id) {
  			previousIndex = i;
  			continue;
  		}
  		newItems.push(item);
  	}
  
  	newState = Object.assign({}, state);
  	newState[listKey] = newItems;
  
  	if (previousIndex >= newItems.length) {
  		previousIndex = newItems.length - 1;
  	}
  
  	const newId = previousIndex >= 0 ? newItems[previousIndex].id : null;
  	newState[selectedItemKey] = action.type === 'NOTE_DELETE' ? [newId] : newId;
  
  	if (!newId && newState.notesParentType !== 'Folder') {
  		newState.notesParentType = 'Folder';
  	}
  
  	return newState;
  }
  
  function updateOneItem(state, action, keyName = '') {
  	let itemsKey = null;
  	if (keyName) itemsKey = keyName;
  	else {
  		if (action.type === 'TAG_UPDATE_ONE') itemsKey = 'tags';
  		if (action.type === 'FOLDER_UPDATE_ONE') itemsKey = 'folders';
  		if (action.type === 'MASTERKEY_UPDATE_ONE') itemsKey = 'masterKeys';
  	}
  
  	let newItems = state[itemsKey].splice(0);
  	let item = action.item;
  
  	var found = false;
  	for (let i = 0; i < newItems.length; i++) {
  		let n = newItems[i];
  		if (n.id == item.id) {
  			newItems[i] = Object.assign(newItems[i], item);
  			found = true;
  			break;
  		}
  	}
  
  	if (!found) newItems.push(item);
  
  	let newState = Object.assign({}, state);
  
  	newState[itemsKey] = newItems;
  
  	return newState;
  }
  
  function defaultNotesParentType(state, exclusion) {
  	let newNotesParentType = null;
  
  	if (exclusion !== 'Folder' && state.selectedFolderId) {
  		newNotesParentType = 'Folder';
  	} else if (exclusion !== 'Tag' && state.selectedTagId) {
  		newNotesParentType = 'Tag';
  	} else if (exclusion !== 'Search' && state.selectedSearchId) {
  		newNotesParentType = 'Search';
  	}
  
  	return newNotesParentType;
  }
  
  function changeSelectedFolder(state, action) {
  	newState = Object.assign({}, state);
  	newState.selectedFolderId = 'folderId' in action ? action.folderId : action.id;
  	if (!newState.selectedFolderId) {
  		newState.notesParentType = defaultNotesParentType(state, 'Folder');
  	} else {
  		newState.notesParentType = 'Folder';
  	}
  	return newState;
  }
  
  function changeSelectedNotes(state, action) {
  	let noteIds = [];
  	if (action.id) noteIds = [action.id];
  	if (action.ids) noteIds = action.ids;
  	if (action.noteId) noteIds = [action.noteId];
  
  	// const noteIds = 'id' in action ? (action.id ? [action.id] : []) : action.ids;
  
  	let newState = Object.assign({}, state);
  
  	if (action.type === 'NOTE_SELECT') {
  		newState.selectedNoteIds = noteIds;
  		newState.newNote = null;
  		return newState;
  	}
  
  	if (action.type === 'NOTE_SELECT_ADD') {
  		if (!noteIds.length) return state;
  		newState.selectedNoteIds = ArrayUtils.unique(newState.selectedNoteIds.concat(noteIds));
  		newState.newNote = null;
  		return newState;
  	}
  
  	if (action.type === 'NOTE_SELECT_REMOVE') {
  		if (!noteIds.length) return state; // Nothing to unselect
  		if (state.selectedNoteIds.length <= 1) return state; // Cannot unselect the last note
  
  		let newSelectedNoteIds = [];
  		for (let i = 0; i < newState.selectedNoteIds.length; i++) {
  			const id = newState.selectedNoteIds[i];
  			if (noteIds.indexOf(id) >= 0) continue;
  			newSelectedNoteIds.push(id);
  		}
  		newState.selectedNoteIds = newSelectedNoteIds;
  		newState.newNote = null;
  
  		return newState;
  	}
  
  	if (action.type === 'NOTE_SELECT_TOGGLE') {
  		if (!noteIds.length) return state;
  
  		if (newState.selectedNoteIds.indexOf(noteIds[0]) >= 0) {
  			newState = changeSelectedNotes(state, { type: 'NOTE_SELECT_REMOVE', id: noteIds[0] });
  		} else {
  			newState = changeSelectedNotes(state, { type: 'NOTE_SELECT_ADD', id: noteIds[0] });
  		}
  
  		newState.newNote = null;
  
  		return newState;
  	}
  
  	throw new Error('Unreachable');
  }
  
  function removeItemFromArray(array, property, value) {
  	for (let i = 0; i !== array.length; ++i) {
  		let currentItem = array[i];
  		if (currentItem[property] === value) {
  			array.splice(i, 1);
  			break;
  		}
  	}
  	return array;
  }
  
  const reducer = (state = defaultState, action) => {
  	let newState = state;
  
  	try {
  		switch (action.type) {
  
  			case 'NOTE_SELECT':
  			case 'NOTE_SELECT_ADD':
  			case 'NOTE_SELECT_REMOVE':
  			case 'NOTE_SELECT_TOGGLE':
  
  				newState = changeSelectedNotes(state, action);
  				break;
  
  			case 'NOTE_SELECT_EXTEND':
  
  				newState = Object.assign({}, state);
  
  				if (!newState.selectedNoteIds.length) {
  					newState.selectedNoteIds = [action.id];
  				} else {
  					const selectRangeId1 = state.selectedNoteIds[state.selectedNoteIds.length - 1];
  					const selectRangeId2 = action.id;
  					if (selectRangeId1 === selectRangeId2) return state;
  
  					let newSelectedNoteIds = state.selectedNoteIds.slice();
  					let selectionStarted = false;
  					for (let i = 0; i < state.notes.length; i++) {
  						const id = state.notes[i].id;
  
  						if (!selectionStarted && (id === selectRangeId1 || id === selectRangeId2)) {
  							selectionStarted = true;
  							if (newSelectedNoteIds.indexOf(id) < 0) newSelectedNoteIds.push(id);
  							continue;
  						} else if (selectionStarted && (id === selectRangeId1 || id === selectRangeId2)) {
  							if (newSelectedNoteIds.indexOf(id) < 0) newSelectedNoteIds.push(id);
  							break;
  						}
  
  						if (selectionStarted && newSelectedNoteIds.indexOf(id) < 0) {
  							newSelectedNoteIds.push(id);
  						}
  					}
  					newState.selectedNoteIds = newSelectedNoteIds;
  				}
  				break;
  
  			case 'FOLDER_SELECT':
  
  				newState = changeSelectedFolder(state, action);
  				break;
  
  			case 'FOLDER_AND_NOTE_SELECT':
  
  				newState = changeSelectedFolder(state, action);
  				const noteSelectAction = Object.assign({}, action, { type: 'NOTE_SELECT'});
  				newState = changeSelectedNotes(newState, noteSelectAction);
  				break;
  
  			case 'SETTING_UPDATE_ALL':
  
  				newState = Object.assign({}, state);
  				newState.settings = action.settings;
  				break;
  
  			case 'SETTING_UPDATE_ONE':
  
  				newState = Object.assign({}, state);
  				let newSettings = Object.assign({}, state.settings);
  				newSettings[action.key] = action.value;
  				newState.settings = newSettings;
  				break;
  
  			// Replace all the notes with the provided array
  			case 'NOTE_UPDATE_ALL':
  
  				newState = Object.assign({}, state);
  				newState.notes = action.notes;
  				newState.notesSource = action.notesSource;
  				break;
  
  			// Insert the note into the note list if it's new, or
  			// update it within the note array if it already exists.
  			case 'NOTE_UPDATE_ONE':
  
  				const modNote = action.note;
  
  				const noteIsInFolder = function(note, folderId) {
  					if (note.is_conflict) return folderId === Folder.conflictFolderId();
  					if (!('parent_id' in modNote) || note.parent_id == folderId) return true;
  					return false;
  				}
  
  				let noteFolderHasChanged = false;
  				let newNotes = state.notes.slice();
  				var found = false;
  				for (let i = 0; i < newNotes.length; i++) {
  					let n = newNotes[i];
  					if (n.id == modNote.id) {
  
  						// Note is still in the same folder
  						if (noteIsInFolder(modNote, n.parent_id)) {
  							// Merge the properties that have changed (in modNote) into
  							// the object we already have.
  							newNotes[i] = Object.assign({}, newNotes[i]);
  
  							for (let n in modNote) {
  								if (!modNote.hasOwnProperty(n)) continue;
  								newNotes[i][n] = modNote[n];
  							}
  
  						} else { // Note has moved to a different folder
  							newNotes.splice(i, 1);
  							noteFolderHasChanged = true;
  						}
  						found = true;
  						break;
  					}
  				}
  
  				// Note was not found - if the current folder is the same as the note folder,
  				// add it to it.
  				if (!found) {
  					if (noteIsInFolder(modNote, state.selectedFolderId)) {
  						newNotes.push(modNote);
  					}
  				}
  
  				//newNotes = Note.sortNotes(newNotes, state.notesOrder, newState.settings.uncompletedTodosOnTop);
  				newNotes = Note.sortNotes(newNotes, stateUtils.notesOrder(state.settings), newState.settings.uncompletedTodosOnTop);
  				newState = Object.assign({}, state);
  				newState.notes = newNotes;
  
  				if (noteFolderHasChanged) {
  					newState.selectedNoteIds = newNotes.length ? [newNotes[0].id] : [];
  				}
  				break;
  
  			case 'NOTE_DELETE':
  
  				newState = handleItemDelete(state, action);
  				break;
  
  			case 'TAG_DELETE':
  
  				newState = handleItemDelete(state, action);
  				newState.selectedNoteTags = removeItemFromArray(newState.selectedNoteTags.splice(0), 'id', action.id);
  				break;
  
  			case 'FOLDER_UPDATE_ALL':
  
  				newState = Object.assign({}, state);
  				newState.folders = action.items;
  				break;
  
  			case 'FOLDER_SET_COLLAPSED':
  
  				newState = folderSetCollapsed(state, action);
  				break;
  
  			case 'FOLDER_TOGGLE':
  
  				if (state.collapsedFolderIds.indexOf(action.id) >= 0) {
  					newState = folderSetCollapsed(state, Object.assign({ collapsed: false }, action));
  				} else {
  					newState = folderSetCollapsed(state, Object.assign({ collapsed: true }, action));
  				}
  				break;
  
  			case 'FOLDER_SET_COLLAPSED_ALL':
  
  				newState = Object.assign({}, state);
  				newState.collapsedFolderIds = action.ids.slice();
  				break;
  
  			case 'TAG_UPDATE_ALL':
  
  				newState = Object.assign({}, state);
  				newState.tags = action.items;
  				break;
  
  			case 'TAG_SELECT':
  
  				newState = Object.assign({}, state);
  				newState.selectedTagId = action.id;
  				if (!action.id) {
  					newState.notesParentType = defaultNotesParentType(state, 'Tag');
  				} else {
  					newState.notesParentType = 'Tag';
  				}
  				break;
  
  			case 'TAG_UPDATE_ONE':
  
  				newState = updateOneItem(state, action);
  				newState = updateOneItem(newState, action, 'selectedNoteTags');
  				break;
  
  			case 'NOTE_TAG_REMOVE':
  
  				newState = updateOneItem(state, action, 'tags');
  				let tagRemoved = action.item;
  				newState.selectedNoteTags = removeItemFromArray(newState.selectedNoteTags.splice(0), 'id', tagRemoved.id);;
  				break;
  
  			case 'FOLDER_UPDATE_ONE':
  			case 'MASTERKEY_UPDATE_ONE':
  
  				newState = updateOneItem(state, action);
  				break;
  
  			case 'FOLDER_DELETE':
  
  				newState = handleItemDelete(state, action);
  				break;
  
  			case 'MASTERKEY_UPDATE_ALL':
  
  				newState = Object.assign({}, state);
  				newState.masterKeys = action.items;
  				break;
  
  			case 'MASTERKEY_ADD_NOT_LOADED':
  
  				if (state.notLoadedMasterKeys.indexOf(action.id) < 0) {
  					newState = Object.assign({}, state);
  					const keys = newState.notLoadedMasterKeys.slice();
  					keys.push(action.id);
  					newState.notLoadedMasterKeys = keys;
  				}
  				break;
  
  			case 'MASTERKEY_REMOVE_NOT_LOADED':
  
  				const ids = action.id ? [action.id] : action.ids;
  				for (let i = 0; i < ids.length; i++) {
  					const id = ids[i];
  					const index = state.notLoadedMasterKeys.indexOf(id);
  					if (index >= 0) {
  						newState = Object.assign({}, state);
  						const keys = newState.notLoadedMasterKeys.slice();
  						keys.splice(index, 1);
  						newState.notLoadedMasterKeys = keys;
  					}
  				}
  				break;
  
  			case 'SYNC_STARTED':
  
  				newState = Object.assign({}, state);
  				newState.syncStarted = true;
  				break;
  
  			case 'SYNC_COMPLETED':
  
  				newState = Object.assign({}, state);
  				newState.syncStarted = false;
  				break;
  
  			case 'SYNC_REPORT_UPDATE':
  
  				newState = Object.assign({}, state);
  				newState.syncReport = action.report;
  				break;
  
  			case 'SEARCH_QUERY':
  
  				newState = Object.assign({}, state);
  				newState.searchQuery = action.query.trim();
  				break;
  
  			case 'SEARCH_ADD':
  
  				newState = Object.assign({}, state);
  				var searches = newState.searches.slice();
  				searches.push(action.search);
  				newState.searches = searches;
  				break;
  
  			case 'SEARCH_UPDATE':
  
  				newState = Object.assign({}, state);
  				var searches = newState.searches.slice();
  				var found = false;
  				for (let i = 0; i < searches.length; i++) {
  					if (searches[i].id === action.search.id) {
  						searches[i] = Object.assign({}, action.search);
  						found = true;
  						break;
  					}
  				}
  
  				if (!found) searches.push(action.search);
  
  				if (!action.search.query_pattern) {
  					newState.notesParentType = defaultNotesParentType(state, 'Search');
  				} else {
  					newState.notesParentType = 'Search';
  				}
  
  				newState.searches = searches;
  				break;
  
  			case 'SEARCH_DELETE':
  
  				newState = handleItemDelete(state, action);
  				break;
  
  			case 'SEARCH_SELECT':
  
  				newState = Object.assign({}, state);
  				newState.selectedSearchId = action.id;
  				if (!action.id) {
  					newState.notesParentType = defaultNotesParentType(state, 'Search');
  				} else {
  					newState.notesParentType = 'Search';
  				}
  				break;
  
  			case 'APP_STATE_SET':
  
  				newState = Object.assign({}, state);
  				newState.appState = action.state;
  				break;
  
  			case 'SYNC_HAS_DISABLED_SYNC_ITEMS':
  
  				newState = Object.assign({}, state);
  				newState.hasDisabledSyncItems = true;
  				break;
  
  			case 'NOTE_SET_NEW_ONE':
  
  				newState = Object.assign({}, state);
  				newState.newNote = action.item;
  				break;
  
  			case 'CLIPPER_SERVER_SET':
  
  				newState = Object.assign({}, state);
  				const clipperServer = Object.assign({}, newState.clipperServer);
  				if ('startState' in action) clipperServer.startState = action.startState;
  				if ('port' in action) clipperServer.port = action.port;
  				newState.clipperServer = clipperServer;
  				break;
  
  			case 'DECRYPTION_WORKER_SET':
  
  				newState = Object.assign({}, state);
  				const decryptionWorker = Object.assign({}, newState.decryptionWorker);
  				for (var n in action) {
  					if (!action.hasOwnProperty(n) || n === 'type') continue;
  					decryptionWorker[n] = action[n];
  				}
  				newState.decryptionWorker = decryptionWorker;
  				break;
  
  			case 'RESOURCE_FETCHER_SET':
  
  				newState = Object.assign({}, state);
  				const rf = Object.assign({}, action);
  				delete rf.type;
  				newState.resourceFetcher = rf;
  				break;
  
  			case 'LOAD_CUSTOM_CSS':
  
  				newState = Object.assign({}, state);
  				newState.customCss = action.css;
  				break;
          
  			case 'SET_NOTE_TAGS':
  				newState = Object.assign({}, state);
  				newState.selectedNoteTags = action.items;
  				break;
  
  		}
  	} catch (error) {
  		error.message = 'In reducer: ' + error.message + ' Action: ' + JSON.stringify(action);
  		throw error;
  	}
  
  	if (action.type.indexOf('NOTE_UPDATE') === 0 || action.type.indexOf('FOLDER_UPDATE') === 0 || action.type.indexOf('TAG_UPDATE') === 0) {
  		newState = Object.assign({}, newState);
  		newState.hasEncryptedItems = stateHasEncryptedItems(newState);
  	}
  
  	return newState;
  }
  
  module.exports = { reducer, defaultState, stateUtils };
  