const readerComponent = () => {
	return {
		loading: true,
		mode: 'continuous', // Can be 'continuous', 'height' or 'width'
		msg: 'Loading the web reader. Please wait...',
		alertClass: 'uk-alert-primary',
		items: [],
		curItem: {},
		enableFlipAnimation: true,
		flipAnimation: null,
		longPages: false,
		lastSavedPage: page,
		selectedIndex: 0, // 0: not selected; 1: the first page
		margin: 30,
		preloadLookahead: 3,

		/**
		 * Initialize the component by fetching the page dimensions
		 */
		init(nextTick) {
			$.get(`${base_url}api/dimensions/${tid}/${eid}`)
				.then(data => {
					if (!data.success && data.error)
						throw new Error(resp.error);
					const dimensions = data.dimensions;

					this.items = dimensions.map((d, i) => {
						return {
							id: i + 1,
							url: `${base_url}api/page/${tid}/${eid}/${i+1}`,
							width: d.width,
							height: d.height,
						};
					});

					const avgRatio = this.items.reduce((acc, cur) => {
						return acc + cur.height / cur.width
					}, 0) / this.items.length;

					console.log(avgRatio);
					this.longPages = avgRatio > 2;
					this.loading = false;
					this.mode = localStorage.getItem('mode') || 'continuous';

					// Here we save a copy of this.mode, and use the copy as
					// 	the model-select value. This is because `updateMode`
					// 	might change this.mode and make it `height` or `width`,
					// 	which are not available in mode-select
					const mode = this.mode;
					this.updateMode(this.mode, page, nextTick);
					$('#mode-select').val(mode);

					const savedMargin = localStorage.getItem('margin');
					if (savedMargin) {
						this.margin = savedMargin;
					}

					// Preload Images
					this.preloadLookahead = +(localStorage.getItem('preloadLookahead') ?? 3);
					const limit = Math.min(page + this.preloadLookahead, this.items.length + 1);
					for (let idx = page + 1; idx <= limit; idx++) {
						this.preloadImage(this.items[idx - 1].url);
					}

					const savedFlipAnimation = localStorage.getItem('enableFlipAnimation');
					this.enableFlipAnimation = savedFlipAnimation === null || savedFlipAnimation === 'true';
				})
				.catch(e => {
					const errMsg = `Failed to get the page dimensions. ${e}`;
					console.error(e);
					this.alertClass = 'uk-alert-danger';
					this.msg = errMsg;
				})
		},
		/**
		 * Preload an image, which is expected to be cached
		 */
		preloadImage(url) {
			(new Image()).src = url;
		},
		/**
		 * Handles the `change` event for the page selector
		 */
		pageChanged() {
			const p = parseInt($('#page-select').val());
			this.toPage(p);
		},
		/**
		 * Handles the `change` event for the mode selector
		 *
		 * @param {function} nextTick - Alpine $nextTick magic property
		 */
		modeChanged(nextTick) {
			const mode = $('#mode-select').val();
			const curIdx = parseInt($('#page-select').val());

			this.updateMode(mode, curIdx, nextTick);
		},
		/**
		 * Handles the window `resize` event
		 */
		resized() {
			if (this.mode === 'continuous') return;

			const wideScreen = $(window).width() > $(window).height();
			this.mode = wideScreen ? 'height' : 'width';
		},
		/**
		 * Handles the window `keydown` event
		 *
		 * @param {Event} event - The triggering event
		 */
		keyHandler(event) {
			if (this.mode === 'continuous') return;

			if (event.key === 'ArrowLeft' || event.key === 'k')
				this.flipPage(false);
			if (event.key === 'ArrowRight' || event.key === 'j')
				this.flipPage(true);
		},
		/**
		 * Flips to the next or the previous page
		 *
		 * @param {bool} isNext - Whether we are going to the next page 
		 */
		flipPage(isNext) {
			const idx = parseInt(this.curItem.id);
			const newIdx = idx + (isNext ? 1 : -1);

			if (newIdx <= 0 || newIdx > this.items.length) return;

			if (newIdx + this.preloadLookahead < this.items.length + 1) {
				this.preloadImage(this.items[newIdx + this.preloadLookahead - 1].url);
			}

			this.toPage(newIdx);

			if (this.enableFlipAnimation) {
				if (isNext)
					this.flipAnimation = 'right';
				else
					this.flipAnimation = 'left';
			}

			setTimeout(() => {
				this.flipAnimation = null;
			}, 500);

			this.replaceHistory(newIdx);
		},
		/**
		 * Jumps to a specific page
		 *
		 * @param {number} idx - One-based index of the page
		 */
		toPage(idx) {
			if (this.mode === 'continuous') {
				$(`#${idx}`).get(0).scrollIntoView(true);
			} else {
				if (idx >= 1 && idx <= this.items.length) {
					this.curItem = this.items[idx - 1];
				}
			}
			this.replaceHistory(idx);
			UIkit.modal($('#modal-sections')).hide();
		},
		/**
		 * Replace the address bar history and save the reading progress if necessary 
		 *
		 * @param {number} idx - One-based index of the page
		 */
		replaceHistory(idx) {
			const ary = window.location.pathname.split('/');
			ary[ary.length - 1] = idx;
			ary.shift(); // remove leading `/`
			ary.unshift(window.location.origin);
			const url = ary.join('/');
			this.saveProgress(idx);
			history.replaceState(null, "", url);
		},
		/**
		 * Updates the backend reading progress if:
		 * 		1) the current page is more than five pages away from the last 
		 * 			saved page, or
		 * 		2) the average height/width ratio of the pages is over 2, or
		 * 		3) the current page is the first page, or
		 * 		4) the current page is the last page
		 *
		 * @param {number} idx - One-based index of the page
		 * @param {function} cb - Callback
		 */
		saveProgress(idx, cb) {
			idx = parseInt(idx);
			if (Math.abs(idx - this.lastSavedPage) >= 5 ||
				this.longPages ||
				idx === 1 || idx === this.items.length
			) {
				this.lastSavedPage = idx;
				console.log('saving progress', idx);

				const url = `${base_url}api/progress/${tid}/${idx}?${$.param({eid: eid})}`;
				$.ajax({
						method: 'PUT',
						url: url,
						dataType: 'json'
					})
					.done(data => {
						if (data.error)
							alert('danger', data.error);
						if (cb) cb();
					})
					.fail((jqXHR, status) => {
						alert('danger', `Error: [${jqXHR.status}] ${jqXHR.statusText}`);
					});
			}
		},
		/**
		 * Updates the reader mode
		 *
		 * @param {string} mode - Either `continuous` or `paged`
		 * @param {number} targetPage - The one-based index of the target page
		 * @param {function} nextTick - Alpine $nextTick magic property
		 */
		updateMode(mode, targetPage, nextTick) {
			localStorage.setItem('mode', mode);

			// The mode to be put into the `mode` prop. It can't be `screen`
			let propMode = mode;

			if (mode === 'paged') {
				const wideScreen = $(window).width() > $(window).height();
				propMode = wideScreen ? 'height' : 'width';
			}

			this.mode = propMode;

			if (mode === 'continuous') {
				nextTick(() => {
					this.setupScroller();
				});
			}

			nextTick(() => {
				this.toPage(targetPage);
			});
		},
		/**
		 * Shows the control modal
		 *
		 * @param {Event} event - The triggering event
		 */
		showControl(event) {
			const idx = event.currentTarget.id;
			this.selectedIndex = idx;
			UIkit.modal($('#modal-sections')).show();
		},
		/**
		 * Redirects to a URL
		 *
		 * @param {string} url - The target URL
		 */
		redirect(url) {
			window.location.replace(url);
		},
		/**
		 * Set up the scroll handler that calls `replaceHistory` when an image 
		 * 	enters the view port
		 */
		setupScroller() {
			if (this.mode !== 'continuous') return;
			$('img').each((idx, el) => {
				$(el).on('inview', (event, inView) => {
					if (inView) {
						const current = $(event.currentTarget).attr('id');

						this.curItem = this.items[current - 1];
						this.replaceHistory(current);
					}
				});
			});
		},
		/**
		 * Marks progress as 100% and jumps to the next entry
		 *
		 * @param {string} nextUrl - URL of the next entry
		 */
		nextEntry(nextUrl) {
			this.saveProgress(this.items.length, () => {
				this.redirect(nextUrl);
			});
		},
		/**
		 * Exits the reader, and sets the reading progress tp 100%
		 *
		 * @param {string} exitUrl - The Exit URL
		 */
		exitReader(exitUrl) {
			this.saveProgress(this.items.length, () => {
				this.redirect(exitUrl);
			});
		},

		/**
		 * Handles the `change` event for the entry selector
		 */
		entryChanged() {
			const id = $('#entry-select').val();
			this.redirect(`${base_url}reader/${tid}/${id}`);
		},

		marginChanged() {
			localStorage.setItem('margin', this.margin);
			this.toPage(this.selectedIndex);
		},

		preloadLookaheadChanged() {
			localStorage.setItem('preloadLookahead', this.preloadLookahead);
		},

		enableFlipAnimationChanged() {
			localStorage.setItem('enableFlipAnimation', this.enableFlipAnimation);
		},
	};
}
