import com.google.common.base.Join;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;

import java.io.*;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.Properties;

/**
 * The Java, user and machine environment of a test run.
 *
 * @author jessewilson
 */
public final class HostInfo implements Serializable {
  private final Map<String, String> properties;

  /**
   * Use {@link Builder} to create instances.
   */
  HostInfo(Map<String, String> properties) {
    this.properties = Collections.unmodifiableMap(properties);
  }

  /**
   * Returns a Map containing environment field names and their values.
   */
  public Map<String, String> getMap() {
    return properties;
  }

  @Override public String toString() {
    return properties.toString();
  }

  public static void main(String[] args) {
    HostInfo hostInfo = new Builder().build();
    for (Map.Entry<String, String> entry : hostInfo.getMap().entrySet()) {
      System.out.println(entry.getKey() + ": " + entry.getValue());
    }
  }

  public static class Builder {
    private boolean includeJava = true;
    private boolean includeUsername = true;
    private boolean includeHardware = true;
    private final Map<String, String> environment = Maps.newLinkedHashMap();

    public static Builder forMap(Map<String, String> map) {
      Builder result = new Builder();
      result.environment.putAll(map);
      result.includeJava = false;
      result.includeUsername = false;
      result.includeHardware = false;
      return result;
    }

    public Builder includeJava(boolean includeJava) {
      this.includeJava = includeJava;
      return this;
    }

    public Builder includeUsername(boolean includeUsername) {
      this.includeUsername = includeUsername;
      return this;
    }

    public Builder includeHardware(boolean includeHardware) {
      this.includeHardware = includeHardware;
      return this;
    }

    public HostInfo build() {
      if (includeJava) {
        environment.putAll(getJavaEnvironment());
      }
      if (includeUsername) {
        environment.putAll(getUserEnvironment());
      }
      if (includeHardware) {
        environment.putAll(getHardwareEnvironment());
      }

      return new HostInfo(environment);
    }

    Map<String, String> getHardwareEnvironment() {
      if ("Linux".equals(System.getProperty("os.name"))) {
        return getLinuxEnvironment();

      } else if ("Mac OS X".equals(System.getProperty("os.name"))) {
        return getMacOsXEnvironment();

      } else {
        // TODO(jessewilson): figure out cpu counts etc. for other operating systems
        return Maps.immutableMap();
      }
    }

    Map<String, String> getJavaEnvironment() {
      Properties systemProperties = System.getProperties();

      Map<String, String> result = Maps.newLinkedHashMap();
      result.put("java.version", systemProperties.getProperty("java.version"));
      result.put("java.vm.name", systemProperties.getProperty("java.vm.name"));
      result.put("os.arch", systemProperties.getProperty("os.arch"));
      result.put("os.name", systemProperties.getProperty("os.name"));
      result.put("os.version", systemProperties.getProperty("os.version"));
      return result;
    }

    Map<String, String> getUserEnvironment() {
      Map<String, String> result = Maps.newLinkedHashMap();
      result.put("user.name", System.getProperty("user.name"));

      try {
        result.put("host.name", InetAddress.getLocalHost().getHostName());
      } catch (UnknownHostException ignored) {
      }

      return result;
    }

    /**
     * Returns environment properties on a Linux machine.
     */
    Map<String, String>  getLinuxEnvironment() {
      Map<String, String> result = Maps.newLinkedHashMap();

      Multimap<String, String> cpuInfo = propertiesFromCommand("/bin/cat", "/proc/cpuinfo");
      result.put("host.cpus", "" + cpuInfo.get("processor").size());
      result.put("host.cpu.cores", collectionToString(cpuInfo.get("cpu cores")));
      result.put("host.cpu.speeds", collectionToString(cpuInfo.get("cpu MHz")));
      result.put("host.cpu.names", collectionToString(cpuInfo.get("model name")));
      result.put("host.cpu.caches", collectionToString(cpuInfo.get("cache size")));

      Multimap<String, String> memInfo = propertiesFromCommand("/bin/cat", "/proc/meminfo");
      result.put("host.memory.physical", collectionToString(memInfo.get("MemTotal")));
      result.put("host.memory.swap", collectionToString(memInfo.get("SwapTotal")));

      return result;
    }

    Map<String, String> getMacOsXEnvironment() {
      Map<String, String> result = Maps.newLinkedHashMap();

      Multimap<String, String> sysctl = propertiesFromCommand("/usr/sbin/sysctl", "-a");
      result.put("os.kernel.version", collectionToString(sysctl.get("kern.osrelease")));
      result.put("host.model", collectionToString(sysctl.get("hw.model")));
      result.put("host.cpus", collectionToString(sysctl.get("hw.ncpu")));
      result.put("host.cpu.speeds", collectionToString(sysctl.get("hw.cpufrequency")));
      result.put("host.cpu.arch", collectionToString(sysctl.get("hw.machine")));
      result.put("host.cpu.bits.physical",
          collectionToString(sysctl.get("machdep.cpu.address_bits.physical")));
      result.put("host.cpu.names", collectionToString(sysctl.get("machdep.cpu.brand_string")));
      result.put("host.cpu.caches.l2", collectionToString(sysctl.get("hw.l2cachesize")));
      result.put("host.memory.physical", collectionToString(sysctl.get("hw.physmem")));

      return result;
    }

    /**
     * Returns the key/value pairs from the specified properties-file like
     * reader. Unlike standard Java properties files, {@code reader} is allowed
     * to list the same property multiple times. Comments etc. are unsupported.
     */
    Multimap<String, String> propertiesFileToMultimap(Reader reader)
        throws IOException {
      Multimap<String, String> result = Multimaps.newLinkedHashMultimap();
      BufferedReader in = new BufferedReader(reader);

      String line;
      while((line = in.readLine()) != null) {
        String[] parts = line.split("\\s*[\\:\\=]\\s*", 2);
        if (parts.length == 2) {
          result.put(parts[0], parts[1]);
        }
      }

      return result;
    }

    Multimap<String, String> propertiesFromCommand(String... command) {
      try {
        Process process = Runtime.getRuntime().exec(command);
        return propertiesFileToMultimap(
            new InputStreamReader(process.getInputStream(), "ISO-8859-1"));
      } catch (IOException e) {
        return Multimaps.immutableMultimap();
      }
    }

    String collectionToString(Collection<String> collection) {
      if (collection.isEmpty()) {
        return "";
      } else if (collection.size() == 1) {
        return Iterables.getOnlyElement(collection);
      } else {
        return Join.join(", ", collection);
      }
    }
  }
}

