package project.batch;

import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import project.db.model.FwJobRepeat;
import batch.controller.JobRequestor;
import batch.status.Job;
import common.db.JdbcSource;
import common.db.dao.Dao;
import common.sql.QueryUtil;
import core.util.DateUtil;
import core.util.NumberUtil;

/**
 * 定期起動起動バッチ
 * @author Tadashi Nakayama
 */
public final class ScheduledBatch extends ProjectBatch {

	/**
	 * メイン処理
	 * @param args 引数
	 */
	public static void main(final String... args) {
		int ret = ProjectPerform.start(ScheduledBatch.class, args);
		Runtime.getRuntime().exit(ret);
	}

	/**
	 * @see batch.base.Batch#getBatchName()
	 */
	@Override
	public String getBatchName() {
		return "定期起動起動バッチ";
	}

	/**
	 * @see batch.base.Batch#perform(java.lang.String...)
	 */
	@Override
	public int perform(final String... args) {
		Scheduler sche = new Scheduler(Scheduler.getFutureJob());
		sche.scheduleAll();

		ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
		executor.scheduleWithFixedDelay(sche, 120, 120, TimeUnit.SECONDS);
		return RET_SUCCESS;
	}

	/**
	 * スケジューラ
	 * @author Tadashi Nakayama
	 */
	private static final class Scheduler implements Runnable {
		/** スケジューラ */
		private final ScheduledExecutorService ses = Executors.newSingleThreadScheduledExecutor();

		/** タスクリスト */
		private final List<FutureJob> future;

		/**
		 * コンストラクタ
		 * @param val タスクリスト
		 */
		Scheduler(final List<FutureJob> val) {
			this.future = val;
		}

		/**
		 * タスクスケジュール
		 */
		public void scheduleAll() {
			for (final FutureJob job : this.future) {
				schedule(job);
			}
		}

		/**
		 * タスクスケジュール
		 * @param job タスク
		 */
		private void schedule(final FutureJob job) {
			long delay = job.getDelay();
			if (0 < delay) {
				job.setFuture(this.ses.schedule(new JobCommand(job), delay, TimeUnit.SECONDS));
			}
		}

		/**
		 * タスク取得
		 * @return タスク
		 */
		public static List<FutureJob> getFutureJob() {
			try (Dao dao = JdbcSource.getDao()) {
				String query = QueryUtil.getSqlFromFile("SelectJobRepeat", ScheduledBatch.class);
				return dao.selectAll(FutureJob.class, query).collect(Collectors.toList());
			}
		}

		/**
		 * @see java.lang.Runnable#run()
		 */
		@Override
		public void run() {
			List<FutureJob> list = getFutureJob();
			int i = 0;
			int j = 0;
			while (i < this.future.size() || j < list.size()) {
				FutureJob job1 = getFutureJobAt(i, this.future);
				FutureJob job2 = getFutureJobAt(j, list);

				if (job1 == null) {
					// 新規追加された
					schedule(job2);
					j++;
				} else if (job2 == null) {
					// 削除された
					cancel(job1);
					i++;
				} else if (job1.getId().compareTo(job2.getId()) < 0) {
					// 削除された
					cancel(job1);
					i++;
				} else if (job1.getId().compareTo(job2.getId()) > 0) {
					// 新規追加された
					schedule(job2);
					j++;
				} else if (job1.getId().equals(job2.getId())) {
					if (job1.getVersion() == job2.getVersion()) {
						// 変更なし
						if (job1.getFuture() != null && job1.getFuture().isDone()) {
							schedule(job2);
						}

						if (job2.getFuture() == null) {
							job2.setFuture(job1.getFuture());
						}
					} else {
						// 変更された
						cancel(job1);
						schedule(job2);
					}
					i++;
					j++;
				}
			}
			this.future.clear();
			this.future.addAll(list);
		}

		/**
		 * ジョブ取得
		 * @param list ジョブリスト
		 * @param loc 位置
		 * @return ジョブ
		 */
		private FutureJob getFutureJobAt(final int loc, final List<FutureJob> list) {
			if (loc < list.size()) {
				return list.get(loc);
			}
			return null;
		}

		/**
		 * キャンセル処理
		 * @param job ジョブ
		 */
		private void cancel(final FutureJob job) {
			Future<?> f = job.getFuture();
			if (f != null) {
				f.cancel(false);
				job.setFuture(null);
			}
		}
	}

	/**
	 * ジョブ依頼コマンド
	 * @author Tadashi Nakayama
	 */
	private static final class JobCommand implements Runnable {
		/** ジョブ */
		private final FutureJob future;

		/**
		 * コンストラクタ
		 * @param fj FutureJob
		 */
		JobCommand(final FutureJob fj) {
			this.future = fj;
		}

		/**
		 * @see java.lang.Runnable#run()
		 */
		@Override
		public void run() {
			if (this.future.getVersion() == getVersion(this.future.getId())) {
				JobRequestor.requestJob(getJob());
			}
		}

		/**
		 * バージョン取得
		 * @param val ID
		 * @return バージョン
		 */
		private int getVersion(final Long val) {
			try (Dao dao = JdbcSource.getDao()) {
				String query = QueryUtil.getSqlFromFile("SelectVersion", this.getClass());
				Integer ret = dao.select(Integer.class, query, val);
				if (ret != null) {
					return ret.intValue();
				}
				return 0;
			}
		}

		/**
		 * Job作成
		 * @return Job
		 */
		private Job getJob() {
			Job job = new Job();
			job.setExecParam(this.future.getBatParam());
			job.setGamenParam(this.future.getDispParam());
			job.setJobId(this.future.getJobId());
			job.setJobName(this.future.getJobName());
			job.setDateTime(DateUtil.getDateTime());
			job.setUid(this.future.getUpdateId());
			return job;
		}
	}

	/**
	 * スケジュールジョブ
	 * @author Tadashi Nakayama
	 */
	public static final class FutureJob extends FwJobRepeat {
		/** serialVersionUID */
		private static final long serialVersionUID = 2253566566769631226L;

		/** タスク */
		private transient Future<?> future;

		/**
		 * @return the future
		 */
		public Future<?> getFuture() {
			return this.future;
		}

		/**
		 * @param val the future to set
		 */
		public void setFuture(final Future<?> val) {
			this.future = val;
		}

		/**
		 * 初期実行迄時間取得
		 * @return 初期実行迄時間
		 */
		public long getDelay() {
			long ret = 0;
			Date now = new Date();
			if ("day".equalsIgnoreCase(super.getInterval())) {
				ret = getDayDelay(now, DateUtil.calcDay(now, 1));
			} else if ("week".equalsIgnoreCase(super.getInterval())) {
				ret = getWeekDelay(now);
			} else if ("month".equalsIgnoreCase(super.getInterval())) {
				ret = getMonthDelay(now, super.getYearMonthDay(), null);
			} else if ("year".equalsIgnoreCase(super.getInterval())) {
				ret = getYearDelay(now);
			} else if ("next".equalsIgnoreCase(super.getInterval())) {
				ret = getNextDelay(now);
			}
			return ret;
		}

		/**
		 * 差分（秒）取得
		 * @param now 現在日時
		 * @param target 対象日
		 * @return 差分（秒）
		 */
		private long toDelay(final Date now, final Date target) {
			if (target != null) {
				Date dt = DateUtil.toDateTime(DateUtil.format(target, DateUtil.FORMAT_DATE)
								+ super.getHour() + super.getMinute());
				return (dt.getTime() - now.getTime()) / 1000;
			}
			return 0;
		}

		/**
		 * 指定月内の日付取得
		 * @param cal 指定月のカレンダー
		 * @param day 日
		 * @return 指定月内日
		 */
		private int getDayIn(final Calendar cal, final String day) {
			return Math.min(cal.getActualMaximum(Calendar.DATE), NumberUtil.toInt(day, 0));
		}

		/**
		 * 次起動間隔（日次）取得
		 * @param now 現在日時
		 * @param next 次起動日
		 * @return 次起動間隔（日次）
		 */
		private long getDayDelay(final Date now, final Date next) {
			if ((super.getHour() + super.getMinute()).compareTo(
							DateUtil.format(now, "HHmm")) > 0) {
				return toDelay(now, now);
			}
			return toDelay(now, next);
		}

		/**
		 * 次起動間隔（週次）取得
		 * @param now 現在日時
		 * @return 次起動間隔
		 */
		private long getWeekDelay(final Date now) {
			Calendar cal = Calendar.getInstance();
			cal.setTime(now);
			int diff = NumberUtil.toInt(super.getWeek(), 0) - cal.get(Calendar.DAY_OF_WEEK);
			if (0 < diff) {
				return toDelay(now, DateUtil.calcDay(now, diff));
			} else if (diff < 0) {
				return toDelay(now, DateUtil.calcDay(now, diff + 7));
			}
			return getDayDelay(now, DateUtil.calcDay(now, 7));
		}

		/**
		 * 次起動間隔（月次）取得
		 * @param now 現在日時
		 * @param day 日
		 * @param next 次起動日
		 * @return 次起動間隔（月次）
		 */
		private long getMonthDelay(final Date now, final String day, final Date next) {
			Calendar cal = Calendar.getInstance();
			cal.setTime(now);
			int diff = getDayIn(cal, day) - cal.get(Calendar.DAY_OF_MONTH);
			if (diff > 0) {
				return toDelay(now, DateUtil.calcDay(now, diff));
			}

			Date dt = next;
			if (dt == null) {
				cal.add(Calendar.MONTH, 1);
				cal.set(Calendar.DAY_OF_MONTH, getDayIn(cal, day));
				dt = cal.getTime();
			}

			if (diff < 0) {
				return toDelay(now, dt);
			}
			return getDayDelay(now, dt);
		}

		/**
		 * 次起動間隔（年次）取得
		 * @param now 現在日時
		 * @return 次起動間隔（年次）
		 */
		private long getYearDelay(final Date now) {
			String ymd = super.getYearMonthDay();
			int loc = ymd.offsetByCodePoints(0, 2);

			Calendar cal = Calendar.getInstance();
			cal.setTime(now);
			int diff = NumberUtil.toInt(ymd.substring(0, loc), 0) - cal.get(Calendar.MONTH) - 1;
			if (0 < diff) {
				cal.add(Calendar.MONTH, diff);
				cal.set(Calendar.DAY_OF_MONTH, getDayIn(cal, ymd.substring(loc)));
				return toDelay(now, cal.getTime());
			}

			cal.add(Calendar.YEAR, 1);
			cal.set(Calendar.MONTH, NumberUtil.toInt(ymd.substring(0, loc), 0) - 1);
			cal.set(Calendar.DAY_OF_MONTH, getDayIn(cal, ymd.substring(loc)));
			if (diff < 0) {
				return toDelay(now, cal.getTime());
			}

			return getMonthDelay(now, ymd.substring(loc), cal.getTime());
		}

		/**
		 * 次起動間隔（次回）取得
		 * @param now 現在日時
		 * @return 次起動間隔（次回）
		 */
		private long getNextDelay(final Date now) {
			Date dt = DateUtil.toDate(super.getYearMonthDay());
			int diff = dt.compareTo(DateUtil.toDate(now));
			if (diff > 0) {
				return toDelay(now, dt);
			} else if (diff < 0) {
				return 0;
			}
			return getDayDelay(now, null);
		}
	}
}
