
import java.io.*;
import java.util.*;

// usage: java ZoneCompiler <setup file> <data directory> <output directory> <tzdata version>
//
// Compile a set of tzfile-formatted files into a single file containing an index.
//
// The compilation is controlled by a setup file, which is provided as a
// command-line argument.  The setup file has the form:
//
// Link <toName> <fromName>
// ...
// <zone filename>
// ...
//
// Note that the links must be declared prior to the zone names.
// A zone name is a filename relative to the source directory such as
// 'GMT', 'Africa/Dakar', or 'America/Argentina/Jujuy'.
//
// Use the 'zic' command-line tool to convert from flat files
// (such as 'africa' or 'northamerica') to a directory
// hierarchy suitable for this tool (containing files such as 'data/Africa/Abidjan').
//

public class ZoneCompactor {
  // Maximum number of characters in a zone name, including '\0' terminator.
  private static final int MAXNAME = 40;

  // Zone name synonyms.
  private Map<String,String> links = new HashMap<String,String>();

  // File offsets by zone name.
  private Map<String,Integer> offsets = new HashMap<String,Integer>();

  // File lengths by zone name.
  private Map<String,Integer> lengths = new HashMap<String,Integer>();

  // Concatenate the contents of 'inFile' onto 'out'.
  private static void copyFile(File inFile, OutputStream out) throws Exception {
    byte[] ret = new byte[0];

    InputStream in = new FileInputStream(inFile);
    byte[] buf = new byte[8192];
    while (true) {
      int nbytes = in.read(buf);
      if (nbytes == -1) {
        break;
      }
      out.write(buf, 0, nbytes);

      byte[] nret = new byte[ret.length + nbytes];
      System.arraycopy(ret, 0, nret, 0, ret.length);
      System.arraycopy(buf, 0, nret, ret.length, nbytes);
      ret = nret;
    }
    out.flush();
  }

  public ZoneCompactor(String setupFile, String dataDirectory, String zoneTabFile, String outputDirectory, String version) throws Exception {
    // Read the setup file and concatenate all the data.
    ByteArrayOutputStream allData = new ByteArrayOutputStream();
    BufferedReader reader = new BufferedReader(new FileReader(setupFile));
    String s;
    int offset = 0;
    while ((s = reader.readLine()) != null) {
      s = s.trim();
      if (s.startsWith("Link")) {
        StringTokenizer st = new StringTokenizer(s);
        st.nextToken();
        String to = st.nextToken();
        String from = st.nextToken();
        links.put(from, to);
      } else {
        String link = links.get(s);
        if (link == null) {
          File sourceFile = new File(dataDirectory, s);
          long length = sourceFile.length();
          offsets.put(s, offset);
          lengths.put(s, (int) length);

          offset += length;
          copyFile(sourceFile, allData);
        }
      }
    }
    reader.close();

    // Fill in fields for links.
    Iterator<String> it = links.keySet().iterator();
    while (it.hasNext()) {
      String from = it.next();
      String to = links.get(from);

      offsets.put(from, offsets.get(to));
      lengths.put(from, lengths.get(to));
    }

    // Create/truncate the destination file.
    RandomAccessFile f = new RandomAccessFile(new File(outputDirectory, "tzdata"), "rw");
    f.setLength(0);

    // Write the header.

    // byte[12] tzdata_version -- 'tzdata2012f\0'
    // int index_offset -- so we can slip in extra header fields in a backwards-compatible way
    // int data_offset
    // int zonetab_offset

    // tzdata_version
    f.write(toAscii(new byte[12], version));

    // Write dummy values for the three offsets, and remember where we need to seek back to later
    // when we have the real values.
    int index_offset_offset = (int) f.getFilePointer();
    f.writeInt(0);
    int data_offset_offset = (int) f.getFilePointer();
    f.writeInt(0);
    int zonetab_offset_offset = (int) f.getFilePointer();
    f.writeInt(0);

    int index_offset = (int) f.getFilePointer();

    // Write the index.
    ArrayList<String> sortedOlsonIds = new ArrayList<String>();
    sortedOlsonIds.addAll(offsets.keySet());
    Collections.sort(sortedOlsonIds);
    it = sortedOlsonIds.iterator();
    while (it.hasNext()) {
      String zoneName = it.next();
      if (zoneName.length() >= MAXNAME) {
        throw new RuntimeException("zone filename too long: " + zoneName.length());
      }

      f.write(toAscii(new byte[MAXNAME], zoneName));
      f.writeInt(offsets.get(zoneName));
      f.writeInt(lengths.get(zoneName));
      f.writeInt(0); // Used to be raw GMT offset. No longer used.
    }

    int data_offset = (int) f.getFilePointer();

    // Write the data.
    f.write(allData.toByteArray());

    int zonetab_offset = (int) f.getFilePointer();

    // Copy the zone.tab.
    reader = new BufferedReader(new FileReader(zoneTabFile));
    while ((s = reader.readLine()) != null) {
      if (!s.startsWith("#")) {
        f.writeBytes(s);
        f.write('\n');
      }
    }
    reader.close();

    // Go back and fix up the offsets in the header.
    f.seek(index_offset_offset);
    f.writeInt(index_offset);
    f.seek(data_offset_offset);
    f.writeInt(data_offset);
    f.seek(zonetab_offset_offset);
    f.writeInt(zonetab_offset);

    f.close();
  }

  private static byte[] toAscii(byte[] dst, String src) {
    for (int i = 0; i < src.length(); ++i) {
      if (src.charAt(i) > '~') {
        throw new RuntimeException("non-ASCII string: " + src);
      }
      dst[i] = (byte) src.charAt(i);
    }
    return dst;
  }

  public static void main(String[] args) throws Exception {
    if (args.length != 5) {
      System.err.println("usage: java ZoneCompactor <setup file> <data directory> <zone.tab file> <output directory> <tzdata version>");
      System.exit(0);
    }
    new ZoneCompactor(args[0], args[1], args[2], args[3], args[4]);
  }
}
