/*
 * Copyright 2014 Open Networking Laboratory
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.opendaylight.atrium.util;

import static java.nio.file.Files.delete;
import static java.nio.file.Files.walkFileTree;
import static org.opendaylight.atrium.util.AtriumGroupedThreadFactory.groupedThreadFactory;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Dictionary;
import java.util.List;
import java.util.Random;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Strings;
import com.google.common.primitives.UnsignedLongs;
import com.google.common.util.concurrent.ThreadFactoryBuilder;

/**
 * Miscellaneous utility methods.
 */
public abstract class AtriumTools {

	private AtriumTools() {
	}

	private static final Logger log = LoggerFactory.getLogger(AtriumTools.class);

	private static Random random = new Random();

	/**
	 * Returns a thread factory that produces threads named according to the
	 * supplied name pattern.
	 *
	 * @param pattern
	 *            name pattern
	 * @return thread factory
	 */
	public static ThreadFactory namedThreads(String pattern) {
		return new ThreadFactoryBuilder().setNameFormat(pattern)
				.setUncaughtExceptionHandler((t, e) -> log.error("Uncaught exception on " + t.getName(), e)).build();
	}

	/**
	 * Returns a thread factory that produces threads named according to the
	 * supplied name pattern and from the specified thread-group. The thread
	 * group name is expected to be specified in slash-delimited format, The
	 * thread names will be produced by converting the thread group name into
	 * dash-delimited format and pre-pended to the specified pattern.
	 *
	 * @param groupName
	 *            group name in slash-delimited format to indicate hierarchy
	 * @param pattern
	 *            name pattern
	 * @return thread factory
	 */
	public static ThreadFactory groupedThreads(String groupName, String pattern) {
		return new ThreadFactoryBuilder().setThreadFactory(groupedThreadFactory(groupName))
				.setNameFormat(groupName.replace(AtriumGroupedThreadFactory.DELIMITER, "-") + "-" + pattern)
				.setUncaughtExceptionHandler((t, e) -> log.error("Uncaught exception on " + t.getName(), e)).build();
	}

	/**
	 * Returns a thread factory that produces threads with MIN_PRIORITY.
	 *
	 * @param factory
	 *            backing ThreadFactory
	 * @return thread factory
	 */
	public static ThreadFactory minPriority(ThreadFactory factory) {
		return new ThreadFactoryBuilder().setThreadFactory(factory).setPriority(Thread.MIN_PRIORITY).build();
	}

	/**
	 * Returns true if the collection is null or is empty.
	 *
	 * @param collection
	 *            collection to test
	 * @return true if null or empty; false otherwise
	 */
	public static boolean isNullOrEmpty(Collection collection) {
		return collection == null || collection.isEmpty();
	}

	/**
	 * Returns the specified item if that items is null; otherwise throws not
	 * found exception.
	 *
	 * @param item
	 *            item to check
	 * @param message
	 *            not found message
	 * @param <T>
	 *            item type
	 * @return item if not null
	 * @throws org.opendaylight.atrium.util.AtriumItemNotFoundException
	 *             if item is null
	 */
	public static <T> T nullIsNotFound(T item, String message) {
		if (item == null) {
			throw new AtriumItemNotFoundException(message);
		}
		return item;
	}

	/**
	 * Converts a string from hex to long.
	 *
	 * @param string
	 *            hex number in string form; sans 0x
	 * @return long value
	 */
	public static long fromHex(String string) {
		return UnsignedLongs.parseUnsignedLong(string, 16);
	}

	/**
	 * Converts a long value to hex string; 16 wide and sans 0x.
	 *
	 * @param value
	 *            long value
	 * @return hex string
	 */
	public static String toHex(long value) {
		return Strings.padStart(UnsignedLongs.toString(value, 16), 16, '0');
	}

	/**
	 * Converts a long value to hex string; 16 wide and sans 0x.
	 *
	 * @param value
	 *            long value
	 * @param width
	 *            string width; zero padded
	 * @return hex string
	 */
	public static String toHex(long value, int width) {
		return Strings.padStart(UnsignedLongs.toString(value, 16), width, '0');
	}

	/**
	 * Get property as a string value.
	 *
	 * @param properties
	 *            properties to be looked up
	 * @param propertyName
	 *            the name of the property to look up
	 * @return value when the propertyName is defined or return null
	 */
	public static String get(Dictionary<?, ?> properties, String propertyName) {
		Object v = properties.get(propertyName);
		String s = (v instanceof String) ? (String) v : v != null ? v.toString() : null;
		return Strings.isNullOrEmpty(s) ? null : s.trim();
	}

	/**
	 * Suspends the current thread for a specified number of millis.
	 *
	 * @param ms
	 *            number of millis
	 */
	public static void delay(int ms) {
		try {
			Thread.sleep(ms);
		} catch (InterruptedException e) {
			throw new RuntimeException("Interrupted", e);
		}
	}

	/**
	 * Suspends the current thread for a random number of millis between 0 and
	 * the indicated limit.
	 *
	 * @param ms
	 *            max number of millis
	 */
	public static void randomDelay(int ms) {
		try {
			Thread.sleep(random.nextInt(ms));
		} catch (InterruptedException e) {
			throw new RuntimeException("Interrupted", e);
		}
	}

	/**
	 * Suspends the current thread for a specified number of millis and nanos.
	 *
	 * @param ms
	 *            number of millis
	 * @param nanos
	 *            number of nanos
	 */
	public static void delay(int ms, int nanos) {
		try {
			Thread.sleep(ms, nanos);
		} catch (InterruptedException e) {
			throw new RuntimeException("Interrupted", e);
		}
	}

	/**
	 * Slurps the contents of a file into a list of strings, one per line.
	 *
	 * @param path
	 *            file path
	 * @return file contents
	 */
	public static List<String> slurp(File path) {
		try {
			BufferedReader br = new BufferedReader(
					new InputStreamReader(new FileInputStream(path), StandardCharsets.UTF_8));

			List<String> lines = new ArrayList<>();
			String line;
			while ((line = br.readLine()) != null) {
				lines.add(line);
			}
			return lines;

		} catch (IOException e) {
			return null;
		}
	}

	/**
	 * Purges the specified directory path.&nbsp;Use with great caution since no
	 * attempt is made to check for symbolic links, which could result in
	 * deletion of unintended files.
	 *
	 * @param path
	 *            directory to be removed
	 * @throws java.io.IOException
	 *             if unable to remove contents
	 */
	public static void removeDirectory(String path) throws IOException {
		DirectoryDeleter visitor = new DirectoryDeleter();
		File dir = new File(path);
		if (dir.exists() && dir.isDirectory()) {
			walkFileTree(Paths.get(path), visitor);
			if (visitor.exception != null) {
				throw visitor.exception;
			}
		}
	}

	/**
	 * Purges the specified directory path.&nbsp;Use with great caution since no
	 * attempt is made to check for symbolic links, which could result in
	 * deletion of unintended files.
	 *
	 * @param dir
	 *            directory to be removed
	 * @throws java.io.IOException
	 *             if unable to remove contents
	 */
	public static void removeDirectory(File dir) throws IOException {
		DirectoryDeleter visitor = new DirectoryDeleter();
		if (dir.exists() && dir.isDirectory()) {
			walkFileTree(Paths.get(dir.getAbsolutePath()), visitor);
			if (visitor.exception != null) {
				throw visitor.exception;
			}
		}
	}

	// Auxiliary path visitor for recursive directory structure removal.
	private static class DirectoryDeleter extends SimpleFileVisitor<Path> {

		private IOException exception;

		@Override
		public FileVisitResult visitFile(Path file, BasicFileAttributes attributes) throws IOException {
			if (attributes.isRegularFile()) {
				delete(file);
			}
			return FileVisitResult.CONTINUE;
		}

		@Override
		public FileVisitResult postVisitDirectory(Path directory, IOException ioe) throws IOException {
			delete(directory);
			return FileVisitResult.CONTINUE;
		}

		@Override
		public FileVisitResult visitFileFailed(Path file, IOException ioe) throws IOException {
			this.exception = ioe;
			return FileVisitResult.TERMINATE;
		}
	}

	/**
	 * Returns a human friendly time ago string for a specified system time.
	 *
	 * @param unixTime
	 *            system time in millis
	 * @return human friendly time ago
	 */
	public static String timeAgo(long unixTime) {
		long deltaMillis = System.currentTimeMillis() - unixTime;
		long secondsSince = (long) (deltaMillis / 1000.0);
		long minsSince = (long) (deltaMillis / (1000.0 * 60));
		long hoursSince = (long) (deltaMillis / (1000.0 * 60 * 60));
		long daysSince = (long) (deltaMillis / (1000.0 * 60 * 60 * 24));
		if (daysSince > 0) {
			return String.format("%dd ago", daysSince);
		} else if (hoursSince > 0) {
			return String.format("%dh ago", hoursSince);
		} else if (minsSince > 0) {
			return String.format("%dm ago", minsSince);
		} else if (secondsSince > 0) {
			return String.format("%ds ago", secondsSince);
		} else {
			return "just now";
		}
	}

	/**
	 * Copies the specified directory path.&nbsp;Use with great caution since no
	 * attempt is made to check for symbolic links, which could result in copy
	 * of unintended files.
	 *
	 * @param src
	 *            directory to be copied
	 * @param dst
	 *            destination directory to be removed
	 * @throws java.io.IOException
	 *             if unable to remove contents
	 */
	public static void copyDirectory(String src, String dst) throws IOException {
		walkFileTree(Paths.get(src), new DirectoryCopier(src, dst));
	}

	/**
	 * Copies the specified directory path.&nbsp;Use with great caution since no
	 * attempt is made to check for symbolic links, which could result in copy
	 * of unintended files.
	 *
	 * @param src
	 *            directory to be copied
	 * @param dst
	 *            destination directory to be removed
	 * @throws java.io.IOException
	 *             if unable to remove contents
	 */
	public static void copyDirectory(File src, File dst) throws IOException {
		walkFileTree(Paths.get(src.getAbsolutePath()),
				new DirectoryCopier(src.getAbsolutePath(), dst.getAbsolutePath()));
	}

	/**
	 * Returns the future value when complete or if future completes
	 * exceptionally returns the defaultValue.
	 * 
	 * @param future
	 *            future
	 * @param defaultValue
	 *            default value
	 * @param <T>
	 *            future value type
	 * @return future value when complete or if future completes exceptionally
	 *         returns the defaultValue.
	 */
	public static <T> T futureGetOrElse(Future<T> future, T defaultValue) {
		try {
			return future.get();
		} catch (InterruptedException e) {
			Thread.currentThread().interrupt();
			return defaultValue;
		} catch (ExecutionException e) {
			return defaultValue;
		}
	}

	/**
	 * Returns the future value when complete or if future completes
	 * exceptionally returns the defaultValue.
	 * 
	 * @param future
	 *            future
	 * @param timeout
	 *            time to wait for successful completion
	 * @param timeUnit
	 *            time unit
	 * @param defaultValue
	 *            default value
	 * @param <T>
	 *            future value type
	 * @return future value when complete or if future completes exceptionally
	 *         returns the defaultValue.
	 */
	public static <T> T futureGetOrElse(Future<T> future, long timeout, TimeUnit timeUnit, T defaultValue) {
		try {
			return future.get(timeout, timeUnit);
		} catch (InterruptedException e) {
			Thread.currentThread().interrupt();
			return defaultValue;
		} catch (ExecutionException | TimeoutException e) {
			return defaultValue;
		}
	}

	/**
	 * Returns a future that is completed exceptionally.
	 * 
	 * @param t
	 *            exception
	 * @param <T>
	 *            future value type
	 * @return future
	 */
	public static <T> CompletableFuture<T> exceptionalFuture(Throwable t) {
		CompletableFuture<T> future = new CompletableFuture<>();
		future.completeExceptionally(t);
		return future;
	}

	/**
	 * Returns the contents of {@code ByteBuffer} as byte array.
	 * <p>
	 * WARNING: There is a performance cost due to array copy when using this
	 * method.
	 * 
	 * @param buffer
	 *            byte buffer
	 * @return byte array containing the byte buffer contents
	 */
	public static byte[] byteBuffertoArray(ByteBuffer buffer) {
		int length = buffer.remaining();
		if (buffer.hasArray()) {
			int offset = buffer.arrayOffset() + buffer.position();
			return Arrays.copyOfRange(buffer.array(), offset, offset + length);
		}
		byte[] bytes = new byte[length];
		buffer.duplicate().get(bytes);
		return bytes;
	}

	// Auxiliary path visitor for recursive directory structure copying.
	private static class DirectoryCopier extends SimpleFileVisitor<Path> {
		private Path src;
		private Path dst;
		private StandardCopyOption copyOption = StandardCopyOption.REPLACE_EXISTING;

		DirectoryCopier(String src, String dst) {
			this.src = Paths.get(src);
			this.dst = Paths.get(dst);
		}

		@Override
		public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
			Path targetPath = dst.resolve(src.relativize(dir));
			if (!Files.exists(targetPath)) {
				Files.createDirectory(targetPath);
			}
			return FileVisitResult.CONTINUE;
		}

		@Override
		public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
			Files.copy(file, dst.resolve(src.relativize(file)), copyOption);
			return FileVisitResult.CONTINUE;
		}
	}

}