// © 2016 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html
/*
 *******************************************************************************
 * Copyright (C) 1996-2014, International Business Machines Corporation and
 * others. All Rights Reserved.
 *******************************************************************************
 *
 */

package com.ibm.icu.dev.test.serializable;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Modifier;
import java.net.URL;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Objects;

import com.ibm.icu.dev.test.format.MeasureUnitTest;
import com.ibm.icu.dev.test.format.PluralRulesTest;
import com.ibm.icu.dev.test.number.NumberFormatterApiTest;
import com.ibm.icu.dev.test.number.PropertiesTest;
import com.ibm.icu.impl.JavaTimeZone;
import com.ibm.icu.impl.OlsonTimeZone;
import com.ibm.icu.impl.TimeZoneAdapter;
import com.ibm.icu.impl.URLHandler;
import com.ibm.icu.math.BigDecimal;
import com.ibm.icu.math.MathContext;
import com.ibm.icu.util.AnnualTimeZoneRule;
import com.ibm.icu.util.Calendar;
import com.ibm.icu.util.Currency;
import com.ibm.icu.util.DateInterval;
import com.ibm.icu.util.DateTimeRule;
import com.ibm.icu.util.GregorianCalendar;
import com.ibm.icu.util.ICUCloneNotSupportedException;
import com.ibm.icu.util.ICUException;
import com.ibm.icu.util.ICUInputTooLongException;
import com.ibm.icu.util.ICUUncheckedIOException;
import com.ibm.icu.util.InitialTimeZoneRule;
import com.ibm.icu.util.RuleBasedTimeZone;
import com.ibm.icu.util.SimpleTimeZone;
import com.ibm.icu.util.TimeArrayTimeZoneRule;
import com.ibm.icu.util.TimeZone;
import com.ibm.icu.util.TimeZoneRule;
import com.ibm.icu.util.ULocale;
import com.ibm.icu.util.VTimeZone;

/**
 * @author emader
 *
 * TODO To change the template for this generated type comment go to
 * Window - Preferences - Java - Code Style - Code Templates
 */
public class SerializableTestUtility {
    private static Class serializable;
    static {
        try {
            serializable = Class.forName("java.io.Serializable");
        } catch (ClassNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    public interface Handler
    {
        public Object[] getTestObjects();

        public boolean hasSameBehavior(Object a, Object b);
    }

    public static Handler getHandler(String className)
    {
        return (Handler) map.get(className);
    }

    private static class TimeZoneHandler implements Handler
    {
        String[] ZONES = { "GMT", "MET", "IST" };

        @Override
        public Object[] getTestObjects()
        {
            TimeZone zones[] = new TimeZone[ZONES.length];

            for(int z = 0; z < ZONES.length; z += 1) {
                zones[z] = TimeZone.getTimeZone(ZONES[z]);
            }

            return zones;
        }

        @Override
        public boolean hasSameBehavior(Object a, Object b)
        {
            TimeZone zone_a = (TimeZone) a;
            TimeZone zone_b = (TimeZone) b;

            if (!(zone_a.getDisplayName().equals(zone_b.getDisplayName()))) {
                return false;
            }

            int a_offsets[] = {0, 0};
            int b_offsets[] = {0, 0};

            boolean bSame = true;
            for (int i = 0; i < sampleTimes.length; i++) {
                zone_a.getOffset(sampleTimes[i], false, a_offsets);
                zone_b.getOffset(sampleTimes[i], false, b_offsets);
                if (a_offsets[0] != b_offsets[0] || a_offsets[1] != b_offsets[1]) {
                    bSame = false;
                    break;
                }
            }
            return bSame;
        }
    }

    private static Locale locales[] = {
        Locale.CANADA, Locale.CANADA_FRENCH, Locale.CHINA,
        Locale.CHINESE, Locale.ENGLISH, Locale.FRANCE, Locale.FRENCH,
        Locale.GERMAN, Locale.GERMANY, Locale.ITALIAN, Locale.ITALY,
        Locale.JAPAN, Locale.JAPANESE, Locale.KOREA, Locale.KOREAN,
        Locale.PRC, Locale.SIMPLIFIED_CHINESE, Locale.TAIWAN,
        Locale.TRADITIONAL_CHINESE, Locale.UK, Locale.US
    };

    private static Locale places[] = {
        Locale.CANADA, Locale.CANADA_FRENCH, Locale.CHINA,
        Locale.FRANCE, Locale.GERMANY, Locale.ITALY,
        Locale.JAPAN, Locale.KOREA, Locale.PRC, Locale.TAIWAN,
        Locale.UK, Locale.US
    };

    public static Locale[] getLocales()
    {
        return locales;
    }

    public static boolean compareStrings(String a[], String b[])
    {
        if (a.length != b.length) {
            return false;
        }

        for (int i = 0; i < a.length; i += 1) {
            if (! a[i].equals(b[i])) {
                return false;
            }
        }

        return true;
    }

    public static boolean compareChars(char a[], char b[])
    {
        if (a.length != b.length) {
            return false;
        }

        for (int i = 0; i < a.length; i += 1) {
            if (a[i] != b[i]) {
                return false;
            }
        }

        return true;
    }

    private static class SimpleTimeZoneHandler extends TimeZoneHandler
    {
        @Override
        public Object[] getTestObjects()
        {
            SimpleTimeZone simpleTimeZones[] = new SimpleTimeZone[6];

            simpleTimeZones[0] = new SimpleTimeZone(32400000, "MyTimeZone");

            simpleTimeZones[1] = new SimpleTimeZone(32400000, "Asia/Tokyo");

            simpleTimeZones[2] = new SimpleTimeZone(32400000, "Asia/Tokyo");
            simpleTimeZones[2].setRawOffset(0);

            simpleTimeZones[3] = new SimpleTimeZone(32400000, "Asia/Tokyo");
            simpleTimeZones[3].setStartYear(100);

            simpleTimeZones[4] = new SimpleTimeZone(32400000, "Asia/Tokyo");
            simpleTimeZones[4].setStartYear(1000);
            simpleTimeZones[4].setDSTSavings(1800000);
            simpleTimeZones[4].setStartRule(3, 4, 180000);
            simpleTimeZones[4].setEndRule(6, 3, 4, 360000);

            simpleTimeZones[5] = new SimpleTimeZone(32400000, "Asia/Tokyo");
            simpleTimeZones[5].setStartRule(2, 3, 4, 360000);
            simpleTimeZones[5].setEndRule(6, 3, 4, 360000);

            return simpleTimeZones;
        }
    }

    private static class VTimeZoneHandler extends TimeZoneHandler {
        @Override
        public Object[] getTestObjects() {
            //TODO
            VTimeZone[] vtzs = new VTimeZone[1];
            vtzs[0] = VTimeZone.create("America/New_York");
            return vtzs;
        }
    }

    private static final int HOUR = 60*60*1000;
    private static final AnnualTimeZoneRule[] TEST_US_EASTERN = {
        new AnnualTimeZoneRule("EST", -5*HOUR, 0,
                new DateTimeRule(Calendar.OCTOBER, -1, Calendar.SUNDAY, 2*HOUR, DateTimeRule.WALL_TIME),
                1967, 2006),

        new AnnualTimeZoneRule("EST", -5*HOUR, 0,
                new DateTimeRule(Calendar.NOVEMBER, 1, Calendar.SUNDAY, true, 2*HOUR, DateTimeRule.WALL_TIME),
                2007, AnnualTimeZoneRule.MAX_YEAR),

        new AnnualTimeZoneRule("EDT", -5*HOUR, 1*HOUR,
                new DateTimeRule(Calendar.APRIL, -1, Calendar.SUNDAY, 2*HOUR, DateTimeRule.WALL_TIME),
                1967, 1973),

        new AnnualTimeZoneRule("EDT", -5*HOUR, 1*HOUR,
                new DateTimeRule(Calendar.JANUARY, 6, 2*HOUR, DateTimeRule.WALL_TIME),
                1974, 1974),

        new AnnualTimeZoneRule("EDT", -5*HOUR, 1*HOUR,
                new DateTimeRule(Calendar.FEBRUARY, 23, 2*HOUR, DateTimeRule.WALL_TIME),
                1975, 1975),

        new AnnualTimeZoneRule("EDT", -5*HOUR, 1*HOUR,
                new DateTimeRule(Calendar.APRIL, -1, Calendar.SUNDAY, 2*HOUR, DateTimeRule.WALL_TIME),
                1976, 1986),

        new AnnualTimeZoneRule("EDT", -5*HOUR, 1*HOUR,
                new DateTimeRule(Calendar.APRIL, 1, Calendar.SUNDAY, true, 2*HOUR, DateTimeRule.WALL_TIME),
                1987, 2006),

        new AnnualTimeZoneRule("EDT", -5*HOUR, 1*HOUR,
                new DateTimeRule(Calendar.MARCH, 8, Calendar.SUNDAY, true, 2*HOUR, DateTimeRule.WALL_TIME),
                2007, AnnualTimeZoneRule.MAX_YEAR)
    };

    private static class RuleBasedTimeZoneHandler extends TimeZoneHandler
    {
        @Override
        public Object[] getTestObjects()
        {
            RuleBasedTimeZone ruleBasedTimeZones[] = new RuleBasedTimeZone[2];

            InitialTimeZoneRule ir = new InitialTimeZoneRule("GMT-5", -5*HOUR, 0);

            // GMT-5, no transition
            ruleBasedTimeZones[0] = new RuleBasedTimeZone("GMT-5", ir);


            // US Eastern since 1967
            ruleBasedTimeZones[1] = new RuleBasedTimeZone("US_East", ir);
            for (int i = 0; i < TEST_US_EASTERN.length; i++) {
                ruleBasedTimeZones[1].addTransitionRule(TEST_US_EASTERN[i]);
            }
            return ruleBasedTimeZones;
        }
    }

    private static class DateTimeRuleHandler implements Handler {
        @Override
        public Object[] getTestObjects() {
            DateTimeRule[] rules = new DateTimeRule[4];

            // DOM + UTC
            rules[0] = new DateTimeRule(Calendar.OCTOBER, 10, 13*HOUR, DateTimeRule.UTC_TIME);

            // DOW + WALL
            rules[1] = new DateTimeRule(Calendar.MARCH, 2, Calendar.SUNDAY, 2*HOUR, DateTimeRule.WALL_TIME);

            // DOW_GEQ_DOM + STD
            rules[2] = new DateTimeRule(Calendar.MAY, 1, Calendar.MONDAY, true, 0*HOUR, DateTimeRule.STANDARD_TIME);

            // DOW_LEQ_DOM + WALL
            rules[3] = new DateTimeRule(Calendar.AUGUST, 31, Calendar.SATURDAY, false, 1*HOUR, DateTimeRule.WALL_TIME);

            return rules;
        }

        @Override
        public boolean hasSameBehavior(Object a, Object b) {
            return hasSameRule((DateTimeRule)a, (DateTimeRule)b);
        }

        static boolean hasSameRule(DateTimeRule dtra, DateTimeRule dtrb) {
            boolean bSame = false;
            if (dtra.getDateRuleType() == dtrb.getDateRuleType()
                    && dtra.getRuleMonth() == dtrb.getRuleMonth()
                    && dtra.getTimeRuleType() == dtrb.getTimeRuleType()
                    && dtra.getRuleMillisInDay() == dtrb.getRuleMillisInDay()) {
                switch (dtra.getDateRuleType()) {
                case DateTimeRule.DOM:
                    bSame = (dtra.getRuleDayOfMonth() == dtrb.getRuleDayOfMonth());
                    break;
                case DateTimeRule.DOW:
                    bSame = (dtra.getRuleDayOfWeek() == dtrb.getRuleDayOfWeek() &&
                                dtra.getRuleWeekInMonth() == dtrb.getRuleWeekInMonth());
                    break;
                case DateTimeRule.DOW_GEQ_DOM:
                case DateTimeRule.DOW_LEQ_DOM:
                    bSame = (dtra.getRuleDayOfMonth() == dtrb.getRuleDayOfMonth() &&
                                dtra.getRuleDayOfWeek() == dtrb.getRuleDayOfWeek());
                    break;
                }
            }
            return bSame;
        }
    }

    private static boolean compareTimeZoneRules(TimeZoneRule ra, TimeZoneRule rb) {
        if (ra.getName().equals(rb.getName()) &&
                ra.getRawOffset() == rb.getRawOffset() &&
                ra.getDSTSavings() == rb.getDSTSavings()) {
            return true;
        }
        return false;
    }

    private static class AnnualTimeZoneRuleHandler implements Handler {
        @Override
        public Object[] getTestObjects() {
            return TEST_US_EASTERN;
        }

        @Override
        public boolean hasSameBehavior(Object a, Object b) {
            AnnualTimeZoneRule ra = (AnnualTimeZoneRule)a;
            AnnualTimeZoneRule rb = (AnnualTimeZoneRule)b;
            if (DateTimeRuleHandler.hasSameRule(ra.getRule(), rb.getRule()) &&
                    ra.getStartYear() == rb.getStartYear() &&
                    ra.getEndYear() == rb.getEndYear()) {
                return compareTimeZoneRules(ra, rb);
            }
            return false;
        }
    }

    private static class InitialTimeZoneRuleHandler implements Handler {
        @Override
        public Object[] getTestObjects() {
            TimeZoneRule[] rules = new TimeZoneRule[2];
            rules[0] = new InitialTimeZoneRule("EST", -5*HOUR, 0);
            rules[1] = new InitialTimeZoneRule("PST", -8*HOUR, 0);
            return rules;
        }

        @Override
        public boolean hasSameBehavior(Object a, Object b) {
            return compareTimeZoneRules((TimeZoneRule)a, (TimeZoneRule)b);
        }
    }

    private static class TimeArrayTimeZoneRuleHandler implements Handler {
        @Override
        public Object[] getTestObjects() {
            TimeArrayTimeZoneRule[] rules = new TimeArrayTimeZoneRule[1];
            long[] ttime = new long[] {-631152000000L, 0L, 946684800000L}; /* {1950-1-1, 1970-1-1, 2000-1-1} */
            rules[0] = new TimeArrayTimeZoneRule("Foo", 1*HOUR, 1*HOUR, ttime, DateTimeRule.UTC_TIME);

            return rules;
        }
        @Override
        public boolean hasSameBehavior(Object a, Object b) {
            TimeArrayTimeZoneRule ra = (TimeArrayTimeZoneRule)a;
            TimeArrayTimeZoneRule rb = (TimeArrayTimeZoneRule)b;

            Date da = ra.getFirstStart(0, 0);
            Date db = rb.getFirstStart(0, 0);
            long t = da.getTime();
            if (da.equals(db)) {
                da = ra.getFinalStart(0, 0);
                db = rb.getFinalStart(0, 0);
                long end = da.getTime();
                if (da.equals(db)) {
                    while (t < end) {
                        da = ra.getNextStart(t, 0, 0, false);
                        db = ra.getNextStart(t, 0, 0, false);
                        if (da == null || db == null || !da.equals(db)) {
                            break;
                        }
                        t = da.getTime();
                    }
                    return compareTimeZoneRules(ra, rb);
                }
            }
            return false;
        }
    }

    private static class ULocaleHandler implements Handler
    {
        @Override
        public Object[] getTestObjects()
        {
            ULocale uLocales[] = new ULocale[locales.length];

            for (int i = 0; i < locales.length; i += 1) {
                uLocales[i] = ULocale.forLocale(locales[i]);
            }

            return uLocales;
        }

        @Override
        public boolean hasSameBehavior(Object a, Object b)
        {
            ULocale uloc_a = (ULocale) a;
            ULocale uloc_b = (ULocale) b;

            return uloc_a.getName().equals(uloc_b.getName());
        }
    }

    public static class DateIntervalHandler implements Handler
    {
        private DateInterval dateInterval[] = {
                new DateInterval(0L, 1164931200000L/*20061201T000000Z*/)
        };
        @Override
        public Object[] getTestObjects()
        {
            return dateInterval;
        }

        @Override
        public boolean hasSameBehavior(Object a, Object b)
        {
            return a.equals(b);
        }
    }

    private static class CurrencyHandler implements Handler
    {
        @Override
        public Object[] getTestObjects()
        {
            Currency currencies[] = new Currency[places.length];

            for (int i = 0; i < places.length; i += 1) {
                currencies[i] = Currency.getInstance(places[i]);
            }

            return currencies;
        }

        @Override
        public boolean hasSameBehavior(Object a, Object b)
        {

            Currency curr_a = (Currency) a;
            Currency curr_b = (Currency) b;

            return a == b
                    || a != null && b != null
                    && curr_a.getCurrencyCode() != null
                    && curr_a.getCurrencyCode().equals(curr_b.getCurrencyCode());

        }
    }

    private static String zoneIDs[] = {
        "Pacific/Honolulu", "America/Anchorage", "America/Los_Angeles", "America/Denver",
        "America/Chicago", "America/New_York", "Africa/Cairo", "Africa/Addis_Ababa", "Africa/Dar_es_Salaam",
        "Africa/Freetown", "Africa/Johannesburg", "Africa/Nairobi", "Asia/Bangkok", "Asia/Baghdad",
        "Asia/Calcutta", "Asia/Hong_Kong", "Asia/Jakarta", "Asia/Jerusalem", "Asia/Manila", "Asia/Tokyo",
        "Europe/Amsterdam", "Europe/Athens", "Europe/Berlin", "Europe/London", "Europe/Malta", "Europe/Moscow",
        "Europe/Paris", "Europe/Rome"
    };

    private static long sampleTimes[] = {
        1136073600000L, // 20060101T000000Z
        1138752000000L, // 20060201T000000Z
        1141171200000L, // 20060301T000000Z
        1143849600000L, // 20060401T000000Z
        1146441600000L, // 20060501T000000Z
        1149120000000L, // 20060601T000000Z
        1151712000000L, // 20060701T000000Z
        1154390400000L, // 20060801T000000Z
        1157068800000L, // 20060901T000000Z
        1159660800000L, // 20061001T000000Z
        1162339200000L, // 20061101T000000Z
        1164931200000L, // 20061201T000000Z
    };

    private static class OlsonTimeZoneHandler implements Handler
    {
        @Override
        public Object[] getTestObjects()
        {
            OlsonTimeZone timeZones[] = new OlsonTimeZone[zoneIDs.length];

            for (int i = 0; i < zoneIDs.length; i += 1) {
                timeZones[i] = new OlsonTimeZone(zoneIDs[i]);
            }

            return timeZones;

        }

        @Override
        public boolean hasSameBehavior(Object a, Object b)
        {
            OlsonTimeZone otz_a = (OlsonTimeZone) a;
            OlsonTimeZone otz_b = (OlsonTimeZone) b;
            int a_offsets[] = {0, 0};
            int b_offsets[] = {0, 0};

            boolean bSame = true;
            for (int i = 0; i < sampleTimes.length; i++) {
                otz_a.getOffset(sampleTimes[i], false, a_offsets);
                otz_b.getOffset(sampleTimes[i], false, b_offsets);
                if (a_offsets[0] != b_offsets[0] || a_offsets[1] != b_offsets[1]) {
                    bSame = false;
                    break;
                }
            }
            return bSame;
        }
    }

    private static class TimeZoneAdapterHandler implements Handler
    {
        @Override
        public Object[] getTestObjects()
        {
            TimeZoneAdapter timeZones[] = new TimeZoneAdapter[zoneIDs.length];

            for (int i = 0; i < zoneIDs.length; i += 1) {
                timeZones[i] = new TimeZoneAdapter(TimeZone.getTimeZone(zoneIDs[i]));
            }

            return timeZones;

        }

        @Override
        public boolean hasSameBehavior(Object a, Object b)
        {
            GregorianCalendar cal = new GregorianCalendar();
            TimeZoneAdapter tza_a = (TimeZoneAdapter) a;
            TimeZoneAdapter tza_b = (TimeZoneAdapter) b;

            int a_offset, b_offset;
            boolean a_dst, b_dst;
            boolean bSame = true;
            for (int i = 0; i < sampleTimes.length; i++) {
                cal.setTimeInMillis(sampleTimes[i]);
                int era = cal.get(Calendar.ERA);
                int year = cal.get(Calendar.YEAR);
                int month = cal.get(Calendar.MONTH);
                int day = cal.get(Calendar.DAY_OF_MONTH);
                int dow = cal.get(Calendar.DAY_OF_WEEK);
                int mid = cal.get(Calendar.MILLISECONDS_IN_DAY);
                a_offset = tza_a.getOffset(era, year, month, day, dow, mid);
                b_offset = tza_b.getOffset(era, year, month, day, dow, mid);
                Date d = new Date(sampleTimes[i]);
                a_dst = tza_a.inDaylightTime(d);
                b_dst = tza_b.inDaylightTime(d);
                if (a_offset != b_offset || a_dst != b_dst) {
                    bSame = false;
                    break;
                }
            }
            return bSame;
        }
    }

    private static class JavaTimeZoneHandler implements Handler {
        String[] ZONES = { "GMT", "America/New_York", "GMT+05:45" };

        @Override
        public Object[] getTestObjects() {
            JavaTimeZone zones[] = new JavaTimeZone[ZONES.length];
            for(int z = 0; z < ZONES.length; z += 1) {
                java.util.TimeZone tz = java.util.TimeZone.getTimeZone(ZONES[z]);
                zones[z] = new JavaTimeZone(tz, ZONES[z]);
            }
            return zones;
        }

        @Override
        public boolean hasSameBehavior(Object a, Object b)
        {
            TimeZone zone_a = (TimeZone) a;
            TimeZone zone_b = (TimeZone) b;

            if (!(TimeZone.getCanonicalID(zone_a.getID()).equals(TimeZone.getCanonicalID(zone_b.getID())))) {
                return false;
            }

            int a_offsets[] = {0, 0};
            int b_offsets[] = {0, 0};

            boolean bSame = true;
            for (int i = 0; i < sampleTimes.length; i++) {
                zone_a.getOffset(sampleTimes[i], false, a_offsets);
                zone_b.getOffset(sampleTimes[i], false, b_offsets);
                if (a_offsets[0] != b_offsets[0] || a_offsets[1] != b_offsets[1]) {
                    bSame = false;
                    break;
                }
            }
            return bSame;
        }
    }

    private static class BigDecimalHandler implements Handler
    {
        String values[] = {
            "1234567890.",
            "123456789.0",
            "12345678.90",
            "1234567.890",
            "123456.7890",
            "12345.67890",
            "1234.567890",
            "123.4567890",
            "12.34567890",
            "1.234567890",
            ".1234567890"};

        @Override
        public Object[] getTestObjects()
        {
            BigDecimal bds[] = new BigDecimal[values.length];

            for (int i = 0; i < values.length; i += 1) {
                bds[i] = new BigDecimal(values[i]);
            }

            return bds;
        }

        @Override
        public boolean hasSameBehavior(Object a, Object b) {
            BigDecimal bda = (BigDecimal) a;
            BigDecimal bdb = (BigDecimal) b;

            return bda.toString().equals(bdb.toString());
        }
    }

    private static class MathContextHandler implements Handler
    {
        int forms[] = {MathContext.PLAIN, MathContext.ENGINEERING, MathContext.SCIENTIFIC};
        int rounds[] = {
            MathContext.ROUND_CEILING, MathContext.ROUND_DOWN, MathContext.ROUND_FLOOR,
            MathContext.ROUND_HALF_DOWN, MathContext.ROUND_HALF_EVEN, MathContext.ROUND_HALF_UP,
            MathContext.ROUND_UNNECESSARY, MathContext.ROUND_UP};

        @Override
        public Object[] getTestObjects()
        {
            int objectCount = forms.length * rounds.length;
            MathContext contexts[] = new MathContext[objectCount];
            int i = 0;

            for (int f = 0; f < forms.length; f += 1) {
                for (int r = 0; r < rounds.length; r += 1) {
                    int digits = f * r;
                    boolean lostDigits = (r & 1) != 0;

                    contexts[i++] = new MathContext(digits, forms[f], lostDigits, rounds[r]);
                }
            }

            return contexts;
        }

        @Override
        public boolean hasSameBehavior(Object a, Object b)
        {
            MathContext mca = (MathContext) a;
            MathContext mcb = (MathContext) b;

            return mca.toString().equals(mcb.toString());
        }
    }

    private static abstract class ExceptionHandlerBase implements Handler {
        @Override
        public boolean hasSameBehavior(Object a, Object b) {
            return sameThrowable((Exception) a, (Exception) b);
        }

        // Exception.equals() does not seem to work.
        private static final boolean sameThrowable(Throwable a, Throwable b) {
            return a == null ? b == null :
                    b == null ? false :
                            a.getClass().equals(b.getClass()) &&
                            Objects.equals(a.getMessage(), b.getMessage()) &&
                            sameThrowable(a.getCause(), b.getCause());
        }
    }

    private static class ICUExceptionHandler extends ExceptionHandlerBase {
        @Override
        public Object[] getTestObjects() {
            return new ICUException[] {
                    new ICUException(),
                    new ICUException("msg1"),
                    new ICUException(new RuntimeException("rte1")),
                    new ICUException("msg2", new RuntimeException("rte2"))
            };
        }
    }

    private static class ICUUncheckedIOExceptionHandler extends ExceptionHandlerBase {
        @Override
        public Object[] getTestObjects() {
            return new ICUUncheckedIOException[] {
                    new ICUUncheckedIOException(),
                    new ICUUncheckedIOException("msg1"),
                    new ICUUncheckedIOException(new RuntimeException("rte1")),
                    new ICUUncheckedIOException("msg2", new RuntimeException("rte2"))
            };
        }
    }

    private static class ICUCloneNotSupportedExceptionHandler extends ExceptionHandlerBase {
        @Override
        public Object[] getTestObjects() {
            return new ICUCloneNotSupportedException[] {
                    new ICUCloneNotSupportedException(),
                    new ICUCloneNotSupportedException("msg1"),
                    new ICUCloneNotSupportedException(new RuntimeException("rte1")),
                    new ICUCloneNotSupportedException("msg2", new RuntimeException("rte2"))
            };
        }
    }

    private static class ICUInputTooLongExceptionHandler extends ExceptionHandlerBase {
        @Override
        public Object[] getTestObjects() {
            return new ICUInputTooLongException[] {
                    new ICUInputTooLongException(),
                    new ICUInputTooLongException("msg1"),
                    new ICUInputTooLongException(new RuntimeException("rte1")),
                    new ICUInputTooLongException("msg2", new RuntimeException("rte2"))
            };
        }
    }

    private static HashMap map = new HashMap();

    static {
        map.put("com.ibm.icu.util.TimeZone", new TimeZoneHandler());
        map.put("com.ibm.icu.util.SimpleTimeZone", new SimpleTimeZoneHandler());
        map.put("com.ibm.icu.util.RuleBasedTimeZone", new RuleBasedTimeZoneHandler());
        map.put("com.ibm.icu.util.VTimeZone", new VTimeZoneHandler());
        map.put("com.ibm.icu.util.DateTimeRule", new DateTimeRuleHandler());
        map.put("com.ibm.icu.util.AnnualTimeZoneRule", new AnnualTimeZoneRuleHandler());
        map.put("com.ibm.icu.util.InitialTimeZoneRule", new InitialTimeZoneRuleHandler());
        map.put("com.ibm.icu.util.TimeArrayTimeZoneRule", new TimeArrayTimeZoneRuleHandler());
        map.put("com.ibm.icu.util.ULocale", new ULocaleHandler());
        map.put("com.ibm.icu.util.Currency", new CurrencyHandler());
        map.put("com.ibm.icu.impl.JavaTimeZone", new JavaTimeZoneHandler());
        map.put("com.ibm.icu.impl.OlsonTimeZone", new OlsonTimeZoneHandler());
        map.put("com.ibm.icu.impl.TimeZoneAdapter", new TimeZoneAdapterHandler());
        map.put("com.ibm.icu.math.BigDecimal", new BigDecimalHandler());
        map.put("com.ibm.icu.math.MathContext", new MathContextHandler());

        map.put("com.ibm.icu.text.NumberFormat", new FormatHandler.NumberFormatHandler());
        map.put("com.ibm.icu.text.DecimalFormat", new FormatHandler.DecimalFormatHandler());
        map.put("com.ibm.icu.text.CompactDecimalFormat", new FormatHandler.CompactDecimalFormatHandler());
        map.put("com.ibm.icu.text.RuleBasedNumberFormat", new FormatHandler.RuleBasedNumberFormatHandler());
        map.put("com.ibm.icu.text.CurrencyPluralInfo", new FormatHandler.CurrencyPluralInfoHandler());
        map.put("com.ibm.icu.text.DecimalFormatSymbols", new FormatHandler.DecimalFormatSymbolsHandler());
        map.put("com.ibm.icu.text.MessageFormat", new FormatHandler.MessageFormatHandler());
        map.put("com.ibm.icu.text.DateFormat", new FormatHandler.DateFormatHandler());
        map.put("com.ibm.icu.text.DateFormatSymbols", new FormatHandler.DateFormatSymbolsHandler());
        map.put("com.ibm.icu.util.DateInterval", new DateIntervalHandler());
        map.put("com.ibm.icu.text.DateIntervalFormat", new FormatHandler.DateIntervalFormatHandler());
        map.put("com.ibm.icu.text.DateIntervalInfo", new FormatHandler.DateIntervalInfoHandler());
        map.put("com.ibm.icu.text.DateIntervalInfo$PatternInfo", new FormatHandler.PatternInfoHandler());
        map.put("com.ibm.icu.text.SimpleDateFormat", new FormatHandler.SimpleDateFormatHandler());
        map.put("com.ibm.icu.text.ChineseDateFormat", new FormatHandler.ChineseDateFormatHandler());
        map.put("com.ibm.icu.text.ChineseDateFormatSymbols", new FormatHandler.ChineseDateFormatSymbolsHandler());
        map.put("com.ibm.icu.impl.DateNumberFormat", new FormatHandler.DateNumberFormatHandler());
        map.put("com.ibm.icu.text.PluralFormat", new FormatHandler.PluralFormatHandler());
        map.put("com.ibm.icu.text.PluralRules", new FormatHandler.PluralRulesHandler());
        map.put("com.ibm.icu.text.PluralRulesSerialProxy", new FormatHandler.PluralRulesSerialProxyHandler());
        map.put("com.ibm.icu.text.TimeUnitFormat", new FormatHandler.TimeUnitFormatHandler());
        map.put("com.ibm.icu.text.SelectFormat", new FormatHandler.SelectFormatHandler());
        map.put("com.ibm.icu.impl.TimeZoneNamesImpl", new FormatHandler.TimeZoneNamesHandler());
        map.put("com.ibm.icu.text.TimeZoneFormat", new FormatHandler.TimeZoneFormatHandler());
        map.put("com.ibm.icu.impl.TimeZoneGenericNames", new FormatHandler.TimeZoneGenericNamesHandler());
        map.put("com.ibm.icu.impl.TZDBTimeZoneNames", new FormatHandler.TZDBTimeZoneNamesHandler());

        map.put("com.ibm.icu.util.Calendar", new CalendarHandler.BasicCalendarHandler());
        map.put("com.ibm.icu.util.BuddhistCalendar", new CalendarHandler.BuddhistCalendarHandler());
        map.put("com.ibm.icu.util.ChineseCalendar", new CalendarHandler.ChineseCalendarHandler());
        map.put("com.ibm.icu.util.CopticCalendar", new CalendarHandler.CopticCalendarHandler());
        map.put("com.ibm.icu.util.DangiCalendar", new CalendarHandler.DangiCalendarHandler());
        map.put("com.ibm.icu.util.EthiopicCalendar", new CalendarHandler.EthiopicCalendarHandler());
        map.put("com.ibm.icu.util.GregorianCalendar", new CalendarHandler.GregorianCalendarHandler());
        map.put("com.ibm.icu.util.HebrewCalendar", new CalendarHandler.HebrewCalendarHandler());
        map.put("com.ibm.icu.util.IndianCalendar", new CalendarHandler.IndianCalendarHandler());
        map.put("com.ibm.icu.util.IslamicCalendar", new CalendarHandler.IslamicCalendarHandler());
        map.put("com.ibm.icu.util.JapaneseCalendar", new CalendarHandler.JapaneseCalendarHandler());
        map.put("com.ibm.icu.util.PersianCalendar", new CalendarHandler.PersianCalendarHandler());
        map.put("com.ibm.icu.util.TaiwanCalendar", new CalendarHandler.TaiwanCalendarHandler());

        map.put("com.ibm.icu.text.ArabicShapingException", new ExceptionHandler.ArabicShapingExceptionHandler());
        map.put("com.ibm.icu.text.StringPrepParseException", new ExceptionHandler.StringPrepParseExceptionHandler());
        map.put("com.ibm.icu.util.UResourceTypeMismatchException", new ExceptionHandler.UResourceTypeMismatchExceptionHandler());
        map.put("com.ibm.icu.impl.InvalidFormatException", new ExceptionHandler.InvalidFormatExceptionHandler());

        map.put("com.ibm.icu.text.NumberFormat$Field", new FormatHandler.NumberFormatFieldHandler());
        map.put("com.ibm.icu.text.DateFormat$Field", new FormatHandler.DateFormatFieldHandler());
        map.put("com.ibm.icu.text.ChineseDateFormat$Field", new FormatHandler.ChineseDateFormatFieldHandler());
        map.put("com.ibm.icu.text.MessageFormat$Field", new FormatHandler.MessageFormatFieldHandler());
        map.put("com.ibm.icu.text.RelativeDateTimeFormatter$Field", new FormatHandler.RelativeDateTimeFormatterFieldHandler());
        map.put("com.ibm.icu.text.DateIntervalFormat$SpanField", new FormatHandler.DateIntervalSpanFieldHandler());
        map.put("com.ibm.icu.text.ListFormatter$Field", new FormatHandler.ListFormatterFieldHandler());
        map.put("com.ibm.icu.text.ListFormatter$SpanField", new FormatHandler.ListFormatterSpanFieldHandler());

        map.put("com.ibm.icu.impl.duration.BasicDurationFormat", new FormatHandler.BasicDurationFormatHandler());
        map.put("com.ibm.icu.impl.RelativeDateFormat", new FormatHandler.RelativeDateFormatHandler());
        map.put("com.ibm.icu.util.IllformedLocaleException", new ExceptionHandler.IllformedLocaleExceptionHandler());
        map.put("com.ibm.icu.impl.locale.LocaleSyntaxException", new ExceptionHandler.LocaleSyntaxExceptionHandler());
        map.put("com.ibm.icu.impl.IllegalIcuArgumentException", new ExceptionHandler.IllegalIcuArgumentExceptionHandler());

        map.put("com.ibm.icu.text.PluralRules$FixedDecimal", new PluralRulesTest.FixedDecimalHandler());
        map.put("com.ibm.icu.util.MeasureUnit", new MeasureUnitTest.MeasureUnitHandler());
        map.put("com.ibm.icu.util.TimeUnit", new MeasureUnitTest.MeasureUnitHandler());
        map.put("com.ibm.icu.util.NoUnit", new MeasureUnitTest.MeasureUnitHandler());
        map.put("com.ibm.icu.text.MeasureFormat", new MeasureUnitTest.MeasureFormatHandler());
        map.put("com.ibm.icu.impl.number.Properties", new PropertiesTest.ICU59PropertiesHandler());
        map.put("com.ibm.icu.impl.number.DecimalFormatProperties", new PropertiesTest.PropertiesHandler());
        map.put("com.ibm.icu.impl.number.CustomSymbolCurrency", new CurrencyHandler());
        map.put("com.ibm.icu.number.SkeletonSyntaxException", new ExceptionHandler.SkeletonSyntaxExceptionHandler());
        map.put("com.ibm.icu.impl.number.LocalizedNumberFormatterAsFormat", new NumberFormatterApiTest.FormatHandler());

        map.put("com.ibm.icu.util.ICUException", new ICUExceptionHandler());
        map.put("com.ibm.icu.util.ICUUncheckedIOException", new ICUUncheckedIOExceptionHandler());
        map.put("com.ibm.icu.util.ICUCloneNotSupportedException", new ICUCloneNotSupportedExceptionHandler());
        map.put("com.ibm.icu.util.ICUInputTooLongException", new ICUInputTooLongExceptionHandler());
    }

    /*
     * Serialization Helpers
     */
    static Object[] getSerializedObjects(byte[] serializedBytes) throws ClassNotFoundException, IOException {
        ByteArrayInputStream bis = new ByteArrayInputStream(serializedBytes);
        ObjectInputStream ois = new ObjectInputStream(bis);
        Object inputObjects[] = (Object[]) ois.readObject();

        ois.close();
        return inputObjects;
    }

    static byte[] getSerializedBytes(Object[] objectsOut) throws IOException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(objectsOut);

        byte[] serializedBytes = bos.toByteArray();
        oos.close();
        return serializedBytes;
    }

    static Object[] getSerializedObjects(File testFile) throws IOException, ClassNotFoundException {
        FileInputStream fis = new FileInputStream(testFile);
        ObjectInputStream ois = new ObjectInputStream(fis);
        Object[] objects = (Object[]) ois.readObject();
        fis.close();
        return objects;
    }

    static byte[] copyStreamBytes(InputStream is) throws IOException {
        byte[] buffer = new byte[1024];

        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        int len;
        while((len = is.read(buffer, 0, buffer.length)) >= 0) {
            bos.write(buffer, 0, len);
        }
        return bos.toByteArray();
    }

    static List<String> getSerializationClassList(Object caller) throws IOException {
        List<String> classList = new ArrayList();
        Enumeration<URL> urlEnum = caller.getClass().getClassLoader().getResources("com/ibm/icu");
        while (urlEnum.hasMoreElements()) {
            URL url = urlEnum.nextElement();
            URLHandler handler  = URLHandler.get(url);
            if (handler == null) {
                System.out.println("Unsupported URL: " + url);
                continue;
            }
            CoverageClassVisitor visitor = new CoverageClassVisitor(classList);
            handler.guide(visitor, true, false);
        }
        return classList;
    }

    private static class CoverageClassVisitor implements URLHandler.URLVisitor {
        private List<String> classNames;

        public CoverageClassVisitor(List<String> classNamesList) {
            this.classNames = classNamesList;
        }

        /* (non-Javadoc)
         * @see com.ibm.icu.impl.URLHandler.URLVisitor#visit(java.lang.String)
         */
        @Override
        public void visit(String classPath) {
            int ix = classPath.lastIndexOf(".class");
            if (ix < 0) {
                return;
            }
            String className = "com.ibm.icu" + classPath.substring(0, ix).replace('/', '.');

            // Skip things in com.ibm.icu.dev; they're not relevant.
            if (className.startsWith("com.ibm.icu.dev.")) {
                return;
            }
            Class c;
            try {
                c = Class.forName(className);
            } catch (ClassNotFoundException e) {
                return;
            }
            int m = c.getModifiers();

            if (className.equals("com.ibm.icu.text.PluralRules$FixedDecimal")) {
                // Known Issue: "10268", "Serializable interface is not implemented in PluralRules$FixedDecimal"
                return;
            }

            if (className.equals("com.ibm.icu.text.DecimalFormat_ICU58")) {
                // Do not test the legacy DecimalFormat class in ICU 59
                return;
            }

            if (c.isEnum() || !serializable.isAssignableFrom(c)) {
                //System.out.println("@@@ Skipping: " + className);
                return;
            }
            if (!Modifier.isPublic(m) || Modifier.isInterface(m)) {
                //System.out.println("@@@ Skipping: " + className);
                return;
            }

            this.classNames.add(className);
        }
    }

    public static void serializeObjects(File oof, Object[] objectsOut) throws IOException {
        FileOutputStream fos = new FileOutputStream(oof);
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(objectsOut);

        oos.close();
    }
}
