/* 
 * Windows timestamp accessor (Java)
 * 
 * Copyright (c) 2020 Project Nayuki
 * All rights reserved. Contact Nayuki for licensing.
 * https://www.nayuki.io/page/windows-timestamp-accessor-library
 */

import java.io.BufferedReader;
import java.io.EOFException;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;


// Note: Ticks is the number of 100-nanosecond units since the epoch of
// midnight UTC on January 1st, Year 1 on the proleptic Gregorian calendar.

public final class WindowsTimestampAccessor implements AutoCloseable {
	
	/*---- Object state ----*/
	
	private Process process;
	private PrintWriter query;
	private BufferedReader response;
	
	
	
	/*---- Initialization and disposal ----*/
	
	private static final String EXECUTABLE_PATH = "WindowsTimestampAccessor.exe";  // Assumes it is in the current working directory
	
	
	public WindowsTimestampAccessor() throws IOException {
		process = Runtime.getRuntime().exec(new String[]{EXECUTABLE_PATH});
		query = new PrintWriter(new OutputStreamWriter(process.getOutputStream(), StandardCharsets.UTF_8));
		response = new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8));
	}
	
	
	public synchronized void close() throws IOException, InterruptedException {
		query.close();
		response.close();
		if (process.waitFor() != 0)
			throw new RuntimeException("Subprocess exited with non-zero status code");
	}
	
	
	
	/*---- Methods to get/set file/directory timestamps ----*/
	
	public long getCreationTime(File item) throws IOException {
		return getSomeTime("Creation", item);
	}
	
	public long getModificationTime(File item) throws IOException {
		return getSomeTime("Modification", item);
	}
	
	public long getAccessTime(File item) throws IOException {
		return getSomeTime("Access", item);
	}
	
	
	private synchronized long getSomeTime(String type, File item) throws IOException {
		query.printf("Get%sTime\t%s%n", type, item.getAbsolutePath());
		query.flush();
		String line = response.readLine();
		if (line == null)
			throw new EOFException();
		String[] tokens = line.split("\t", -1);
		if (!tokens[0].equals("ok") || tokens.length != 2)
			throw new RuntimeException("Invalid data");
		return Long.parseLong(tokens[1]);
	}
	
	
	public void setCreationTime(File item, long ticks) throws IOException {
		setSomeTime("Creation", item, ticks);
	}
	
	public void setModificationTime(File item, long ticks) throws IOException {
		setSomeTime("Modification", item, ticks);
	}
	
	public void setAccessTime(File item, long ticks) throws IOException {
		setSomeTime("Access", item, ticks);
	}
	
	
	private synchronized void setSomeTime(String type, File item, long ticks) throws IOException {
		query.printf("Set%sTime\t%s\t%d%n", type, item.getAbsolutePath(), ticks);
		query.flush();
		String line = response.readLine();
		if (line == null)
			throw new EOFException();
		if (!line.equals("ok"))
			throw new RuntimeException("Invalid data");
	}
	
	
	
	/*---- DateTime/ticks conversion ----*/
	
	/* 
	 * Returns an array of 7 integers representing the date and time:
	 * - [0]: Year
	 * - [1]: Month, range [1, 12]
	 * - [2]: Day, range [1, 31]
	 * - [3]: Hour, range [0, 23]
	 * - [4]: Minute, range [0, 59]
	 * - [5]: Second, range [0, 59]
	 * - [6]: Microsecond, range [0, 999999]
	 * Note that this conversion loses the last digit of precision, because 1 tick is 0.1 microsecond.
	 */
	public static int[] ticksToDatetime(long ticks) {
		int[] result = new int[7];
		long temp = ticks;
		
		// Calculate sub-day units
		{
			long mod;
			mod = mod(temp, 10000000);  // Ticks modulo second
			result[6] = (int)(mod / 10);  // Microseconds
			long quot = temp / 10000000;  // Do flooring division and avoid overflow
			if (temp < 0 && quot * 10000000 != temp)
				quot--;
			temp = quot;
			
			mod = mod(temp, 60);
			result[5] = (int)mod;  // Seconds
			temp = (temp - mod) / 60;
			
			mod = mod(temp, 60);
			result[4] = (int)mod;  // Minutes
			temp = (temp - mod) / 60;
			
			mod = mod(temp, 24);
			result[3] = (int)mod;  // Hours
			temp = (temp - mod) / 24;
		}
		
		// Calculate years, months, days
		{
			int d = (int)temp + 306;
			int mod = mod(d, 146097);
			int y = (d - mod) / 146097 * 400;
			d = mod;
			y += Math.min(d / 36524, 3) * 100;
			d -= Math.min(d / 36524, 3) * 36524;
			y += d / 1461 * 4;
			d -= d / 1461 * 1461;
			y += Math.min(d / 365, 3);
			d -= Math.min(d / 365, 3) * 365;
			
			int m = 0;
			while (m < 11 && CUMULATIVE_DAYS[m + 1] <= d)
				m++;
			d = d - CUMULATIVE_DAYS[m] + 1;
			m += 3;
			if (m > 12) {
				y++;
				m -= 12;
			}
			
			result[0] = y;
			result[1] = m;
			result[2] = d;
		}
		
		return result;
	}
	
	
	// Takes an array of 7 integers representing the date and time:
	// {year, month, day, hour, minute, second, microsecond}.
	// The method accepts lenient date-time representations. For example,
	// month 13 = January of the next year; hour -1 = 23 o'clock of the previous day.
	public static long datetimeToTicks(int... dt) {
		if (dt.length != 7)
			throw new IllegalArgumentException();
		
		long y = dt[0];
		long m = dt[1];
		
		// Reduce months
		m -= 3;
		long mmod = mod(m, 12);
		y += (m - mmod) / 12;
		m = mmod;
		
		// Reduce years
		long ymod = mod(y, 400);
		long days = (y - ymod) / 400 * 146097;
		y = ymod;
		
		days += y*365 + y/4 - y/100 + y/400 + (m * 153 + 2) / 5 + dt[2] - 307;
		
		// Accumulate sub-day units
		BigInteger result = BigInteger.valueOf(days);
		result = result.multiply(BigInteger.valueOf(24)).add(BigInteger.valueOf(dt[3]));  // Hours
		result = result.multiply(BigInteger.valueOf(60)).add(BigInteger.valueOf(dt[4]));  // Minutes
		result = result.multiply(BigInteger.valueOf(60)).add(BigInteger.valueOf(dt[5]));  // Seconds
		result = result.multiply(BigInteger.valueOf(1000000)).add(BigInteger.valueOf(dt[6]));  // Microseconds
		result = result.multiply(BigInteger.TEN);  // Ticks
		
		if (result.bitLength() >= 64)
			throw new ArithmeticException("Arithmetic overflow");
		return result.longValue();
	}
	
	
	private static int mod(int x, int y) {
		return (x % y + y) % y;
	}
	
	private static long mod(long x, long y) {
		return (x % y + y) % y;
	}
	
	
	private static final int[] CUMULATIVE_DAYS = {0, 31, 61, 92, 122, 153, 184, 214, 245, 275, 306, 337};
	
}
