/*
 * Copyright 2011 Christian Thiemann <christian@spato.net>
 * Developed at Northwestern University <http://rocs.northwestern.edu>
 *
 * This file is part of the deployment infrastructure for
 * SPaTo Visual Explorer (SPaTo).
 *
 * SPaTo is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * SPaTo is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with SPaTo.  If not, see <http://www.gnu.org/licenses/>.
 */

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.PrintWriter;
import java.security.KeyStore;
import java.security.MessageDigest;
import java.security.PrivateKey;
import java.security.Signature;
import javax.swing.JOptionPane;
import javax.swing.JPasswordField;

import net.spato.sve.app.SPaTo_Visual_Explorer;
import net.spato.sve.app.util.Base64;
import processing.xml.XMLElement;


public class SPaTo_Update_Builder {

  private static String fixPath(String path) {
    return path.replace('/', File.separatorChar).replace('\\', File.separatorChar);
  }

  private static PrivateKey getPrivateKey(String keystore, String alias, String password) throws Exception {
    char pass[] = null;
    // get password
    if ((password != null) && (password.length() > 0) && !password.equals("${private.key.password}"))
      pass = password.toCharArray();
    else {
      JPasswordField pw = new JPasswordField(30);
      int res = JOptionPane.showConfirmDialog(null, pw, "Enter private key password", JOptionPane.OK_CANCEL_OPTION);
      pass = pw.getPassword();
      if ((res != JOptionPane.OK_OPTION) || (pass == null) || (pass.length == 0))
        throw new Exception("No password provided!");
    }
    // get key
    FileInputStream fis = new FileInputStream(keystore);
    KeyStore store = KeyStore.getInstance("JKS");
    store.load(fis, pass);
    fis.close();
    return (PrivateKey)store.getKey(alias, pass);
  }

  private static XMLElement getReleaseInfo(String releaseNotes) throws Exception {
    XMLElement xmlRelease = loadXML(releaseNotes).getChild(0);
    while (xmlRelease.getChildCount() > 0) xmlRelease.removeChild(0);  // purge the actual release notes
    System.out.println(xmlRelease);
    if (!xmlRelease.getString("version").equals(SPaTo_Visual_Explorer.VERSION))
      throw new Exception("Version mismatch: " + xmlRelease.getString("version") + " != " + SPaTo_Visual_Explorer.VERSION);
    if (!xmlRelease.getString("debug").equals(SPaTo_Visual_Explorer.VERSION_DEBUG))
      throw new Exception("Version debug mismatch: " + xmlRelease.getString("debug") + " != " + SPaTo_Visual_Explorer.VERSION_DEBUG);
    if (!xmlRelease.getString("date").equals(SPaTo_Visual_Explorer.VERSION_TIMESTAMP))
      throw new Exception("Version date mismatch: " + xmlRelease.getString("date") + " != " + SPaTo_Visual_Explorer.VERSION_TIMESTAMP);
    return xmlRelease;
  }

  private static XMLElement loadXML(String filename) throws Exception {
    FileReader reader = new FileReader(filename);
    XMLElement xml = XMLElement.parse(reader);
    reader.close();
    if (xml == null) throw new Exception("XMLElement.parse returned null for " + filename);
    return xml;
  }

  private static void saveXML(XMLElement xml, String filename) throws Exception {
    PrintWriter writer = new PrintWriter(filename);
    if (!xml.write(writer)) throw new Exception("XMLElement.write returned false writing to " + filename);
    writer.close();
  }

  public static String serializeDigest(byte digest[]) {
    return String.format("%032x", new java.math.BigInteger(1, digest));
  }

  public static XMLElement serializeSignature(byte signature[]) {
    XMLElement xmlSignature = new XMLElement("signature");
    String sigStr = "";
    for (String line : Base64.encode(signature, 43).split("\r\n"))
      sigStr += "      " + line + "\n";
    xmlSignature.setContent("\n" + sigStr + "    ");
    return xmlSignature;
  }

  public static void processIndex(String filename, String srcdir, String dstdir,
                                  XMLElement xmlRelease, PrivateKey privKey) throws Exception {
    // load index and add release info
    XMLElement xmlIndex = loadXML(filename);
    xmlIndex.insertChild(xmlRelease, 0);
    System.out.println("Processing " + xmlIndex.getString("index") + " (" + xmlIndex.getChildren("file").length + " files)");
    // set up crypto-stuff
    byte buf[] = new byte[8*1024];
    MessageDigest md5 = MessageDigest.getInstance("md5");
    Signature sig = Signature.getInstance("MD5withRSA");
    sig.initSign(privKey);
    // copy files to update directory (dstdir) and add MD5 checksums and signatures to index
    for (XMLElement xmlFile : xmlIndex.getChildren("file")) {
      File fsrc = new File(srcdir, fixPath(xmlFile.getChild("local").getString("path")));
      File fdst = new File(dstdir, fixPath(xmlFile.getChild("remote").getString("path")));
      fdst.getParentFile().mkdirs();  // make sure the destination directory exists
      // feed the file contents through MD5 and RSA while copying...
      FileInputStream fis = new FileInputStream(fsrc);
      FileOutputStream fos = new FileOutputStream(fdst);
      int n, size = 0;
      while ((n = fis.read(buf)) > 0) {
        size += n;
        md5.update(buf, 0, n);
        sig.update(buf, 0, n);
        fos.write(buf, 0, n);
      }
      fis.close(); fos.close();
      // copy last-modified timestamp
      fdst.setLastModified(fsrc.lastModified());
      // add file size, digest and signature to the index
      xmlFile.getChild("remote").setString("size", "" + size);
      xmlFile.getChild("remote").setString("md5", serializeDigest(md5.digest()));
      xmlFile.addChild(serializeSignature(sig.sign()));
    }
    // save index file
    saveXML(xmlIndex, dstdir + File.separator + xmlIndex.getString("index"));
  }

  public static void main(String args[]) {
    try {
      // load private key and release info
      PrivateKey privKey = getPrivateKey(fixPath("deploy/keystore"), "spato.update", (args.length > 0) ? args[0] : null);
      XMLElement xmlRelease = getReleaseInfo(fixPath("docs/release-notes/RELEASE_NOTES.xml"));
      // copy files into place and add crypto-stuff to indices
      String dstdir = fixPath("build/update");
      processIndex(fixPath("deploy/INDEX.linux"), fixPath("build/linux/SPaTo Visual Explorer"), dstdir, xmlRelease, privKey);
      processIndex(fixPath("deploy/INDEX.macosx"), fixPath("build/macosx/SPaTo Visual Explorer.app"), dstdir, xmlRelease, privKey);
      processIndex(fixPath("deploy/INDEX.windows"), fixPath("build/windows/SPaTo Visual Explorer"), dstdir, xmlRelease, privKey);
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

}
