/*
 * 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.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import jp.sourceforge.glad.calendar.CalendarException;
import jp.sourceforge.glad.calendar.Instant;

import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

/**
 * 祝日の一覧。
 * 
 * @author GLAD!!
 */
public class Holidays {

    // ---- constants

    static final String CONFIG_PATH =
            "jp/sourceforge/glad/calendar/holiday/holiday-config.xml";

    /** 日本の国コード */
    static final String JAPAN = "JP";

    static final String VERNAL_EQUINOX_DAY_TYPE = "VernalEquinoxDay";
    static final String AUTUMNAL_EQUINOX_DAY_TYPE = "AutumnalEquinoxDay";
    static final String SUBSTITUTE_HOLIDAY_TYPE = "SubstituteHoliday";
    static final String NATIONAL_HOLIDAY_TYPE = "NationalHoliday";

    // ---- fields

    /** 国ごとの祝日の一覧 */
    final Map<String, CountryHolidays> holidaysMap;

    // ---- constructors

    /**
     * オブジェクトを構築します。
     */
    Holidays() {
        InputStream in = getResourceAsStream(CONFIG_PATH);
        try {
            SAXParserFactory spfactory = SAXParserFactory.newInstance();
            SAXParser parser = spfactory.newSAXParser();
            SaxHandler handler = new SaxHandler();
            parser.parse(in, handler);
            Map<String, CountryHolidays> map = handler.holidaysMap;
            this.holidaysMap = Collections.unmodifiableMap(map);
        } catch (ParserConfigurationException e) {
            throw new CalendarException(e);
        } catch (SAXException e) {
            throw new CalendarException(e);
        } catch (IOException e) {
            throw new CalendarException(e);
        } finally {
            try {
                in.close();
            } catch (IOException e) {}
        }
    }

    @Deprecated
    Holidays(Map<String, CountryHolidays> holidaysMap) {
        this.holidaysMap = holidaysMap;
    }

    /**
     * 指定された名前のリソースを InputStream で返します。
     * 
     * @param name リソース名
     * @return InputStream
     */
    static InputStream getResourceAsStream(String name) {
        ClassLoader classLoader =
                Thread.currentThread().getContextClassLoader();
        InputStream in = classLoader.getResourceAsStream(name);
        if (in == null) {
            throw new CalendarException("resource not found: " + name);
        }
        return in;
    }

    /**
     * 設定ファイルを解析するハンドラです。
     */
    static class SaxHandler extends DefaultHandler {

        final Map<String, CountryHolidays> holidaysMap =
                new LinkedHashMap<String, CountryHolidays>();

        String country;
        List<Holiday> holidays;
        SubstituteHoliday substituteHoliday;
        NationalHoliday nationalHoliday;

        @Override
        public void startElement(
                String uri, String localName, String name,
                Attributes attributes) {
            if ("holiday".equals(name)) {
                addHoliday(attributes);
            } else if ("holidays".equals(name)) {
                initCountryHolidays(attributes);
            }
        }

        @Override
        public void endElement(String uri, String localName, String name) {
            if ("holidays".equals(name)) {
                addCountryHolidays();
            }
        }

        void initCountryHolidays(Attributes attrs) {
            country = attrs.getValue("country");
            holidays = new ArrayList<Holiday>();
            substituteHoliday = null;
            nationalHoliday = null;
        }

        void addCountryHolidays() {
            holidaysMap.put(country, createCountryHolidays());
        }

        CountryHolidays createCountryHolidays() {
            if (JAPAN.equals(country)) {
                return new JapaneseHolidays(
                        holidays, substituteHoliday, nationalHoliday);
            } else {
                throw new CalendarException(
                        "unsupported country: " + country);
            }
        }

        void addHoliday(Attributes attrs) {
            addHoliday(createHoliday(attrs));
        }

        void addHoliday(Holiday holiday) {
            if (holiday instanceof SubstituteHoliday) {
                substituteHoliday = (SubstituteHoliday) holiday;
            } else if (holiday instanceof NationalHoliday) {
                nationalHoliday = (NationalHoliday) holiday;
            } else {
                holidays.add(holiday);
            }
        }

        Holiday createHoliday(Attributes attrs) {
            String name = attrs.getValue("name");
            String date = attrs.getValue("date");
            Integer startYear = toInteger(attrs.getValue("start"));
            Integer endYear = toInteger(attrs.getValue("end"));
            String type = attrs.getValue("type");
            return createHoliday(name, date, startYear, endYear, type);
        }

        Holiday createHoliday(String name, String date,
                Integer startYear, Integer endYear, String type) {
            if (VERNAL_EQUINOX_DAY_TYPE.equals(type)) {
                return new VernalEquinoxDay(country, name, startYear, endYear);
            } else if (AUTUMNAL_EQUINOX_DAY_TYPE.equals(type)) {
                return new AutumnalEquinoxDay(country, name, startYear, endYear);
            } else if (SUBSTITUTE_HOLIDAY_TYPE.equals(type)) {
                return new SubstituteHoliday(country, name, startYear, endYear);
            } else if (NATIONAL_HOLIDAY_TYPE.equals(type)) {
                return new NationalHoliday(country, name, startYear, endYear);
            } else {
                return new Holiday(country, name, date, startYear, endYear);
            }
        }

        Integer toInteger(String s) {
            return (s == null) ? null : Integer.valueOf(s);
        }

    }

    // ---- singleton

    /**
     * 唯一のインスタンスを返します。
     * 
     * @return インスタンス
     */
    public static Holidays getInstance() {
        return instance;
    }

    // ---- accessors

    /**
     * 国ごとの祝日一覧を返します。
     * 
     * @return 国コードと祝日一覧のマップ
     */
    public Map<String, CountryHolidays> getHolidaysMap() {
        return holidaysMap;
    }

    /**
     * デフォルトの祝日一覧を返します。
     * 
     * @return 祝日一覧
     */
    public CountryHolidays getDefaultHolidays() {
        return getCountryHolidays(null);
    }

    /**
     * 指定された国の祝日一覧を返します。
     * 
     * @param country 国コード (ISO 3166-1 alpha-2)
     * @return 祝日一覧
     */
    public CountryHolidays getCountryHolidays(String country) {
        if (country == null || country.length() == 0) {
            country = Locale.getDefault().getCountry();
        }
        return holidaysMap.get(country);
    }

    // ---- other methods

    /**
     * 指定年の祝日一覧を返します。
     * 
     * @param year 年 (西暦)
     * @return 祝日のリスト
     */
    public List<Holiday> getHolidays(int year) {
        return getDefaultHolidays().getHolidays(year);
    }

    /**
     * 指定年月日の祝日を返します。
     * 
     * @param year  年 (西暦)
     * @param month 月
     * @param day   日
     * @return 祝日 (祝日でない場合は null)
     */
    public Holiday getHoliday(int year, int month, int day) {
        return getDefaultHolidays().getHoliday(year, month, day);
    }

    /**
     * 指定日の祝日を返します。
     * 
     * @param timeInMillis 通算ミリ秒
     * @param zone タイムゾーン
     * @return 祝日 (祝日でない場合は null)
     */
    public Holiday getHoliday(long timeInMillis, TimeZone zone) {
        return getDefaultHolidays().getHoliday(timeInMillis, zone);
    }

    /**
     * 指定日の祝日を返します。
     * 
     * @param calendar Calendar
     * @return 祝日 (祝日でない場合は null)
     */
    public Holiday getHoliday(Calendar calendar) {
        return getDefaultHolidays().getHoliday(calendar);
    }

    /**
     * 指定日の祝日を返します。
     * 
     * @param date Date
     * @return 祝日 (祝日でない場合は null)
     */
    public Holiday getHoliday(Date date) {
        return getDefaultHolidays().getHoliday(date);
    }

    /**
     * 指定日の祝日を返します。
     * 
     * @param instant 時点
     * @return 祝日 (祝日でない場合は null)
     */
    public Holiday getHoliday(Instant instant) {
        return getDefaultHolidays().getHoliday(instant);
    }

    /**
     * 指定年月日が祝日か判定します。
     * 
     * @param year  年 (西暦)
     * @param month 月
     * @param day   日
     * @return 祝日の場合は true
     */
    public boolean isHoliday(int year, int month, int day) {
        return getDefaultHolidays().isHoliday(year, month, day);
    }

    /**
     * 指定日が祝日か判定します。
     * 
     * @param timeInMillis 通算ミリ秒
     * @param zone タイムゾーン
     * @return 祝日の場合は true
     */
    public boolean isHoliday(long timeInMillis, TimeZone zone) {
        return getDefaultHolidays().isHoliday(timeInMillis, zone);
    }

    /**
     * 指定日が祝日か判定します。
     * 
     * @param calendar Calendar
     * @return 祝日の場合は true
     */
    public boolean isHoliday(Calendar calendar) {
        return getDefaultHolidays().isHoliday(calendar);
    }

    /**
     * 指定日が祝日か判定します。
     * 
     * @param date Date
     * @return 祝日の場合は true
     */
    public boolean isHoliday(Date date) {
        return getDefaultHolidays().isHoliday(date);
    }

    /**
     * 指定日が祝日か判定します。
     * 
     * @param instant 時点
     * @return 祝日の場合は true
     */
    public boolean isHoliday(Instant instant) {
        return getDefaultHolidays().isHoliday(instant);
    }

    // ---- singleton

    /** 唯一のインスタンス */
    static final Holidays instance = new Holidays();

}
