/*
 * 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.era;

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.GregorianCalendar;
import java.util.List;
import java.util.TimeZone;

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

import jp.sourceforge.glad.calendar.CalendarConsts;
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 JapaneseEras {

    // ---- constants

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

    static final JapaneseEra BEFORE_MEIJI =
            new JapaneseEra(0, "AD", "", "0001-01-01");

    // ---- fields

    /** 元号の一覧 */
    final List<JapaneseEra> japaneseEras;

    // ---- constructors

    /**
     * オブジェクトを構築します。
     */
    JapaneseEras() {
        InputStream in = getResourceAsStream(CONFIG_PATH);
        try {
            SAXParserFactory spfactory = SAXParserFactory.newInstance();
            SAXParser parser = spfactory.newSAXParser();
            SaxHandler handler = new SaxHandler();
            parser.parse(in, handler);
            List<JapaneseEra> eras = handler.japaneseEras;
            checkJapaneseEras(eras);
            this.japaneseEras = Collections.unmodifiableList(eras);
        } 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
    JapaneseEras(List<JapaneseEra> japaneseEras) {
        this.japaneseEras = japaneseEras;
    }

    /**
     * 指定された名前のリソースを 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;
    }

    /**
     * 元号の一覧をチェックします。
     * 
     * @param eras 元号のリスト
     */
    static void checkJapaneseEras(List<JapaneseEra> eras) {
        // 適用開始日の昇順に整列する。
        Collections.sort(eras);
        // 明治以前を追加。
        eras.add(0, BEFORE_MEIJI);
        for (int i = 0, size = eras.size(); i < size; ++i) {
            JapaneseEra era = eras.get(i);
            // ID が0から始まる連続値であること。
            if (era.getId() != i) {
                throw new CalendarException("config error: " + era);
            }
        }
    }

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

        final List<JapaneseEra> japaneseEras = new ArrayList<JapaneseEra>();

        @Override
        public void startElement(
                String uri, String localName, String name,
                Attributes attributes) {
            if ("japanese-era".equals(name)) {
                addJapaneseEra(attributes);
            }
        }

        void addJapaneseEra(Attributes attrs) {
            japaneseEras.add(createJapaneseEra(attrs));
        }

        JapaneseEra createJapaneseEra(Attributes attrs) {
            int id = Integer.parseInt(attrs.getValue("id"));
            String name = attrs.getValue("name");
            String abbr = attrs.getValue("abbr");
            String since = attrs.getValue("since");
            return new JapaneseEra(id, name, abbr, since);
        }

    }

    // ---- singleton

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

    // ---- accessors

    /**
     * 日本の元号のリストを返します。
     * 
     * @return 元号のリスト
     */
    public List<JapaneseEra> getJapaneseEras() {
        return japaneseEras;
    }

    // ---- other methods

    /**
     * 指定 ID の元号を返します。
     * 
     * @param id ID
     * @return 元号
     */
    public JapaneseEra getJapaneseEra(int id) {
        if (id < 0 || japaneseEras.size() < id) {
            throw new IllegalArgumentException("id=" + id);
        }
        return japaneseEras.get(id);
    }

    /**
     * 指定年月日 (西暦) の元号を返します。
     * 
     * @param year  年 (西暦)
     * @param month 月
     * @param day   日
     * @return 元号
     */
    public JapaneseEra getJapaneseEra(int year, int month, int day) {
        return getJapaneseEra0(getDateInMillisUTC(year, month, day));
    }

    /**
     * 指定通算ミリ秒の元号を返します。
     * 
     * @param timeInMillis 通算ミリ秒
     * @param zone タイムゾーン
     * @return 元号
     */
    public JapaneseEra getJapaneseEra(long timeInMillis, TimeZone zone) {
        if (zone == null) {
            zone = TimeZone.getDefault();
        }
        int offset = zone.getOffset(timeInMillis);
        return getJapaneseEra0(timeInMillis + offset);
    }

    /**
     * 指定日の元号を返します。
     * 
     * @param calendar Calendar
     * @return 元号
     */
    public JapaneseEra getJapaneseEra(Calendar calendar) {
        return getJapaneseEra(
                calendar.getTimeInMillis(), calendar.getTimeZone());
    }

    /**
     * 指定日の元号を返します。
     * 
     * @param date Date
     * @return 元号
     */
    public JapaneseEra getJapaneseEra(Date date) {
        return getJapaneseEra(date.getTime(), null);
    }

    /**
     * 指定時点の元号を返します。
     * 
     * @param instant 時点
     * @return 元号
     */
    public JapaneseEra getJapaneseEra(Instant instant) {
        return getJapaneseEra(
                instant.getTimeInMillis(), instant.getTimeZone());
    }

    JapaneseEra getJapaneseEra0(long timeInMillisUTC) {
        for (int i = japaneseEras.size() - 1; 0 < i; --i) {
            JapaneseEra era = japaneseEras.get(i);
            if (era.getSince() <= timeInMillisUTC) {
                return era;
            }
        }
        return japaneseEras.get(0);
    }

    /**
     * 指定年月日 (西暦) の元号の ID を返します。
     * 
     * @param year  年 (西暦)
     * @param month 月
     * @param day   日
     * @return ID
     * @see jp.sourceforge.glad.calendar.CalendarConsts
     */
    public int getEraId(int year, int month, int day) {
        return getJapaneseEra(year, month, day).getId();
    }

    /**
     * 指定通算ミリ秒の元号の ID を返します。
     * 
     * @param timeInMillis 通算ミリ秒
     * @param zone タイムゾーン
     * @return ID
     * @see jp.sourceforge.glad.calendar.CalendarConsts
     */
    public int getEraId(long timeInMillis, TimeZone zone) {
        return getJapaneseEra(timeInMillis, zone).getId();
    }

    /**
     * 指定日の元号の ID を返します。
     * 
     * @param calendar Calendar
     * @return ID
     * @see jp.sourceforge.glad.calendar.CalendarConsts
     */
    public int getEraId(Calendar calendar) {
        return getJapaneseEra(calendar).getId();
    }

    /**
     * 指定日の元号の ID を返します。
     * 
     * @param date Date
     * @return ID
     * @see jp.sourceforge.glad.calendar.CalendarConsts
     */
    public int getEraId(Date date) {
        return getJapaneseEra(date).getId();
    }

    /**
     * 指定時点の元号の ID を返します。
     * 
     * @param instant 時点
     * @return ID
     * @see jp.sourceforge.glad.calendar.CalendarConsts
     */
    public int getEraId(Instant instant) {
        return getJapaneseEra(instant).getId();
    }

    /**
     * ID の最小値を返します。
     * 
     * @return ID の最小値
     */
    public int getMinimumEraId() {
        return 0;
    }

    /**
     * ID の最大値を返します。
     * 
     * @return ID の最大値
     */
    public int getMaximumEraId() {
        return japaneseEras.size() - 1;
    }

    /**
     * 指定年月日 (西暦) の和暦年を返します。
     * 
     * @param year  年 (西暦)
     * @param month 月
     * @param day   日
     * @return 和暦年 (該当する元号がない場合は西暦年)
     */
    public int getYearOfEra(int year, int month, int day) {
        JapaneseEra era = getJapaneseEra(year, month, day);
        return era.getYearOfEra(year);
    }

    /**
     * 指定日の和暦年を返します。
     * 
     * @param timeInMillis 通算ミリ秒
     * @param zone タイムゾーン
     * @return 和暦年 (該当する元号がない場合は西暦年)
     */
    public int getYearOfEra(long timeInMillis, TimeZone zone) {
        return getYearOfEra0(toGregorianCalendar(timeInMillis, zone));
    }

    /**
     * 指定日の和暦年を返します。
     * 
     * @param calendar Calendar
     * @return 和暦年 (該当する元号がない場合は西暦年)
     */
    public int getYearOfEra(Calendar calendar) {
        return getYearOfEra0(toGregorianCalendar(calendar));
    }

    /**
     * 指定日の和暦年を返します。
     * 
     * @param date Date
     * @return 和暦年 (該当する元号がない場合は西暦年)
     */
    public int getYearOfEra(Date date) {
        return getYearOfEra(toGregorianCalendar(date));
    }

    /**
     * 指定日の和暦年を返します。
     * 
     * @param instant 時点
     * @return 和暦年 (該当する元号がない場合は西暦年)
     */
    public int getYearOfEra(Instant instant) {
        return getYearOfEra0(instant.toGregorianCalendar());
    }

    /**
     * 指定日の和暦年を返します。
     * 
     * @param calendar GregorianCalendar
     * @return 和暦年 (該当する元号がない場合は西暦年)
     */
    int getYearOfEra0(GregorianCalendar calendar) {
        return getYearOfEra(
                calendar.get(Calendar.YEAR),
                calendar.get(Calendar.MONTH) + 1,
                calendar.get(Calendar.DAY_OF_MONTH));
    }

    /**
     * 指定和暦年の西暦年を返します。
     * 
     * @param eraId     元号
     * @param yearOfEra 和暦年
     * @return 西暦年
     */
    public int getGregorianYear(int eraId, int yearOfEra) {
        JapaneseEra era = getJapaneseEra(eraId);
        return era.getGregorianYear(yearOfEra);
    }

    /**
     * UTC 基準のミリ秒を返します。
     * 
     * @param year  年
     * @param month 月
     * @param day   日
     * @return ミリ秒
     */
    static long getDateInMillisUTC(int year, int month, int day) {
        Calendar calendar = new GregorianCalendar(CalendarConsts.UTC);
        calendar.clear();
        calendar.set(year, month - 1, day);
        return calendar.getTimeInMillis();
    }

    static GregorianCalendar toGregorianCalendar(
            long timeInMillis, TimeZone zone) {
        if (zone == null) {
            zone = TimeZone.getDefault();
        }
        GregorianCalendar calendar = new GregorianCalendar(zone);
        calendar.setTimeInMillis(timeInMillis);
        return calendar;
    }

    static GregorianCalendar toGregorianCalendar(Calendar calendar) {
        if (calendar instanceof GregorianCalendar) {
            return (GregorianCalendar) calendar;
        } else {
            return toGregorianCalendar(
                    calendar.getTimeInMillis(), calendar.getTimeZone());
        }
    }

    /**
     * Date を GregorianCalendar に変換します。
     * 
     * @param date Date
     * @return GregorianCalendar
     */
    static GregorianCalendar toGregorianCalendar(Date date) {
        GregorianCalendar calendar = new GregorianCalendar();
        calendar.setTime(date);
        return calendar;
    }

    // ---- singleton

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

}
