package minejava.reg.time;

import minejava.reg.io.DataOutput;
import minejava.reg.io.IOException;
import minejava.reg.io.InvalidObjectException;
import minejava.reg.io.ObjectInputStream;
import minejava.reg.io.Serializable;
import minejava.reg.time.temporal.TemporalAccessor;
import minejava.time.format.DateTimeFormatterBuilder;
import minejava.reg.time.format.TextStyle;
import minejava.reg.time.temporal.TemporalField;
import minejava.reg.time.temporal.TemporalQueries;
import minejava.reg.time.temporal.TemporalQuery;
import minejava.reg.time.temporal.UnsupportedTemporalTypeException;
import minejava.time.zone.ZoneRules;
import minejava.time.zone.ZoneRulesException;
import minejava.time.zone.ZoneRulesProvider;
import minejava.reg.util.Collections;
import minejava.reg.util.HashMap;
import minejava.reg.util.Locale;
import minejava.reg.util.Map;
import minejava.reg.util.Objects;
import minejava.reg.util.Set;
import minejava.reg.util.TimeZone;

public abstract class ZoneId implements Serializable{

    /**
     * A map of zone overrides to enable the short time-zone names to be used.
     * <p>
     * Use of short zone IDs has been deprecated in{@code TimeZone}.
     * This map allows the IDs to continue to be used via the
     *{@link #of(String, Map)}factory method.
     * <p>
     * This map contains a mapping of the IDs that is in line with TZDB 2005r and
     * later, where 'EST', 'MST' and 'HST' map to IDs which do not include daylight
     * savings.
     * <p>
     * This maps as follows:
     * <ul>
     * <li>EST - -05:00</li>
     * <li>HST - -10:00</li>
     * <li>MST - -07:00</li>
     * <li>ACT - Australia/Darwin</li>
     * <li>AET - Australia/Sydney</li>
     * <li>AGT - America/Argentina/Buenos_Aires</li>
     * <li>ART - Africa/Cairo</li>
     * <li>AST - America/Anchorage</li>
     * <li>BET - America/Sao_Paulo</li>
     * <li>BST - Asia/Dhaka</li>
     * <li>CAT - Africa/Harare</li>
     * <li>CNT - America/St_Johns</li>
     * <li>CST - America/Chicago</li>
     * <li>CTT - Asia/Shanghai</li>
     * <li>EAT - Africa/Addis_Ababa</li>
     * <li>ECT - Europe/Paris</li>
     * <li>IET - America/Indiana/Indianapolis</li>
     * <li>IST - Asia/Kolkata</li>
     * <li>JST - Asia/Tokyo</li>
     * <li>MIT - Pacific/Apia</li>
     * <li>NET - Asia/Yerevan</li>
     * <li>NST - Pacific/Auckland</li>
     * <li>PLT - Asia/Karachi</li>
     * <li>PNT - America/Phoenix</li>
     * <li>PRT - America/Puerto_Rico</li>
     * <li>PST - America/Los_Angeles</li>
     * <li>SST - Pacific/Guadalcanal</li>
     * <li>VST - Asia/Ho_Chi_Minh</li>
     * </ul>
     * The map is unmodifiable.
     */
    public static final Map<String, String> SHORT_IDS;
    static{
        Map<String, String> map = new HashMap<>(64);
        map.put("ACT", "Australia/Darwin");
        map.put("AET", "Australia/Sydney");
        map.put("AGT", "America/Argentina/Buenos_Aires");
        map.put("ART", "Africa/Cairo");
        map.put("AST", "America/Anchorage");
        map.put("BET", "America/Sao_Paulo");
        map.put("BST", "Asia/Dhaka");
        map.put("CAT", "Africa/Harare");
        map.put("CNT", "America/St_Johns");
        map.put("CST", "America/Chicago");
        map.put("CTT", "Asia/Shanghai");
        map.put("EAT", "Africa/Addis_Ababa");
        map.put("ECT", "Europe/Paris");
        map.put("IET", "America/Indiana/Indianapolis");
        map.put("IST", "Asia/Kolkata");
        map.put("JST", "Asia/Tokyo");
        map.put("MIT", "Pacific/Apia");
        map.put("NET", "Asia/Yerevan");
        map.put("NST", "Pacific/Auckland");
        map.put("PLT", "Asia/Karachi");
        map.put("PNT", "America/Phoenix");
        map.put("PRT", "America/Puerto_Rico");
        map.put("PST", "America/Los_Angeles");
        map.put("SST", "Pacific/Guadalcanal");
        map.put("VST", "Asia/Ho_Chi_Minh");
        map.put("EST", "-05:00");
        map.put("MST", "-07:00");
        map.put("HST", "-10:00");
        SHORT_IDS = Collections.unmodifiableMap(map);
    }

    private static final long serialVersionUID = 8352817235686L;

    public static ZoneId systemDefault(){
        return TimeZone.getDefault().toZoneId();
    }

    public static Set<String> getAvailableZoneIds(){
        return ZoneRulesProvider.getAvailableZoneIds();
    }

    public static ZoneId of(String zoneId, Map<String, String> aliasMap){
        Objects.requireNonNull(zoneId, "zoneId");
        Objects.requireNonNull(aliasMap, "aliasMap");
        String id = aliasMap.get(zoneId);
        id = (id != null ? id : zoneId);
        return of(id);
    }

    public static ZoneId of(String zoneId){
        return of(zoneId, true);
    }

    public static ZoneId ofOffset(String prefix, ZoneOffset offset){
        Objects.requireNonNull(prefix, "prefix");
        Objects.requireNonNull(offset, "offset");
        if (prefix.length() == 0){
            return offset;
        }

        if (!prefix.equals("GMT") && !prefix.equals("UTC") && !prefix.equals("UT")){
             throw new IllegalArgumentException("prefix should be GMT, UTC or UT, is: " + prefix);
        }

        if (offset.getTotalSeconds() != 0){
            prefix = prefix.concat(offset.getId());
        }
        return new ZoneRegion(prefix, offset.getRules());
    }

    static ZoneId of(String zoneId, boolean checkAvailable){
        Objects.requireNonNull(zoneId, "zoneId");
        if (zoneId.length() <= 1 || zoneId.startsWith("+") || zoneId.startsWith("-")){
            return ZoneOffset.of(zoneId);
        }else if (zoneId.startsWith("UTC") || zoneId.startsWith("GMT")){
            return ofWithPrefix(zoneId, 3, checkAvailable);
        }else if (zoneId.startsWith("UT")){
            return ofWithPrefix(zoneId, 2, checkAvailable);
        }
        return ZoneRegion.ofId(zoneId, checkAvailable);
    }

    private static ZoneId ofWithPrefix(String zoneId, int prefixLength, boolean checkAvailable){
        String prefix = zoneId.substring(0, prefixLength);
        if (zoneId.length() == prefixLength){
            return ofOffset(prefix, ZoneOffset.UTC);
        }
        if (zoneId.charAt(prefixLength) != '+' && zoneId.charAt(prefixLength) != '-'){
            return ZoneRegion.ofId(zoneId, checkAvailable);

        }
        try{
            ZoneOffset offset = ZoneOffset.of(zoneId.substring(prefixLength));
            if (offset == ZoneOffset.UTC){
                return ofOffset(prefix, offset);
            }
            return ofOffset(prefix, offset);
        }catch (DateTimeException ex){
            throw new DateTimeException("Invalid ID for offset-based ZoneId: " + zoneId, ex);
        }
    }

    public static ZoneId from(TemporalAccessor temporal){
        ZoneId obj = temporal.query(TemporalQueries.zone());
        if (obj == null){
            throw new DateTimeException("Unable to obtain ZoneId from TemporalAccessor: " +
                    temporal + " of type " + temporal.getClass().getName());
        }
        return obj;
    }

    ZoneId(){
        if (getClass() != ZoneOffset.class && getClass() != ZoneRegion.class){
            throw new AssertionError("Invalid subclass");
        }
    }

    public abstract String getId();

    public String getDisplayName(TextStyle style, Locale locale){
        return new DateTimeFormatterBuilder().appendZoneText(style).toFormatter(locale).format(toTemporal());
    }

    private TemporalAccessor toTemporal(){
        return new TemporalAccessor(){
            @Override
            public boolean isSupported(TemporalField field){
                return false;
            }
            @Override
            public long getLong(TemporalField field){
                throw new UnsupportedTemporalTypeException("Unsupported field: " + field);
            }
            @SuppressWarnings("unchecked")
            @Override
            public <R> R query(TemporalQuery<R> query){
                if (query == TemporalQueries.zoneId()){
                    return (R) ZoneId.this;
                }
                return TemporalAccessor.super.query(query);
            }
        };
    }

    public abstract ZoneRules getRules();

    public ZoneId normalized(){
        try{
            ZoneRules rules = getRules();
            if (rules.isFixedOffset()){
                return rules.getOffset(Instant.EPOCH);
            }
        }catch (ZoneRulesException ex){

        }
        return this;
    }

    @Override
    public boolean equals(Object obj){
        if (this == obj){
           return true;
        }
        if (obj instanceof ZoneId){
            ZoneId other = (ZoneId) obj;
            return getId().equals(other.getId());
        }
        return false;
    }

    @Override
    public int hashCode(){
        return getId().hashCode();
    }

    private void readObject(ObjectInputStream s) throws InvalidObjectException{
        throw new InvalidObjectException("Deserialization via serialization delegate");
    }

    @Override
    public String toString(){
        return getId();
    }

    private Object writeReplace(){
        return new Ser(Ser.ZONE_REGION_TYPE, this);
    }

    abstract void write(DataOutput out) throws IOException;

}