/*
 * Copyright (C) 2008-2009 GLAD!! (ITO Yoshiichi)
 *
 * 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 jp.sourceforge.glad.calendar.holiday;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.WeakHashMap;

import jp.sourceforge.glad.calendar.CalendarConsts;
import jp.sourceforge.glad.calendar.ISOCalendar;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * 日本の祝日の一覧。
 * 
 * @author GLAD!!
 */
public class JapaneseHolidays extends CountryHolidays {

    // ---- static fields

    static final Log log = LogFactory.getLog(JapaneseHolidays.class);

    // ---- fields

    /** 振替休日 */
    final SubstituteHoliday substituteHoliday;

    /** 国民の休日 */
    final NationalHoliday nationalHoliday;

    /** 年ごとの祝日一覧のキャッシュ */
    private final Map<Integer, HolidaysTable> holidaysTableMap =
            new WeakHashMap<Integer, HolidaysTable>();

    // ---- constructors

    /**
     * オブジェクトを構築します。
     * 
     * @param holidays 祝日の一覧
     * @param substituteHoliday 振替休日
     * @param nationalHoliday 国民の休日
     */
    JapaneseHolidays(
            List<Holiday> holidays,
            SubstituteHoliday substituteHoliday,
            NationalHoliday nationalHoliday) {
        super(Holidays.JAPAN, holidays);
        checkArgs(substituteHoliday, nationalHoliday);
        this.substituteHoliday = substituteHoliday;
        this.nationalHoliday = nationalHoliday;
    }

    static void checkArgs(
            SubstituteHoliday substituteHoliday,
            NationalHoliday nationalHoliday) {
        if (substituteHoliday == null) {
            throw new IllegalArgumentException("substituteHoliday is null");
        }
        if (nationalHoliday == null) {
            throw new IllegalArgumentException("nationalHoliday is null");
        }
    }

    // ---- flyweight

    public static JapaneseHolidays getInstance() {
        return (JapaneseHolidays) getInstance(Holidays.JAPAN);
    }

    // ---- accessors

    /**
     * 振替休日を返します。
     * 
     * @return 振替休日
     */
    public SubstituteHoliday getSubstituteHoliday() {
        return substituteHoliday;
    }

    /**
     * 国民の休日を返します。
     * 
     * @return 国民の休日
     */
    public NationalHoliday getNationalHoliday() {
        return nationalHoliday;
    }

    // ---- other methods

    /**
     * {@inheritDoc}
     */
    public List<Holiday> getHolidays(int year) {
        return getHolidaysTable(year).getHolidays();
    }

    /**
     * {@inheritDoc}
     */
    public Holiday getHoliday(int year, int month, int day) {
        HolidaysTable holidaysTable = getHolidaysTable(year);
        long dateInMillisUTC = getDateInMillisUTC(year, month, day);
        return holidaysTable.getHoliday(dateInMillisUTC);
    }

    /**
     * 年ごとの祝日一覧を返します。
     */
    HolidaysTable getHolidaysTable(int year) {
        synchronized (holidaysTableMap) {
            // 弱参照にするためオブジェクトを新たに生成する。
            Integer key = new Integer(year);
            HolidaysTable holidaysTable = holidaysTableMap.get(key);
            if (holidaysTable == null) {
                // キャッシュに存在しない場合は、日付を計算して一覧を作成する。
                holidaysTable = new HolidaysTable(year);
                holidaysTableMap.put(key, holidaysTable);
            }
            return holidaysTable;
        }
    }

    /**
     * UTC 基準のミリ秒を返します。
     */
    long getDateInMillisUTC(int year, int month, int day) {
        return new ISOCalendar(year, month, day, CalendarConsts.UTC)
                .getTimeInMillis();
    }

    /**
     * 年ごとの祝日一覧。
     */
    class HolidaysTable {

        static final int ONE_DAY  = 1 * 24 * 60 * 60 * 1000;
        static final int TWO_DAYS = 2 * 24 * 60 * 60 * 1000;

        /** 年 (西暦) */
        final int year;

        /** 祝日一覧 (キーは UTC 基準ミリ秒) */
        final SortedMap<Long, Holiday> holidayMap =
                new TreeMap<Long, Holiday>();

        /**
         * オブジェクトを構築します。
         */
        HolidaysTable(int year) {
            this.year = year;
            // 振替が必要な祝日。
            List<ISOCalendar> needSubstitute = new ArrayList<ISOCalendar>();
            addHolidays(needSubstitute);
            if (substituteHoliday.isAvailableYear(year)) {
                addSubstituteHolidays(needSubstitute);
            }
            if (nationalHoliday.isAvailableYear(year)) {
                addNationalHolidays();
            }
            log.debug(this);
        }

        /**
         * 祝日を追加します。
         */
        void addHolidays(List<ISOCalendar> needSubstitute) {
            for (Holiday holiday : holidays) {
                if (holiday.isAvailableYear(year)) {
                    ISOCalendar calendar =
                            holiday.getISOCalendar(year, CalendarConsts.UTC);
                    Long dateInMillisUTC = calendar.getTimeInMillis();
                    holidayMap.put(dateInMillisUTC, holiday);
                    if (calendar.isSunday()) {
                        needSubstitute.add(calendar);
                    }
                }
            }
        }

        /**
         * 振替休日を追加します。
         */
        void addSubstituteHolidays(List<ISOCalendar> needSubstitute) {
            for (ISOCalendar target : needSubstitute) {
                Long dateInMillisUTC = target.getTimeInMillis() + ONE_DAY;
                if (!substituteHoliday.isAvailableDate(dateInMillisUTC)) {
                    continue;
                }
                while (holidayMap.containsKey(dateInMillisUTC)) {
                    dateInMillisUTC += ONE_DAY;
                }
                logSubstituteHoliday(dateInMillisUTC);
                holidayMap.put(dateInMillisUTC, substituteHoliday);
            }
        }

        void logSubstituteHoliday(long dateInMillisUTC) {
            if (log.isDebugEnabled()) {
                DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
                df.setTimeZone(CalendarConsts.UTC);
                log.debug(substituteHoliday.getName() + ": "
                        + df.format(new Date(dateInMillisUTC)));
            }
        }

        /**
         * 国民の休日を追加します。
         */
        void addNationalHolidays() {
            List<Long> nationalHolidays = new ArrayList<Long>();
            Long prev = null;
            for (Map.Entry<Long, Holiday> entry : holidayMap.entrySet()) {
                Long dateInMillisUTC = entry.getKey();
                if (entry.getValue() instanceof SubstituteHoliday) {
                    prev = null;
                    continue;
                }
                if (prev != null) {
                    if (dateInMillisUTC - prev == TWO_DAYS) {
                        ISOCalendar calendar = new ISOCalendar(
                                dateInMillisUTC - ONE_DAY, CalendarConsts.UTC);
                        if (!calendar.isSunday()) {
                            logNationalHoliday(calendar);
                            nationalHolidays.add(calendar.getTimeInMillis());
                        }
                    }
                }
                prev = dateInMillisUTC;
            }
            for (Long dateInMillis : nationalHolidays) {
                holidayMap.put(dateInMillis, nationalHoliday);
            }
        }

        void logNationalHoliday(ISOCalendar calendar) {
            if (log.isDebugEnabled()) {
                log.debug(nationalHoliday.getName() + ": "
                        + calendar.format("yyyy-MM-dd"));
            }
        }

        /**
         * 祝日の一覧を返します。
         * 
         * @return 祝日のリスト
         */
        public List<Holiday> getHolidays() {
            return new ArrayList<Holiday>(holidayMap.values());
        }

        /**
         * 指定日の祝日を返します。
         * 
         * @param dateInMillisUTC UTC 基準ミリ秒
         * @return 祝日 (祝日でない場合は null)
         */
        public Holiday getHoliday(long dateInMillisUTC) {
            return holidayMap.get(dateInMillisUTC);
        }

        /**
         * オブジェクトの文字列表現を返します。
         */
        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder();
            sb.append("JapaneseHolidays(").append(year).append("): ");
            if (!holidayMap.isEmpty()) {
                ISOCalendar calendar = new ISOCalendar();
                for (Long date : holidayMap.keySet()) {
                    sb.append(calendar.setTimeInMillis(date).format("MM-dd, "));
                }
                sb.setLength(sb.length() - 2);
            }
            return sb.toString();
        }

    }

}
