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,
	collapsedFolderIds: [],
	clipperServer: {
		startState: 'idle',
		port: null,
	},
	decryptionWorker: {
		state: 'idle',
		itemIndex: 0,
		itemCount: 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) {
	let itemsKey = null;
	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 changeSelectedNotes(state, action) {
	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');
}

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 = Object.assign({}, state);
				newState.selectedFolderId = action.id;
				if (!action.id) {
					newState.notesParentType = defaultNotesParentType(state, 'Folder');
				} else {
					newState.notesParentType = 'Folder';
				}
				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);
				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':
			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;

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