const Note = require('lib/models/Note.js');
const Folder = require('lib/models/Folder.js');
const ArrayUtils = require('lib/ArrayUtils.js');
const { ALL_NOTES_FILTER_ID } = require('lib/reserved-ids');

const defaultState = {
	notes: [],
	notesSource: '',
	notesParentType: null,
	folders: [],
	tags: [],
	masterKeys: [],
	notLoadedMasterKeys: [],
	searches: [],
	selectedNoteIds: [],
	selectedNoteHash: '',
	selectedFolderId: null,
	selectedTagId: null,
	selectedSearchId: null,
	selectedItemType: 'note',
	lastSelectedNotesIds: {
		Folder: {},
		Tag: {},
		Search: {},
	},
	showSideMenu: false,
	screens: {},
	historyCanGoBack: false,
	syncStarted: false,
	syncReport: {},
	searchQuery: '',
	settings: {},
	sharedData: null,
	appState: 'starting',
	hasDisabledSyncItems: false,
	hasDisabledEncryptionItems: false,
	customCss: '',
	templates: [],
	collapsedFolderIds: [],
	clipperServer: {
		startState: 'idle',
		port: null,
	},
	decryptionWorker: {
		state: 'idle',
		itemIndex: 0,
		itemCount: 0,
		decryptedItemCounts: {},
		decryptedItemCount: 0,
		skippedItemCount: 0,
	},
	selectedNoteTags: [],
	resourceFetcher: {
		toFetchCount: 0,
	},
	backwardHistoryNotes: [],
	forwardHistoryNotes: [],
	plugins: {},
	provisionalNoteIds: [],
	editorNoteStatuses: {},
};

const MAX_HISTORY = 200;

const stateUtils = {};

const derivedStateCache_ = {};

// Allows, for a given state, to return the same derived
// objects, to prevent unecessary updates on calling components.
const cacheEnabledOutput = (key, output) => {
	key = `${key}_${JSON.stringify(output)}`;
	if (derivedStateCache_[key]) return derivedStateCache_[key];

	derivedStateCache_[key] = output;
	return derivedStateCache_[key];
};

stateUtils.notesOrder = function(stateSettings) {
	return cacheEnabledOutput('notesOrder', [
		{
			by: stateSettings['notes.sortOrder.field'],
			dir: stateSettings['notes.sortOrder.reverse'] ? 'DESC' : 'ASC',
		},
	]);
};

stateUtils.foldersOrder = function(stateSettings) {
	return cacheEnabledOutput('foldersOrder', [
		{
			by: stateSettings['folders.sortOrder.field'],
			dir: stateSettings['folders.sortOrder.reverse'] ? 'DESC' : 'ASC',
		},
	]);
};

stateUtils.hasNotesBeingSaved = function(state) {
	for (const id in state.editorNoteStatuses) {
		if (state.editorNoteStatuses[id] === 'saving') return true;
	}
	return false;
};

stateUtils.parentItem = function(state) {
	const t = state.notesParentType;
	let id = null;
	if (t === 'Folder') id = state.selectedFolderId;
	if (t === 'Tag') id = state.selectedTagId;
	if (t === 'Search') id = state.selectedSearchId;
	if (!t || !id) return null;
	return { type: t, id: id };
};

stateUtils.lastSelectedNoteIds = function(state) {
	const parent = stateUtils.parentItem(state);
	if (!parent) return [];
	const output = state.lastSelectedNotesIds[parent.type][parent.id];
	return output ? output : [];
};

stateUtils.getCurrentNote = function(state) {
	const selectedNoteIds = state.selectedNoteIds;
	const notes = state.notes;
	if (selectedNoteIds != null && selectedNoteIds.length > 0) {
		const currNote = notes.find(note => note.id === selectedNoteIds[0]);
		if (currNote != null) {
			return {
				id: currNote.id,
				parent_id: currNote.parent_id,
				notesParentType: state.notesParentType,
				selectedFolderId: state.selectedFolderId,
				selectedTagId: state.selectedTagId,
				selectedSearchId: state.selectedSearchId,
				searches: state.searches,
				selectedSmartFilterId: state.selectedSmartFilterId,
			};
		}
	}
	return null;
};

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);
	}

	const newState = Object.assign({}, state);
	newState.collapsedFolderIds = collapsedFolderIds;
	return newState;
}

function removeAdjacentDuplicates(items) {
	return items.filter((item, idx) => (idx >= 1) ? items[idx - 1].id !== item.id : true);
}

// When deleting a note, tag or folder
function handleItemDelete(state, action) {
	const map = {
		FOLDER_DELETE: ['folders', 'selectedFolderId', true],
		NOTE_DELETE: ['notes', 'selectedNoteIds', false],
		TAG_DELETE: ['tags', 'selectedTagId', true],
		SEARCH_DELETE: ['searches', 'selectedSearchId', true],
	};

	const listKey = map[action.type][0];
	const selectedItemKey = map[action.type][1];
	const isSingular = map[action.type][2];

	const selectedItemKeys = isSingular ? [state[selectedItemKey]] : state[selectedItemKey];
	const isSelected = selectedItemKeys.includes(action.id);

	const items = state[listKey];
	const newItems = [];
	let newSelectedIndexes = [];

	for (let i = 0; i < items.length; i++) {
		const item = items[i];
		if (isSelected) {
			// the selected item is deleted so select the following item
			// if multiple items are selected then just use the first one
			if (selectedItemKeys[0] == item.id) {
				newSelectedIndexes.push(newItems.length);
			}
		} else {
			// the selected item/s is not deleted so keep it selected
			if (selectedItemKeys.includes(item.id)) {
				newSelectedIndexes.push(newItems.length);
			}
		}
		if (item.id == action.id) {
			continue;
		}
		newItems.push(item);
	}

	if (newItems.length == 0) {
		newSelectedIndexes = []; // no remaining items so no selection

	}  else if (newSelectedIndexes.length == 0) {
		newSelectedIndexes.push(0); // no selection exists so select the top

	} else {
		// when the items at end of list are deleted then select the end
		for (let i = 0; i < newSelectedIndexes.length; i++) {
			if (newSelectedIndexes[i] >= newItems.length) {
				newSelectedIndexes = [newItems.length - 1];
				break;
			}
		}
	}

	const newState = Object.assign({}, state);
	newState[listKey] = newItems;

	const newIds = [];
	for (let i = 0; i < newSelectedIndexes.length; i++) {
		newIds.push(newItems[newSelectedIndexes[i]].id);
	}
	newState[selectedItemKey] = isSingular ? newIds[0] : newIds;

	if ((newIds.length == 0) && 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';
	}

	const newItems = state[itemsKey].splice(0);
	const item = action.item;

	let found = false;
	for (let i = 0; i < newItems.length; i++) {
		const n = newItems[i];
		if (n.id == item.id) {
			newItems[i] = Object.assign(newItems[i], item);
			found = true;
			break;
		}
	}

	if (!found) newItems.push(item);

	const 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, options = null) {
	if (!options) options = {};
	const 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';
	}

	if (newState.selectedFolderId === state.selectedFolderId && newState.notesParentType === state.notesParentType) return state;

	if (options.clearSelectedNoteIds) newState.selectedNoteIds = [];

	return newState;
}

function recordLastSelectedNoteIds(state, noteIds) {
	const newOnes = Object.assign({}, state.lastSelectedNotesIds);
	const parent = stateUtils.parentItem(state);
	if (!parent) return state;

	newOnes[parent.type][parent.id] = noteIds.slice();

	return Object.assign({}, state, {
		lastSelectedNotesIds: newOnes,
	});
}

function changeSelectedNotes(state, action, options = null) {
	if (!options) options = {};

	let noteIds = [];
	if (action.id) noteIds = [action.id];
	if (action.ids) noteIds = action.ids;
	if (action.noteId) noteIds = [action.noteId];

	let newState = Object.assign({}, state);

	if (action.type === 'NOTE_SELECT') {
		if (JSON.stringify(newState.selectedNoteIds) === JSON.stringify(noteIds)) return state;
		newState.selectedNoteIds = noteIds;
		newState.selectedNoteHash = action.hash ? action.hash : '';
	} else if (action.type === 'NOTE_SELECT_ADD') {
		if (!noteIds.length) return state;
		newState.selectedNoteIds = ArrayUtils.unique(newState.selectedNoteIds.concat(noteIds));
	} else 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

		const 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;
	} else 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] });
		}
	} else {
		throw new Error('Unreachable');
	}

	newState = recordLastSelectedNoteIds(newState, newState.selectedNoteIds);

	return newState;
}

function removeItemFromArray(array, property, value) {
	for (let i = 0; i !== array.length; ++i) {
		const currentItem = array[i];
		if (currentItem[property] === value) {
			array.splice(i, 1);
			break;
		}
	}
	return array;
}

const getContextFromHistory = (ctx) => {
	const result = {};
	result.notesParentType = ctx.notesParentType;
	if (result.notesParentType === 'Folder') {
		result.selectedFolderId = ctx.selectedFolderId;
	} else if (result.notesParentType === 'Tag') {
		result.selectedTagId = ctx.selectedTagId;
	} else if (result.notesParentType === 'Search') {
		result.selectedSearchId = ctx.selectedSearchId;
		result.searches = ctx.searches;
	} else if (result.notesParentType === 'SmartFilter') {
		result.selectedSmartFilterId = ctx.selectedSmartFilterId;
	}
	return result;
};

function handleHistory(state, action) {
	let newState = Object.assign({}, state);
	let backwardHistoryNotes = newState.backwardHistoryNotes.slice();
	let forwardHistoryNotes = newState.forwardHistoryNotes.slice();
	const currentNote = stateUtils.getCurrentNote(state);
	switch (action.type) {
	case 'HISTORY_BACKWARD': {
		const note = backwardHistoryNotes[backwardHistoryNotes.length - 1];
		if (currentNote != null && (forwardHistoryNotes.length === 0 || currentNote.id != forwardHistoryNotes[forwardHistoryNotes.length - 1].id)) {
			forwardHistoryNotes = forwardHistoryNotes.concat(currentNote).slice(-MAX_HISTORY);
		}

		newState = changeSelectedFolder(newState, Object.assign({}, action, { type: 'FOLDER_SELECT', folderId: note.parent_id }));
		newState = changeSelectedNotes(newState, Object.assign({}, action, { type: 'NOTE_SELECT', noteId: note.id }));

		const ctx = backwardHistoryNotes[backwardHistoryNotes.length - 1];
		newState = Object.assign(newState, getContextFromHistory(ctx));

		backwardHistoryNotes.pop();
		break;
	}
	case 'HISTORY_FORWARD': {
		const note = forwardHistoryNotes[forwardHistoryNotes.length - 1];

		if (currentNote != null && (backwardHistoryNotes.length === 0 || currentNote.id != backwardHistoryNotes[backwardHistoryNotes.length - 1].id)) {
			backwardHistoryNotes = backwardHistoryNotes.concat(currentNote).slice(-MAX_HISTORY);
		}

		newState = changeSelectedFolder(newState, Object.assign({}, action, { type: 'FOLDER_SELECT', folderId: note.parent_id }));
		newState = changeSelectedNotes(newState, Object.assign({}, action, { type: 'NOTE_SELECT', noteId: note.id }));

		const ctx = forwardHistoryNotes[forwardHistoryNotes.length - 1];
		newState = Object.assign(newState, getContextFromHistory(ctx));


		forwardHistoryNotes.pop();
		break;
	}
	case 'NOTE_SELECT':
		if (currentNote != null &&  action.id != currentNote.id) {
			forwardHistoryNotes = [];
			backwardHistoryNotes = backwardHistoryNotes.concat(currentNote).slice(-MAX_HISTORY);
		}
		// History should be free from duplicates.
		if (backwardHistoryNotes != null && backwardHistoryNotes.length > 0 &&
						action.id === backwardHistoryNotes[backwardHistoryNotes.length - 1].id) {
			backwardHistoryNotes.pop();
		}
		break;
	case 'TAG_SELECT':
	case 'FOLDER_AND_NOTE_SELECT':
	case 'FOLDER_SELECT':
		if (currentNote != null) {
			forwardHistoryNotes = [];
			backwardHistoryNotes = backwardHistoryNotes.concat(currentNote).slice(-MAX_HISTORY);
		}
		break;
	case 'NOTE_UPDATE_ONE': {
		const modNote = action.note;

		backwardHistoryNotes = backwardHistoryNotes.map(note => {
			if (note.id === modNote.id) {
				return Object.assign(note, { parent_id: modNote.parent_id, selectedFolderId: modNote.parent_id });
			}
			return note;
		});

		forwardHistoryNotes = forwardHistoryNotes.map(note => {
			if (note.id === modNote.id) {
				return Object.assign(note, { parent_id: modNote.parent_id, selectedFolderId: modNote.parent_id });
			}
			return note;
		});

		break;
	}
	case 'SEARCH_UPDATE':
		if (currentNote != null && (backwardHistoryNotes.length === 0 ||
						backwardHistoryNotes[backwardHistoryNotes.length - 1].id != currentNote.id)) {
			forwardHistoryNotes = [];
			backwardHistoryNotes = backwardHistoryNotes.concat(currentNote).slice(-MAX_HISTORY);
		}
		break;
	case 'FOLDER_DELETE':
		backwardHistoryNotes = backwardHistoryNotes.filter(note => note.parent_id != action.id);
		forwardHistoryNotes = forwardHistoryNotes.filter(note => note.parent_id != action.id);

		backwardHistoryNotes = removeAdjacentDuplicates(backwardHistoryNotes);
		forwardHistoryNotes = removeAdjacentDuplicates(forwardHistoryNotes);
		break;
	case 'NOTE_DELETE': {
		backwardHistoryNotes = backwardHistoryNotes.filter(note => note.id != action.id);
		forwardHistoryNotes = forwardHistoryNotes.filter(note => note.id != action.id);

		backwardHistoryNotes = removeAdjacentDuplicates(backwardHistoryNotes);
		forwardHistoryNotes = removeAdjacentDuplicates(forwardHistoryNotes);

		// Fix the case where after deletion the currently selected note is also the latest in history
		const selectedNoteIds = newState.selectedNoteIds;
		if (selectedNoteIds.length && backwardHistoryNotes.length && backwardHistoryNotes[backwardHistoryNotes.length - 1].id === selectedNoteIds[0]) {
			backwardHistoryNotes = backwardHistoryNotes.slice(0, backwardHistoryNotes.length - 1);
		}
		if (selectedNoteIds.length && forwardHistoryNotes.length && forwardHistoryNotes[forwardHistoryNotes.length - 1].id === selectedNoteIds[0]) {
			forwardHistoryNotes = forwardHistoryNotes.slice(0, forwardHistoryNotes.length - 1);
		}
		break;
	}
	default:
		// console.log('Unknown action in history reducer.' ,action.type);
		return state;
	}

	newState.backwardHistoryNotes = backwardHistoryNotes;
	newState.forwardHistoryNotes = forwardHistoryNotes;
	return newState;
}

const reducer = (state = defaultState, action) => {
	// if (!['SIDE_MENU_OPEN_PERCENT'].includes(action.type)) console.info('Action', action.type);

	let newState = state;

	// NOTE_DELETE requires post processing
	if (action.type !== 'NOTE_DELETE') {
		newState = handleHistory(newState, action);
	}

	try {
		switch (action.type) {

		case 'NOTE_SELECT':
		case 'NOTE_SELECT_ADD':
		case 'NOTE_SELECT_REMOVE':
		case 'NOTE_SELECT_TOGGLE':
			newState = changeSelectedNotes(newState, 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;

					const 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 'NOTE_SELECT_ALL':
			newState = Object.assign({}, state);
			newState.selectedNoteIds = newState.notes.map(n => n.id);
			break;

		case 'NOTE_SELECT_ALL_TOGGLE': {
			newState = Object.assign({}, state);
			const allSelected = state.notes.every(n => state.selectedNoteIds.includes(n.id));
			if (allSelected) {
				newState.selectedNoteIds = [];
			} else {
				newState.selectedNoteIds = newState.notes.map(n => n.id);
			}
			break;
		}

		case 'SMART_FILTER_SELECT':
			newState = Object.assign({}, state);
			newState.notesParentType = 'SmartFilter';
			newState.selectedSmartFilterId = action.id;
			break;

		case 'FOLDER_SELECT':
			newState = changeSelectedFolder(newState, action, { clearSelectedNoteIds: true });
			break;

		case 'FOLDER_AND_NOTE_SELECT':
			{
				newState = changeSelectedFolder(newState, 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);
				const newSettings = Object.assign({}, state.settings);
				newSettings[action.key] = action.value;
				newState.settings = newSettings;
			}
			break;

		case 'NOTE_PROVISIONAL_FLAG_CLEAR':
			{
				const newIds = ArrayUtils.removeElement(state.provisionalNoteIds, action.id);
				if (newIds !== state.provisionalNoteIds) {
					newState = Object.assign({}, state, { provisionalNoteIds: newIds });
				}
			}
			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 isViewingAllNotes = (state.notesParentType === 'SmartFilter' && state.selectedSmartFilterId === ALL_NOTES_FILTER_ID);
				const isViewingConflictFolder = state.notesParentType === 'Folder' && state.selectedFolderId === Folder.conflictFolderId();

				const noteIsInFolder = function(note, folderId) {
					if (note.is_conflict && isViewingConflictFolder) return true;
					if (!('parent_id' in modNote) || note.parent_id == folderId) return true;
					return false;
				};

				let movedNotePreviousIndex = 0;
				let noteFolderHasChanged = false;
				let newNotes = state.notes.slice();
				let found = false;
				for (let i = 0; i < newNotes.length; i++) {
					const n = newNotes[i];
					if (n.id == modNote.id) {
						// Note is still in the same folder
						if (isViewingAllNotes || 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 (const 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;
							movedNotePreviousIndex = i;
						}
						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 (isViewingAllNotes || 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) {
					let newIndex = movedNotePreviousIndex;
					if (newIndex >= newNotes.length) newIndex = newNotes.length - 1;
					if (!newNotes.length) newIndex = -1;
					newState.selectedNoteIds = newIndex >= 0 ? [newNotes[newIndex].id] : [];
				}


				if (action.provisional) {
					newState.provisionalNoteIds.push(modNote.id);
				} else {
					const idx = newState.provisionalNoteIds.indexOf(modNote.id);
					if (idx >= 0) {
						const t = newState.provisionalNoteIds.slice();
						t.splice(idx, 1);
						newState.provisionalNoteIds = t;
					}
				}
			}
			break;

		case 'NOTE_DELETE':

			{
				newState = handleItemDelete(state, action);

				const idx = newState.provisionalNoteIds.indexOf(action.id);
				if (idx >= 0) {
					const t = newState.provisionalNoteIds.slice();
					t.splice(idx, 1);
					newState.provisionalNoteIds = t;
				}
			}
			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.selectedTagId = action.id;
			if (!action.id) {
				newState.notesParentType = defaultNotesParentType(state, 'Tag');
			} else {
				newState.notesParentType = 'Tag';
			}
			newState.selectedNoteIds = [];
			break;

		case 'TAG_UPDATE_ONE':
			{
				// We only want to update the selected note tags if the tag belongs to the currently open note
				const selectedNoteHasTag = !!state.selectedNoteTags.find(tag => tag.id === action.item.id);
				newState = updateOneItem(state, action);
				if (selectedNoteHasTag) newState = updateOneItem(newState, action, 'selectedNoteTags');
			}
			break;

		case 'NOTE_TAG_REMOVE':
			{
				newState = updateOneItem(state, action, 'tags');
				const tagRemoved = action.item;
				newState.selectedNoteTags = removeItemFromArray(newState.selectedNoteTags.splice(0), 'id', tagRemoved.id);
			}
			break;

		case 'EDITOR_NOTE_STATUS_SET':

			{
				const newStatuses = Object.assign({}, state.editorNoteStatuses);
				newStatuses[action.id] = action.status;
				newState = Object.assign({}, state, { editorNoteStatuses: newStatuses });
			}
			break;

		case 'EDITOR_NOTE_STATUS_REMOVE':

			{
				const newStatuses = Object.assign({}, state.editorNoteStatuses);
				delete newStatuses[action.id];
				newState = Object.assign({}, state, { editorNoteStatuses: newStatuses });
			}
			break;

		case 'FOLDER_UPDATE_ONE':
		case 'MASTERKEY_UPDATE_ONE':
			newState = updateOneItem(state, action);
			break;

		case 'FOLDER_DELETE':
			newState = handleItemDelete(newState, action);
			break;

		case 'MASTERKEY_UPDATE_ALL':
			newState = Object.assign({}, state);
			newState.masterKeys = action.items;
			break;

		case 'MASTERKEY_SET_NOT_LOADED':
			newState = Object.assign({}, state);
			newState.notLoadedMasterKeys = action.ids;
			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);
				const searches = newState.searches.slice();
				searches.push(action.search);
				newState.searches = searches;
			}
			break;

		case 'SEARCH_UPDATE':
			{
				const searches = newState.searches.slice();
				let 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';
			}
			newState.selectedNoteIds = [];
			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 'ENCRYPTION_HAS_DISABLED_ITEMS':
			newState = Object.assign({}, state);
			newState.hasDisabledEncryptionItems = action.value;
			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 (const 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 'TEMPLATE_UPDATE_ALL':
			newState = Object.assign({}, state);
			newState.templates = action.templates;
			break;

		case 'SET_NOTE_TAGS':
			newState = Object.assign({}, state);
			newState.selectedNoteTags = action.items;
			break;

		case 'PLUGIN_DIALOG_SET':
			{
				if (!action.pluginName) throw new Error('action.pluginName not specified');
				newState = Object.assign({}, state);
				const newPlugins = Object.assign({}, newState.plugins);
				const newPlugin = newState.plugins[action.pluginName] ? Object.assign({}, newState.plugins[action.pluginName]) : {};
				if ('open' in action) newPlugin.dialogOpen = action.open;
				newPlugins[action.pluginName] = newPlugin;
				newState.plugins = newPlugins;
			}
			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);
	}

	if (action.type === 'NOTE_DELETE') {
		newState = handleHistory(newState, action);
	}

	return newState;
};

module.exports = { reducer, defaultState, stateUtils, MAX_HISTORY };