import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.time.Instant;
import java.util.HashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/*
 * 内閣府の「国民の祝日」CSVを取得して祝日情報を管理するクラス
 */
public class HolidayInfo{
	/*******************************************************************************
	 * 定数
	 ******************************************************************************/
	private final static String FETCH_URI = "https://www8.cao.go.jp/chosei/shukujitsu/syukujitsu.csv";
	private final static String FILENAME = "env"+File.separator+"holidays.csv";
	private final static String encoding = "MS932";

	/*******************************************************************************
	 * 静的メンバー
	 ******************************************************************************/
	private static HashMap<String, HolidayInfo> holidays = null;

	/*******************************************************************************
	 * 部品以外のインスタンスメンバー
	 ******************************************************************************/
	private int year;
	private int month;
	private int day;
	private String holidayName;

	/*******************************************************************************
	 * コンストラクタ
	 ******************************************************************************/
	public HolidayInfo(){
		year = 0;
		month = 0;
		day = 0;
		holidayName = null;
	}

	/*******************************************************************************
	 * 公開メソッド
	 ******************************************************************************/
	// 祝日の西暦年を返す
	public int getYear(){
		return year;
	}

	// 祝日の月を返す
	public int getMonth(){
		return month;
	}

	// 祝日の日を返す
	public int getDay(){
		return day;
	}

	// 祝日の名称を返す
	public String getHolidayName(){
		return holidayName;
	}

	/*******************************************************************************
	 * 静的公開メソッド
	 ******************************************************************************/
	/*
	 * CSVファイルを読み込む
	 */
	public static boolean Load(String uri, int interval){
		if (IsCSVFileLifetimeExpired(interval)){
			FetchCSVFile(uri);
		}

		return LoadFromCSVFile(FILENAME);
	}

	/*
	 * CSVファイルを更新する
	 */
	public static boolean Refresh(String uri, int interval){
		if (!IsCSVFileLifetimeExpired(interval))
			return true;

		FetchCSVFile(uri);

		return LoadFromCSVFile(FILENAME);
	}

	/*
	 * CSVファイルを内閣府のサーバーからダウンロードする
	 */
	public static boolean FetchCSVFile(String uri){
		if (uri == null || uri.isEmpty())
			uri = FETCH_URI;

		BufferedWriter bw = null;
		BufferedReader br = null;
		try {
			// 内閣府のホームページに接続する
			URL url = new URL(uri);
			HttpURLConnection ucon = (HttpURLConnection)url.openConnection();
			ucon.setConnectTimeout(5000);
			ucon.setReadTimeout(5000);
			ucon.setRequestMethod("GET");
			ucon.connect();

			// レスポンスを一時ファイルに書き出す
			String tmpfile = FILENAME + ".tmp";
			bw = new BufferedWriter(new FileWriter(tmpfile));
			br = new BufferedReader(new InputStreamReader(ucon.getInputStream(), encoding));

			String str;
		    while((str = br.readLine()) != null){
		    	bw.write(str);
		    	bw.write("\n");
		    }

		    br.close();
		    br = null;
		    bw.close();
		    bw = null;

			// CSVファイルにリネームする
		    File ofile = new File(FILENAME);
		    if ( ofile.exists() && ! ofile.delete() ) {
				System.err.println("[HolidayInfo]CSVファイルを削除できない:" + ofile.getAbsolutePath());
		    }

		    File nfile = new File(tmpfile);
		    if ( ! nfile.renameTo(ofile) ) {
				System.err.println("[HolidayInfo]CSVファイルをリネームできない:" +
						nfile.getAbsolutePath() + "=>" + ofile.getAbsolutePath());
		    	return false;
		    }

		    return true;
		} catch (MalformedURLException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
		finally{
			if (br != null) try { br.close(); } catch (Exception e) {}
			if (bw != null) try { bw.close(); } catch (Exception e) {}
		}

		return false;
	}

	/*
	 * CSVファイルを読み込む
	 */
	public static boolean LoadFromCSVFile(String path){
		if (path == null)
			return false;

		HashMap<String, HolidayInfo> map = new HashMap<String, HolidayInfo>();
		BufferedReader br = null;

		// CSVファイルが存在するかチェックする
		File file = new File(path);
		try {
			if (!file.exists()){
				System.err.println("[HolidayInfo]ファイルがない: " + file.getAbsolutePath());
				return false;
			}

			br = new BufferedReader(new InputStreamReader(new FileInputStream(file)));

			// 1行ずつ読み込む
			String str;
			while ((str = br.readLine()) != null) {
				// CSVの形式は YYYY/MM/DD,名称となっている
				Matcher ma = Pattern.compile("^(\\d{4})/(\\d{1,2})/(\\d{1,2}),(.*)$").matcher(str);
				if (!ma.find())
					continue;

				// 祝日情報を生成する
				HolidayInfo hi = new HolidayInfo();
				hi.year = Integer.parseInt(ma.group(1));
				hi.month = Integer.parseInt(ma.group(2));
				hi.day = Integer.parseInt(ma.group(3));
				hi.holidayName = ma.group(4);

				// キーに紐づけてハッシュマップに追加する
				String s = FormatKey( hi.year, hi.month, hi.day);
				map.put(s, hi);
			}
		}
		catch (Exception e) {
			e.printStackTrace();
			System.err.println("[HolidayInfo]ファイルの読み込みに失敗: "+file.getAbsolutePath());
			return false;
		}
		finally {
			if (br != null) try { br.close(); } catch (Exception e) {}
		}

		holidays = map;
		System.out.println("[HolidayInfo]祝日数: "+map.size());

		return true;
	}

	/*
	 * 指定した日が祝日かどうかを返す
	 */
    public static boolean IsHoliday(int year, int month, int day){
    	return IsHoliday(FormatKey(year, month, day));
    }

    /*
     * 指定した日が祝日の場合、その名前を返す
     */
    public static String GetHolidayName(int year, int month, int day){
    	return GetHolidayName(FormatKey(year, month, day));
    }

    /*
     * 指定した日の祝日情報を返す
     */
    public static HolidayInfo GetHolidayInfo(int year, int month, int day){
    	return GetHolidayInfo(FormatKey(year, month, day));
    }

    /*
     * YYYY/MM/DD形式で指定した日が祝日かどうかを返す
     */
	public static boolean IsHoliday(String date){
		HolidayInfo hi = GetHolidayInfo(date);

    	return (hi != null);
	}

    /*
     * YYYY/MM/DD形式で指定した日が祝日の場合、その名前を返す
     */
	public static String GetHolidayName(String date){
		HolidayInfo hi = GetHolidayInfo(date);

    	return (hi != null) ? hi.holidayName : null;
	}

    /*
     * YYYY/MM/DD形式で指定した日の祝日情報を返す
     */
	public static HolidayInfo GetHolidayInfo(String date){
		if (date == null || holidays == null)
			return null;

		if (date.length() > 10)
			date = date.substring(0, 10);

    	return holidays.get(date);
	}

	/*******************************************************************************
	 * 静的内部関数
	 ******************************************************************************/
    /*
     * ハッシュマップのキーを生成する
     */
	private static String FormatKey(int year, int month, int day){
		return String.format("%04d/%02d/%02d", year, month, day);
	}

	/*
	 * CSVファイルの寿命が尽きているかどうかを返す
	 */
	private static boolean IsCSVFileLifetimeExpired(int interval){
		// インターバルが0以下の場合は寿命はない
		if (interval <= 0)
			return false;

		// CSVファイルが存在しない場合は尽きているとみなす
		File file = new File(FILENAME);
		if (!file.exists())
			return true;

		try {
			BasicFileAttributes attrs = Files.readAttributes(file.toPath(), BasicFileAttributes.class);
			FileTime time = attrs.creationTime();
			Instant now = Instant.now();

			// ファイル生成後の経過ミリ秒数を計算する
			long elapsed = now.toEpochMilli() - time.toMillis();
			System.out.println("[HolidayInfo]経過時間(日):" + (elapsed/1000/24/3600));

			// インターバルを超えていない場合は尽きていない
			if ( elapsed/1000 < interval*24*3600)
				return false;
		} catch (IOException e) {
		}

		return true;
	}

}