/**
 * Copyright (c) 2014 Etherios, a Division of Digi International, Inc.
 * All rights not expressly granted are reserved.
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/.
 *
 * Etherios 11001 Bren Road East, Minnetonka, MN 55343
 * =======================================================================
 */
package com.etherios.connector.filesystem;

import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;

/**
 * Class to define a library.
 * 
 * <p>This class allows to print to the standard output the content of a 'book' 
 * located in the file system of the device.</p>
 **/
public class Library {

	/* Constants */

	// Book file names
	public static final String QUIJOTE_FILE_NAME = "quijote.txt";
	public static final String HAMLET_FILE_NAME = "hamlet.txt";

	// Book titles
	private static final String QUIJOTE_TITLE = "EL INGENIOSO HIDALGO DON QUIJOTE DE LA MANCHA";
	private static final String HAMLET_TITLE = "HAMLET, PRINCE OF DENMARK";

	/* Variables */

	private final HashMap<String, String> books = new HashMap<>(0);

	private ReadBookThread readThread = null;

	public Library() {
		books.put(QUIJOTE_FILE_NAME, QUIJOTE_TITLE);
		books.put(HAMLET_FILE_NAME, HAMLET_TITLE);
	}

	/**
	 * Prints to the standard output the file contents of the given path line 
	 * by line.
	 * 
	 * <p>It waits 5 seconds per line before continuing with the next line.</p>
	 * 
	 * @param path The path of the file to be printed out.
	 */
	public void printOutBook(String path) {
		// Retrieve the file name.
		String name = getBookFileName(path);
		if (name == null || name.length() == 0) {
			System.out.println("File '" + path + "' does not exist");
			return;
		}
		
		// If book included in library, read it.
		if (isBookInLibrary(name)) {
			if (readThread != null && readThread.isReading())
				readThread.stopReading();
			readThread = new ReadBookThread((String) books.get(name), path);
			readThread.start();
		} else
			System.out.println("Book '" + name + "' is not in the library");
	}

	/**
	 * Retrieves the name of the given file path.
	 *
	 * @param path Path to the file to retrieve its name, {@code null} if any
	 *             the file does not exist.
	 * @return File name.
	 */
	private String getBookFileName(String path) {
		if (path == null || path.length() == 0)
			return null;
		
		Path bookPath = Paths.get(path);
		if (bookPath == null 
				|| !Files.exists(bookPath.toAbsolutePath())
				|| !Files.isRegularFile(bookPath))
			return null;
		
		return bookPath.getFileName().toString();
	}

	/**
	 * Checks if the given book is included in the local library.
	 *
	 * @param name the name of the book to check.
	 * @return {@code true} if the given name is a book included in the local 
	 *         library, {@code false} otherwise.
	 */
	private boolean isBookInLibrary(String name) {
		if (name == null || name.length() == 0
				|| books == null || books.isEmpty())
			return false;
		
		return books.containsKey(name);
	}

	/**
	 * Reads the given book file.
	 * 
	 * @param filePath The absolute path of the file that contains the book.
	 * @return Byte array with the file contents or {@code null} if any error
	 *         occurs.
	 * @throws IOException if an I/O error occurs.
	 */
	private synchronized byte[] readBook(String filePath) throws IOException {
		Path bookPath = Paths.get(filePath);
		if (bookPath == null || !Files.exists(bookPath.toAbsolutePath())){
			System.out.println("Book '" + filePath + "' does not exist");
			return null;
		}
		
		// Get the size of the file.
		long length = Files.size(bookPath);
		// It's not possible to create an array using a long type.
		// It has to be an int type.
		// Before converting to an int type, check
		// to ensure that file is not larger than Integer.MAX_VALUE.
		if (length > Integer.MAX_VALUE) {
			// File is too large
			System.out.println("Book '" + bookPath.getFileName() 
					+ "' is too large");
			return null;
		}
		
		byte[] bytes = null;
		InputStream in = null;
		try {
			// Create the byte array to hold the data.
			bytes = new byte[(int) length];
			int offset = 0;
			int numRead = 0;
			
			// Read in the bytes.
			in = Files.newInputStream(bookPath);
			while (offset < bytes.length 
					&& (numRead = in.read(bytes, offset, bytes.length - offset)) >= 0)
				offset += numRead;
			
			// Ensure all the bytes have been read in.
			if (offset < bytes.length) {
				System.out.println("Could not completely read book " 
						+ bookPath.getFileName());
				bytes = null;
			}
		} finally {
			// Close the input stream.
			if (in != null)
				in.close();
		}
		return bytes;
	}

	/**
	 * Read Book Thread.
	 *
	 * <p>It reads the given text file and print out its content to the 
	 * standard output line by line.</p>
	 */
	private class ReadBookThread extends Thread {

		String title = null;
		String filePath = null;
		private boolean reading = false;

		/**
		 * ReadBookThread constructor.
		 * <p>Instances a new {@code ReadBookThread} object with the given 
		 * parameters.</p>
		 * 
		 * @param title The book title.
		 * @param filePath The path of the file that contains the book.
		 */
		public ReadBookThread(String title, String filePath) {
			this.title = title;
			this.filePath = filePath;
		}

		/**
		 * Stops reading the book.
		 */
		public void stopReading() {
			reading = false;
		}

		/**
		 * Returns {@code true} if the book is being read, {@code false} 
		 * otherwise.
		 * 
		 * @return {@code true} if the book is being read, {@code false} 
		 *         otherwise.
		 */
		public boolean isReading() {
			return reading;
		}

		@Override
		public void run() {
			reading = true;
			try {
				// Read the book file.
				byte[] byteContents = readBook(filePath);
				if (byteContents == null || byteContents.length == 0) {
					System.out.println("An error occur while trying to read "
							+ "book: '" + filePath + "'");
					return;
				}
				
				if (!isReading())
					return;
				
				// Display the book contents.
				displayBookContents(title, getText(byteContents));
			} catch (IOException e) {
				e.printStackTrace();
			} catch (InterruptedException e) {
				e.printStackTrace();
			} finally {
				stopReading();
			}
		}

		/**
		 * Returns a string with the decoded contents of the given byte array.
		 * 
		 * @param bytes The byte array.
		 * @return A string. 
		 */
		private String getText(byte[] bytes) {
			try {
				if (filePath.endsWith(HAMLET_FILE_NAME))
					return new String(bytes, "UTF-8");
				if (filePath.endsWith(QUIJOTE_FILE_NAME))
					return new String(bytes, "ISO-8859-1");
			} catch (UnsupportedEncodingException e) {
			}
			return new String(bytes);
		}

		/**
		 * Prints to the standard output the given text line by line. It waits a
		 * certain amount of time (5 seconds per 80 characters) before continuing
		 * with the next line.
		 *
		 * @param title The book title.
		 * @param text The complete text to be displayed.
		 * @throws InterruptedException if another thread has interrupted the
		 *         current thread. The <i>interrupted status</i> of the current 
		 *         thread is cleared when this exception is thrown.
		 */
		private synchronized void displayBookContents(String title, String text) 
				throws InterruptedException {
			wait(2000);
			// Display the title.
			System.out.println("\n--------------------------------------------------------------------------------");
			System.out.println("     Reading: " + title);
			System.out.println("--------------------------------------------------------------------------------\n");
			int lineLength = 0;
			// Start printing out the contents of the book line by line.
			for (int i = 0; i < text.length(); i++) {
				if (!isReading())
					return;
				System.out.print(text.charAt(i));
				lineLength++;
				long milliseconds = 0;
				switch (text.charAt(i)) {
					case '\n':
						milliseconds = lineLength * 5000 / 80; // 5 s / 80 chars
						if (milliseconds < 1000)
							milliseconds = 1000;
						lineLength = 0;
						break;
				}
				// Wait before printing the next line.
				if (milliseconds > 0)
					wait(milliseconds);
			}
		}
	}
}
