package common.master;

import java.util.Objects;
import java.util.Optional;
import java.util.TimeZone;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;

import org.apache.logging.log4j.LogManager;

import core.exception.ThreadExceptionHandler;

/**
 * 間隔リフレッシュ
 *
 * @author Tadashi Nakayama
 */
public final class IntervalTimer implements Runnable {
	/** 分（ミリセック） */
	private static final long MIL_MIN = TimeUnit.MINUTES.toMillis(1);
	/** 時（ミリセック） */
	private static final long MIL_HOUR = TimeUnit.HOURS.toMillis(1);
	/** 日（ミリセック） */
	private static final long MIL_DAY = TimeUnit.DAYS.toMillis(1);

	/** IntervalThreadFactory */
	private static final IntervalThreadFactory FACTORY = new IntervalThreadFactory();

	/** タイマスレッド */
	private static final ScheduledExecutorService EXECUTOR =
					Executors.newSingleThreadScheduledExecutor(FACTORY);

	/** 間隔（ミリセック） */
	private volatile long distance = 0L;
	/** 次回（ミリセック） */
	private volatile long nextTime = 0L;
	/** リフレッシュ対象 */
	private final IntervalCache instance;

	/**
	 * コンストラクタ
	 *
	 * @param inst リフレッシュ対象インスタンス
	 * @param interval 間隔（整数部：時間 少数部：60分を1.0として換算）
	 */
	private IntervalTimer(final IntervalCache inst, final double interval) {
		this.instance = inst;
		if (0.0D < interval) {
			setDistance(interval);
			setNextTime(getNormalized(System.currentTimeMillis(), getDistance()));
		}
	}

	/**
	 * コンストラクタ
	 *
	 * @param inst リフレッシュ対象インスタンス
	 * @param interval 間隔（整数部：時間 少数部：60分を1.0として換算）
	 * @param base 基点時間（0-23)
	 */
	private IntervalTimer(final IntervalCache inst, final double interval, final int base) {
		this.instance = inst;
		if (0.0D < interval) {
			setDistance(interval);
			setNextTime(getNormalized(System.currentTimeMillis(), getDistance(), base));
		}
	}

	/**
	 * 開始
	 *
	 * @param instance リフレッシュ対象インスタンス
	 * @param interval 間隔（整数部：時間 少数部：60分を1.0として換算）
	 */
	public static void start(final Object instance, final double interval) {
		Optional.ofNullable(instance).filter(IntervalCache.class::isInstance).ifPresentOrElse(
			i -> new IntervalTimer(IntervalCache.class.cast(i), interval).startTimer(),
			() -> {
				throw new IllegalArgumentException("invalid instance.");
			}
		);
	}

	/**
	 * 開始
	 *
	 * @param instance リフレッシュ対象インスタンス
	 * @param base 基点時間（0-23)
	 */
	public static void start(final Object instance, final int base) {
		Optional.ofNullable(instance).filter(IntervalCache.class::isInstance).ifPresentOrElse(
			i -> new IntervalTimer(IntervalCache.class.cast(i), 24d, base).startTimer(),
			() -> {
				throw new IllegalArgumentException("invalid instance.");
			}
		);
	}

	/**
	 * 開始
	 *
	 * @param instance リフレッシュ対象インスタンス
	 * @param interval 間隔（整数部：時間 少数部：60分を1.0として換算）
	 * @param base 基点時間（0-23)
	 */
	public static void start(final Object instance, final double interval, final int base) {
		Optional.ofNullable(instance).filter(IntervalCache.class::isInstance).ifPresentOrElse(
			i -> new IntervalTimer(IntervalCache.class.cast(i), interval, base).startTimer(),
			() -> {
				throw new IllegalArgumentException("invalid instance.");
			}
		);
	}

	/**
	 * スレッド停止
	 */
	public static void stop() {
		EXECUTOR.shutdown();
		try {
			if (EXECUTOR.awaitTermination(2, TimeUnit.SECONDS)) {
				return;
			}
		} catch (final InterruptedException e) {
			LogManager.getLogger().info(e.getMessage());
		}
		EXECUTOR.shutdownNow();
	}

	/**
	 * リフレッシュ処理
	 */
	protected synchronized void mayRefresh() {
		if (0L < getDistance()) {
			this.instance.initialize();
		}
	}

	/**
	 * タイマ開始
	 */
	private void startTimer() {
		final var delay = Math.max(this.nextTime - System.currentTimeMillis(), 0);
		FACTORY.setName(this.getClass().getName());
		EXECUTOR.scheduleAtFixedRate(this, delay, getDistance(), TimeUnit.MILLISECONDS);
	}

	/**
	 * 間隔設定
	 *
	 * @param interval 間隔数値
	 */
	private void setDistance(final double interval) {
		final var h = Double.valueOf(interval).longValue();
		final var m = Double.valueOf(Math.ceil((interval - h) * 60D)).intValue();
		this.distance = (h * MIL_HOUR) + (m * MIL_MIN);
	}

	/**
	 * 間隔取得
	 *
	 * @return 間隔数値
	 */
	private long getDistance() {
		return this.distance;
	}

	/**
	 * 次回設定
	 *
	 * @param cur ミリセック
	 */
	private void setNextTime(final long cur) {
		this.nextTime = cur + this.distance;
	}

	/**
	 * 正規化時刻取得
	 *
	 * @param time ミリセック
	 * @param interval 間隔数値
	 * @return ミリセック
	 */
	private long getNormalized(final long time, final double interval) {
		final var diff = TimeZone.getDefault().getRawOffset();
		final var t = time + diff;
		final var hour = (t % MIL_DAY) / MIL_HOUR;
		final var min = (t % MIL_HOUR) / MIL_MIN;
		final var h = Double.valueOf(interval).longValue();
		final var m = Double.valueOf(Math.ceil((interval - h) * 60D)).intValue();

		var ret = 0L;
		if (m != 0) {
			ret = (min / m) * m * MIL_MIN;
		}
		if (h == 0L) {
			ret = ret + ((t / MIL_HOUR) * MIL_HOUR) - diff;
		} else {
			ret = ret + ((t / MIL_DAY) * MIL_DAY) + ((hour / h) * h * MIL_HOUR) - diff;
		}
		return ret;
	}

	/**
	 * 正規化時刻取得
	 *
	 * @param time ミリセック
	 * @param interval 間隔数値
	 * @param base 基点時間（0-23)
	 * @return ミリセック
	 */
	private long getNormalized(final long time, final long interval, final int base) {
		final var diff = TimeZone.getDefault().getRawOffset();
		var ret = (((time + diff) / MIL_DAY) * MIL_DAY) + base * MIL_HOUR - diff;
		while (time < ret) {
			ret = ret - interval;
		}
		return ret;
	}

	/**
	 * スレッド実行
	 */
	@Override
	public void run() {
		mayRefresh();
	}

	/**
	 * インターバルキャッシュ
	 *
	 * @author Tadashi Nakayama
	 */
	public interface IntervalCache {
		/**
		 * 初期化処理
		 */
		void initialize();
	}

	/**
	 * IntervalThreadFactory
	 *
	 * @author Tadashi Nakayama
	 */
	private static final class IntervalThreadFactory implements ThreadFactory {

		/** スレッド名 */
		private static final ThreadLocal<String> NAME = new ThreadLocal<>();

		/**
		 * コンストラクタ
		 */
		IntervalThreadFactory() {
			super();
		}

		/**
		 * 名前設定
		 *
		 * @param val 名前
		 */
		public void setName(final String val) {
			NAME.set(val);
		}

		/**
		 * @see java.util.concurrent.ThreadFactory#newThread(java.lang.Runnable)
		 */
		@Override
		public Thread newThread(final Runnable r) {
			final var th = new Thread(r);
			th.setUncaughtExceptionHandler(new ThreadExceptionHandler());
			th.setName(Objects.toString(NAME.get(), th.getName()));
			NAME.remove();
			return th;
		}
	}
}
