/*
 * Copyright (c) 2009, Takeyuki Nagao
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or
 * without modification, are permitted provided that the
 * following conditions are met:
 * 
 *  * Redistributions of source code must retain the above
 *    copyright notice, this list of conditions and the
 *    following disclaimer.
 *  * Redistributions in binary form must reproduce the above
 *    copyright notice, this list of conditions and the
 *    following disclaimer in the documentation and/or other
 *    materials provided with the distribution.
 *    
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
 * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
 * OF SUCH DAMAGE.
 */

package jp.sourceforge.dvibrowser.dvicore.util;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.URL;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicLong;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.CRC32;

import jp.sourceforge.dvibrowser.dvicore.DviException;


public class DviUtils {
  private static final Logger LOGGER = Logger.getLogger(DviUtils.class
      .getName());
  private static final String OS_NAME = System.getProperty("os.name");
  
  private static final UUID applicationInstanceUUID = UUID.randomUUID();
  private static final AtomicLong serializer = new AtomicLong();
  
  public static long generateSerialNumber()
  {
    return serializer.incrementAndGet();
  }
  
  public static UUID getApplicationInstanceUUID()
  {
    return applicationInstanceUUID;
  }
  
  public static String generateUniqueId()
  {
    String uniqueId = getApplicationInstanceUUID().toString() + "--" +  generateSerialNumber();
    return uniqueId;
  }
  
  private DviUtils() {}
  
  public static boolean isFile(URL url)
  {
    if (url == null) return false;
    return "file".equals(url.getProtocol());
  }

  public static void writeStringToFile(File file, String data)
  throws IOException
  {
    FileOutputStream fos = null;
    try {
      fos = new FileOutputStream(file);
      // TODO: support encodings
      byte[] buf = data.getBytes();
      fos.write(buf);
    } finally {
      if (fos != null) {
        fos.flush();
        silentClose(fos);
      }
    }
  }
  
  public static File toLocalFile(URL url)
  throws DviException
  {
    if (url == null) return null;
    if ("file".equals(url.getProtocol())) {
      return new File(url.getPath());
    } else {
      throw new DviException("URL does not point to a local file. " + url);
    }
  }
  
  public static String md5Hex(String ps)
  {
    if (ps == null) return null;
    try {
      MessageDigest md = MessageDigest.getInstance("MD5");
      // TODO: support character encodings.
      byte[] digest = md.digest(ps.getBytes());
      StringBuilder sb = new StringBuilder();
      for (byte b : digest) {
        sb.append(String.format("%02x", b));
      }
      return sb.toString();
    } catch (NoSuchAlgorithmException e) {
      e.printStackTrace();
    }
    return "0";
  }
  
  private static final Pattern patEnv = Pattern.compile("^([^=]*)=(.*)$");

  public static Map<String, String> getLoginShellEnvPosix() throws IOException, InterruptedException
  {
    return getLoginShellEnvPosix(null);
  }
  
  public static Map<String, String> getLoginShellEnvPosix(String shell)
      throws IOException, InterruptedException {
    String[] cmds = new String[] { "printenv" };
    if (shell != null) {
      cmds = new String[] { shell, "-l", "-c", "printenv" };
    }

    final Process proc = Runtime.getRuntime().exec(cmds);
    final Map<String, String> map = new HashMap<String, String>();
    final InputStream is = proc.getInputStream();
    proc.getOutputStream().close();
    proc.getErrorStream().close();
    {
      Scanner s = new Scanner(proc.getInputStream());
      try {
        while (s.hasNext()) {
          String line = s.nextLine();
          Matcher mat = patEnv.matcher(line);
          if (mat.matches()) {
            String key = mat.group(1);
            String value = mat.group(2);
            LOGGER.finest("key=" + key + " value=" + value);
            map.put(key, value);
          }
        }
      } finally {
        DviUtils.silentClose(is);
      }
    }
    proc.waitFor();
    return map;
  }
  
  public static String getPathByLoginShellPosix() throws IOException, InterruptedException
  {
    String path = null;
    
    // We first determine the user's shell.
    Map<String, String> env0 = getLoginShellEnvPosix(null);
    String shell = env0.get("SHELL");
    if (shell != null) {
      // We then read the PATH environment variable through the login shell.
      Map<String, String> env1 = getLoginShellEnvPosix(shell);
      path = env1.get("PATH");
    }
    
    return path;
  }
  
  public static String join(String left, String mid, String right, Object [] arg)
  {
    if (arg == null) return null;
    StringBuilder sb = new StringBuilder();
    left = wrapNull(left, "");
    mid = wrapNull(mid, "");
    right = wrapNull(right, "");
    String sep = left;
    for (Object o : arg) {
      sb.append(sep + String.valueOf(o));
      sep = mid;
    }
    sb.append(right);
    return sb.toString();
  }

  public static String join(String sep, Object [] args) 
  {
    return join(null, sep, null, args);
  }
  
  public static <T> T wrapNull(T value, T defaultValue) {
    if (value == null) return defaultValue;
    return value;
  }

  // This method is unsafe because we don't escape the command line arguments.  Use it with care.
  public static Process unsafeRunCommandByShell(String[] cmds, String[] envp, File workDir)
      throws IOException
  {
    Process p = null;
    if (!isWindows()) {
      try {
        Map<String, String> env = getLoginShellEnvPosix();
        String shell = env.get("SHELL");
        if (shell != null) {
          p = unsafeRunCommandByShellPosix(shell, true, cmds, envp, workDir);
        }
      } catch (InterruptedException e) {
        LOGGER.warning(e.toString());
      }
    }
    if (p == null) {
      p = Runtime.getRuntime().exec(cmds, envp, workDir);
    }
    return p;
  }
  
  // This method is unsafe because we don't escape the command line arguments.  Use it with care.
  public static Process unsafeRunCommandByShellPosix(String shell,
      boolean useLoginShell, String[] cmds, String[] envp, File workDir)
      throws IOException
  {
    ArrayList<String> list = new ArrayList<String>();
    list.add(shell);
    if (useLoginShell) {
      list.add("-l");
    }

    String[] cmdLine = list.toArray(new String[list.size()]);

    StringBuilder subCommand = new StringBuilder();
    for (String cmd : cmds) {
      subCommand.append(cmd + " ");
    }

    LOGGER.fine("running shell: " + join(" ", cmdLine));
    LOGGER.fine("running command: " + subCommand.toString());

    Process proc = Runtime.getRuntime().exec(cmdLine, envp, workDir);

    PrintWriter out = new PrintWriter(proc.getOutputStream());
    out.println(subCommand.toString());
    out.close();

    return proc;
  }

  public static Thread dumpStreamAsync(final String name, final InputStream is,
      final PrintStream out) {
    Thread t = new Thread(new Runnable() {
      public void run() {
        Scanner s = new Scanner(is);
        try {
          while (s.hasNext()) {
            String line = s.nextLine();
            if (out != null) {
              out.println("[" + name + "] " + line);
            }
            if (LOGGER.isLoggable(Level.FINER)) {
              LOGGER.finer("[" + name + "] " + line);
            }
          }
        } finally {
          try {
            s.close();
          } catch (Exception e) {
          }
        }

      }
    });
    t.start();
    return t;
  }
  
  public static <T> T head(T[] array) {
    if (array == null || array.length == 0) return null;
    return array[0];
  }
  
  public static void logLinesFromStream(final String name, final InputStream is,
      final Logger logger, final Level level) {
    if (logger == null || !logger.isLoggable(level))
      return;
    Scanner s = new Scanner(is);
    while (s.hasNext()) {
      String line = s.nextLine();
      LOGGER.log(level, "[" + name + "] " + line);
    }
  }
  
  public static void addLinesFromStream(final List<String> output, final InputStream is) {
    addLinesFromStream(output, is, null, Level.SEVERE, null);
  }
  
  public static void addLinesFromStream(final List<String> output, final InputStream is, final Logger logger, final Level level, final PrintStream out) {
    if (output == null) return;
    Scanner s = new Scanner(is);
    while (s.hasNext()) {
      String line = s.nextLine();
      output.add(line);
      if (logger != null && logger.isLoggable(level)) {
        logger.log(level, line);
      }
      if (out != null) {
        out.println(line);
        out.flush();
      }
    }
  }

  
  public static Thread dumpStreamAsync(final String name, final InputStream is,
      final Logger logger, final Level level) {
    Thread t = new Thread(new Runnable() {
      public void run() {
        try {
          logLinesFromStream(name, is, logger, level);
        } finally {
          silentClose(is);
        }
      }
    });
    t.start();
    return t;
  }


  public static boolean isMacOSX() {
    return OS_NAME.startsWith("Mac OS X");
  }

  public static boolean isWindows() {
    return OS_NAME.startsWith("Windows");
  }

  public static void silentClose(Closeable s) {
    if (s == null) return;
    try {
      s.close();
    } catch (Exception ex) {
      logStackTrace(LOGGER, Level.WARNING, ex);
    }
  }

  public static byte[] readFileToByteArray(File file) throws IOException {
    if (file == null)
      return null;
    int len = (int) file.length();
    FileInputStream is = new FileInputStream(file);
    try {
      byte[] buf = new byte[len];
      if (len != is.read(buf)) {
        throw new IOException("Failed to read file content");
      }
      return buf;
    } finally {
      silentClose(is);
    }
  }

  public static String readFileToString(File file, String encoding)
      throws IOException {
    byte[] buf = readFileToByteArray(file);
    if (buf != null) {
      return new String(buf, encoding);
    }

    return null;
  }

  public static String readFileToString(File file) throws IOException {
    byte[] buf = readFileToByteArray(file);
    if (buf != null) {
      return new String(buf);
    }

    return null;
  }
  
  public static String [] readLinesFromFile(File file)
  throws IOException
  {
    FileInputStream is = new FileInputStream(file);
    return readLinesFromStream(is);
  }
  
  public static String [] readLinesFromStream(InputStream is)
  throws IOException
  {
    ArrayList<String> list = new ArrayList<String>();
    Scanner s = new Scanner(is);
    try {
      while (s.hasNext()) {
        String line = s.nextLine();
        list.add(line);
      }
      return list.toArray(new String[list.size()]);
    } finally {
      silentClose(is);
    }
  }


  public static String [] removeComments(String [] list, String lineCommentStart)
  {
    if (list == null) return list;

    ArrayList<String> out = new ArrayList<String>();
    for (String item : list) {
      int pos = item.indexOf(lineCommentStart);
      String line = item;
      if (pos != -1) {
        line = line.substring(0, pos);
      }
      line = line.trim();
      if (line.length() > 0) {
        out.add(line);
      }
    }
    return out.toArray(new String[out.size()]);
  }
  
  public static String joinPath(String [] args) {
    if (args == null) return null;
    return join(File.pathSeparator, args);
  }
  
  public static void logStackTrace(Logger logger, Level level, Throwable t)
  {
    StringWriter sw = new StringWriter();
    PrintWriter pw = new PrintWriter(sw);
    pw.println(t.toString());
    for (StackTraceElement e : t.getStackTrace()) {
      pw.println
        ( "at " + e.getClassName()
        + "." + e.getMethodName()
        + "(" + e.getFileName()
        + ":" + e.getLineNumber()
        + ")"
        );
    }
    logger.log(level, sw.toString());
  }

  public static File toFile(URL url) {
    if (url == null) return null;
    if (!isFile(url)) {
      throw new IllegalArgumentException("URL does not point to a local file: " + url);
    }
    return new File(url.getPath());
  }
  
  public static boolean nullSafeEquals(Object o1, Object o2)
  {
    if (o1 == null) return o2 == null;
    return o1.equals(o2);
  }

  public static String join(String sep, Collection<String> list) {
    if (list == null) return null;
    return join(sep, list.toArray(new String[list.size()]));
  }

  public static void accumulateToLineBuffer(InputStream in,
      LineBuffer<String> out) {
    try {
      Scanner s = new Scanner(in);
      while (s.hasNext()) {
        String line = s.nextLine();
        out.append(line);
      }
    } finally {
      DviUtils.silentClose(in);
    }
  }

  public static String getTimeId() {
    Calendar cal = Calendar.getInstance();
    SimpleDateFormat df = new SimpleDateFormat("yyyyMMdd-HHmmss");
    return df.format(cal.getTime());
  }

  public static CRC32 getCRC32(InputStream is) throws IOException
  {
    try {
      byte [] buf = new byte [1024];
      int len;
      CRC32 crc = new CRC32();
      while (-1 != (len = is.read(buf))) {
        crc.update(buf, 0, len);
      }
      return crc;
    } finally {
      DviUtils.silentClose(is);
    }
  }
  
  public static void copyStream(InputStream is, OutputStream os) throws IOException
  {
    try {
      byte [] buf = new byte [1024];
      int len;
      while (-1 != (len = is.read(buf))) {
        os.write(buf, 0, len);
      }
    } finally {
      // We don't close os.  This is intended for use with ZipOutputStream.
      DviUtils.silentClose(is);
    }
  }
  
  public String getZipPath(File file)
  {
    if (file == null) return null;
    return file.getPath().replace('\\', '/');
  }

  public static String hexDump(String unicode) {
    byte [] bytes = unicode.getBytes();
    StringBuilder sb = new StringBuilder();
    for (byte b : bytes) {
      sb.append(String.format("%02x", b));
    }
    return sb.toString();
  }
}
