/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.ratis.util;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.EnumMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.function.BiConsumer;
import java.util.function.Consumer;

/**
 * This class is for the resource generated by hadoop-maven-plugins:version-info.
 * <p>
 * This class is immutable.
 */
public final class VersionInfo {
  static final Logger LOG = LoggerFactory.getLogger(VersionInfo.class);

  private static final String RATIS_VERSION_PROPERTIES = "ratis-version.properties";
  private static final String UNKNOWN = "<unknown>";
  private static final String FORMAT = "  %20s: %s";

  private enum SoftwareInfo {
    // the ordering is the output ordering
    NAME, VERSION, REVISION;

    static SoftwareInfo parse(String key) {
      for (SoftwareInfo info : SoftwareInfo.values()) {
        if (info.name().toLowerCase().equals(key)) {
          return info;
        }
      }
      return null;
    }
  }

  private enum RuntimeInfo {
    // the ordering is the output ordering
    JAVA, USER, MEMORY;

    static final InfoMap<RuntimeInfo> MAP;

    static {
      final EnumMap<RuntimeInfo, String> map = new EnumMap<>(RuntimeInfo.class);
      final Properties properties = System.getProperties();
      map.put(JAVA, properties.getProperty("java.vm.name") + " " + properties.getProperty("java.runtime.version"));
      final Runtime r = Runtime.getRuntime();
      map.put(MEMORY, "max: " + TraditionalBinaryPrefix.long2String(r.maxMemory())
          + ", total: " + TraditionalBinaryPrefix.long2String(r.totalMemory())
          + ", free: " + TraditionalBinaryPrefix.long2String(r.freeMemory()));
      map.put(USER, properties.getProperty("user.name"));
      MAP = new InfoMap<>(map);
    }
  }

  private static class InfoMap<INFO extends Enum<INFO>> {
    private final Map<INFO, String> map;

    InfoMap(EnumMap<INFO, String> map) {
      this.map = Collections.unmodifiableMap(map);
    }

    String getOrDefault(INFO info) {
      return map.getOrDefault(info, UNKNOWN);
    }

    String format(INFO info) {
      return String.format(FORMAT, info.name().toLowerCase(), getOrDefault(info));
    }
  }

  public static VersionInfo load(Class<?> clazz) {
    final Properties properties = new Properties();

    try (InputStream in = clazz.getClassLoader().getResourceAsStream(RATIS_VERSION_PROPERTIES)) {
      if (in != null) {
        properties.load(in);
      } else {
        LOG.warn("Resource '{}' not found for {}", RATIS_VERSION_PROPERTIES, clazz);
      }
    } catch (IOException e) {
      LOG.warn("Failed to load resource '{}' for {}", RATIS_VERSION_PROPERTIES, clazz, e);
    }
    return new VersionInfo(clazz, properties);
  }

  private final Class<?> clazz;
  private final InfoMap<RuntimeInfo> runtimeInfos = RuntimeInfo.MAP;
  private final InfoMap<SoftwareInfo> softwareInfos;
  private final Map<String, String> otherInfos;

  private VersionInfo(Class<?> clazz, Properties properties) {
    this.clazz = Objects.requireNonNull(clazz, "clazz == null");

    final EnumMap<SoftwareInfo, String> softwareInfoMap = new EnumMap<>(SoftwareInfo.class);
    final Map<String, String> others = new LinkedHashMap<>(); // preserve insertion order
    for (Map.Entry<Object, Object> e : properties.entrySet()) {
      final String key = e.getKey().toString();
      final String value = e.getValue().toString();
      final SoftwareInfo k = SoftwareInfo.parse(key);
      if (k != null) {
        softwareInfoMap.put(k, value);
      } else {
        others.put(key, value);
      }
    }

    this.softwareInfos = new InfoMap<>(softwareInfoMap);
    this.otherInfos = Collections.unmodifiableMap(others);
  }

  public void printStartupMessages(Object name, Consumer<String> log) {
    Objects.requireNonNull(name, "name == null");
    log.accept(String.format("Starting %s -- %s %s",
        softwareInfos.getOrDefault(SoftwareInfo.NAME), clazz.getSimpleName(), name));
    final SoftwareInfo[] softwareInfoValues = SoftwareInfo.values();
    for(int i = 1; i < softwareInfoValues.length; i++) {
      log.accept(softwareInfos.format(softwareInfoValues[i]));
    }
    for(RuntimeInfo runtimeInfo : RuntimeInfo.values()) {
      log.accept(runtimeInfos.format(runtimeInfo));
    }
    for (Map.Entry<String, String> e : otherInfos.entrySet()) {
      log.accept(String.format(FORMAT, e.getKey(), e.getValue()));
    }
  }

  static void printSystemProperties(BiConsumer<String, Object> out) {
    final SortedMap<String, Object> sortedMap = new TreeMap<>();
    for(Map.Entry<Object, Object> e : System.getProperties().entrySet()) {
      sortedMap.put(e.getKey().toString(), e.getValue());
    }
    sortedMap.forEach(out);
  }

  /**
   * Get the current ratis version.
   * @return the current ratis version string.
   */
  public static String getSoftwareInfoVersion() {
    return VersionInfo.load(VersionInfo.class).softwareInfos.getOrDefault(SoftwareInfo.VERSION);
  }

  public static void main(String[] args) {
    printSystemProperties((key, value) -> System.out.printf("%-40s = %s%n", key, value));

    VersionInfo.load(VersionInfo.class).printStartupMessages(":", System.out::println);
  }
}
