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

package com.ibm.icu.dev.tool.timezone;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.Writer;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.TreeSet;

import com.ibm.icu.text.DecimalFormat;
import com.ibm.icu.text.DecimalFormatSymbols;
import com.ibm.icu.text.SimpleDateFormat;
import com.ibm.icu.util.GregorianCalendar;
import com.ibm.icu.util.SimpleTimeZone;
import com.ibm.icu.util.ULocale;

/**
 * TimeZone transition dump tool.
 */
public class ICUZDump {
    private static final String DEFAULT_LINE_SEP;

    static {
        String sep = System.getProperty("line.separator");
        DEFAULT_LINE_SEP = (sep == null) ? "\n" : sep;
    }

    private TimeZoneImpl tz = null;

    private int loyear = 1900;
    private int hiyear = 2050;
    private long tick = 1000;
    private DumpFormatter formatter;
    private String linesep = DEFAULT_LINE_SEP;

    public ICUZDump() {
    }
    
    public void setLowYear(int loyear) {
        this.loyear = loyear;
    }
    
    public void setHighYear(int hiyear) {
        this.hiyear = hiyear;
    }

    public void setTick(int tick) {
        if (tick <= 0) {
            throw new IllegalArgumentException("tick must be positive");
        }
        this.tick = tick;
    }
    
    public void setTimeZone(Object tzimpl) {
        this.tz = new TimeZoneImpl(tzimpl);
    }

    public void setDumpFormatter(DumpFormatter formatter) {
        this.formatter = formatter;
    }
    
    public void setLineSeparator(String sep) {
        this.linesep = sep;
    }
    
    public void dump(Writer w) throws IOException {
        if (tz == null) {
            throw new IllegalStateException("timezone is not initialized");
        }

        if (formatter == null) {
            formatter = new DumpFormatter();
        }
        
        final long SEARCH_INCREMENT = 12 * 60 * 60 * 1000; // half day
        long cutovers[] = getCutOverTimes();
        long t = cutovers[0];
        int offset = tz.getOffset(t);
        boolean inDst = tz.inDaylightTime(t);
        while (t < cutovers[1]) {
            long newt = t + SEARCH_INCREMENT;
            int newOffset = tz.getOffset(newt);
            boolean newInDst = tz.inDaylightTime(newt);
            if (offset != newOffset || inDst != newInDst) {
                // find the boundary
                long lot = t;
                long hit = newt;
                while (true) {
                    long diff = hit - lot;
                    if (diff <= tick) {
                        break;
                    }
                    long medt = lot + ((diff / 2) / tick) * tick;
                    int medOffset = tz.getOffset(medt);
                    boolean medInDst = tz.inDaylightTime(medt);
                    if (medOffset != offset || medInDst != inDst) {
                        hit = medt;
                    } else {
                        lot = medt;
                    }
                }
                w.write(formatter.format(lot, offset, tz.inDaylightTime(lot)));
                w.write(" > ");
                w.write(formatter.format(hit, newOffset, tz.inDaylightTime(hit)));
                w.write(linesep);
                offset = newOffset;
                inDst = newInDst;
            }
            t = newt;
        }

    }
    
    private long[] getCutOverTimes() {
        long[] cutovers = new long[2];
        cutovers[0] = tz.getTime(loyear, 0, 1, 0, 0, 0);
        cutovers[1] = tz.getTime(hiyear, 0, 1, 0, 0, 0);
        return cutovers;
    }

    private class TimeZoneImpl {
        private Object tzobj;
        
        public TimeZoneImpl(Object tzobj) {
            this.tzobj = tzobj;
        }

        public int getOffset(long time) {
            try {
                Method method = tzobj.getClass().getMethod("getOffset", new Class[] {long.class});
                Object result = method.invoke(tzobj, new Object[] {time});
                return ((Integer)result).intValue();
            } catch (Exception e) {
                e.printStackTrace();
            }
            return 0;
        }

        public boolean inDaylightTime(long time) {
            try {
                Method method = tzobj.getClass().getMethod("inDaylightTime", new Class[] {Date.class});
                Object result = method.invoke(tzobj, new Object[] {new Date(time)});
                return ((Boolean)result).booleanValue();
            } catch (Exception e) {
                e.printStackTrace();
            }
            return false;
        }

        public long getTime(int year, int month, int dayOfMonth, int hour, int minute, int second) {
            long time;
            if (tzobj instanceof com.ibm.icu.util.TimeZone) {
                GregorianCalendar cal = new GregorianCalendar();
                cal.setTimeZone((com.ibm.icu.util.TimeZone)tzobj);
                cal.clear();
                cal.set(year, month, dayOfMonth, hour, minute, second);
                time = cal.getTimeInMillis();
            } else if (tzobj instanceof java.util.TimeZone) {
                java.util.GregorianCalendar cal = new java.util.GregorianCalendar();
                cal.setTimeZone((java.util.TimeZone)tzobj);
                cal.clear();
                cal.set(year, month, dayOfMonth, hour, minute, second);
                time = cal.getTimeInMillis();
            } else {
                throw new IllegalStateException("Unsupported TimeZone implementation");
            }
            return time;
        }
    }

    public class DumpFormatter {
        private SimpleTimeZone stz = new SimpleTimeZone(0, "");
        private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd EEE HH:mm:ss", ULocale.US);
        private DecimalFormat decf;

        public DumpFormatter() {
            DecimalFormatSymbols decfs = new DecimalFormatSymbols(ULocale.US);
            decf = new DecimalFormat("00", decfs);
        }

        public String format(long time, int offset, boolean isDst) {
            StringBuffer buf = new StringBuffer();
            stz.setRawOffset(offset);
            sdf.setTimeZone(stz);
            buf.append(sdf.format(new Date(time)));
            if (offset < 0) {
                buf.append("-");
                offset = -offset;
            } else {
                buf.append("+");
            }

            int hour, min, sec;

            offset /= 1000;
            sec = offset % 60;
            offset = (offset - sec) / 60;
            min = offset % 60;
            hour = offset / 60;

            buf.append(decf.format(hour));
            buf.append(decf.format(min));
            buf.append(decf.format(sec));

            buf.append("[DST=");
            buf.append(isDst ? "1" : "0");
            buf.append("]");
            return buf.toString();
        }
    }

    /*
     * Usage:
     * 
     * java -cp icu4j.jar com.ibm.icu.dev.tool.timezone [-j] [-a] [-c[<low_year>,]<high_year>] [-d<dir>] [-l<sep>] [<zone_name> [<zone_name>]]
     * 
     * Options:
     *      -j      : Use JDK TimeZone.  By default, ICU TimeZone is used.
     *      -a      : Dump all available zones.
     *      -c[<low_year>,]<high_year>
     *              : When specified, dump transitions starting <low_year> (inclusive) up to
     *                <high_year> (exclusive).  The default values are 1902(low) and 2038(high).
     *      -d<dir> : When specified, write transitions in a file under the directory for each zone.
     *      -l<sep> : New line code type CR/LF/CRLF.
     */
    public static void main(String[] args) {
        boolean jdk = false;
        int low = 1902;
        int high = 2038;
        List idlist = new ArrayList();
        boolean all = false;
        String dir = null;
        String newLineMode = null;
        for (int i = 0; i < args.length; i++) {
            if (args[i].equals("-j")) {
                jdk = true;
            } else if (args[i].startsWith("-c")) {
                String val = args[i].substring(2);
                String[] years = val.split(",");
                if (years.length == 1) {
                    high = Integer.parseInt(years[0]);
                } else if (years.length == 2) {
                    low = Integer.parseInt(years[0]);
                    high = Integer.parseInt(years[1]);
                }
            } else if (args[i].equals("-a")) {
                all = true;
            } else if (args[i].startsWith("-d")) {
                dir = args[i].substring(2);
            } else if (args[i].startsWith("-l")) {
                newLineMode = args[i].substring(2);
            } else if (!args[i].startsWith("-")){
                idlist.add(args[i].trim());
            }
        }

        String lineSep = System.getProperty("line.separator");
        if (newLineMode != null && newLineMode.length() > 0) {
            if (newLineMode.equalsIgnoreCase("CR")) {
                lineSep = "\r";
            } else if (newLineMode.equalsIgnoreCase("LF")) {
                lineSep = "\n";
            } else if (newLineMode.equalsIgnoreCase("CRLF")) {
                lineSep = "\r\n";            
            }
        }
        
        String[] tzids = null;

        if (all) {
            if (jdk) {
                tzids = java.util.TimeZone.getAvailableIDs();
            } else {
                tzids = com.ibm.icu.util.TimeZone.getAvailableIDs();
            }

            // sort tzids
            TreeSet set = new TreeSet();
            for (int i = 0; i < tzids.length; i++) {
                set.add(tzids[i]);
            }
            Iterator it = set.iterator();
            int i = 0;
            while (it.hasNext()) {
                tzids[i++] = (String)it.next();
            }
        } else {
            int len = idlist.size();
            if (len == 0) {
                tzids = new String[1];
                tzids[0] = java.util.TimeZone.getDefault().getID();
            } else {
                tzids = new String[idlist.size()];
                idlist.toArray(tzids);
            }            
        }

        File dirfile = null;
        if (dir == null || dir.length() == 0) {
            PrintWriter pw = new PrintWriter(System.out);
            try {
                for (int i = 0; i < tzids.length; i++) {
                    if (i != 0) {
                        pw.println();
                    }
                    pw.write("ZONE: ");
                    pw.write(tzids[i]);
                    pw.println();
                    dumpZone(pw, lineSep, tzids[i], low, high, jdk);
                }
                pw.flush();
            } catch (IOException ioe) {
                ioe.printStackTrace();
            }
        } else {
            dirfile = new File(dir);
            dirfile.mkdirs();

            try {
                for (int i = 0; i < tzids.length; i++) {
                    FileOutputStream fos = new FileOutputStream(new File(dirfile, tzids[i].replace('/', '-')));
                    Writer w = new BufferedWriter(new OutputStreamWriter(fos));
                    dumpZone(w, lineSep, tzids[i], low, high, jdk);
                    w.close();
                }
            } catch (IOException ioe) {
                ioe.printStackTrace();
            }
        }
    }

    private static void dumpZone(Writer w, String lineSep, String tzid, int low, int high, boolean isJdk) throws IOException {
        ICUZDump dumper = new ICUZDump();
        Object tzimpl;
        if (isJdk) {
            tzimpl = java.util.TimeZone.getTimeZone(tzid);
        } else {
            tzimpl = com.ibm.icu.util.TimeZone.getTimeZone(tzid);
        }
        dumper.setTimeZone(tzimpl);
        dumper.setLowYear(low);
        dumper.setHighYear(high);
        dumper.setLineSeparator(lineSep);
        dumper.dump(w);
    }
}
