/*
 * Copyright ConsenSys AG.
 *
 * Licensed 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.
 *
 * SPDX-License-Identifier: Apache-2.0
 */
package org.hyperledger.besu.util.platform;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.ptr.IntByReference;
import com.sun.jna.ptr.PointerByReference;

/**
 * Detects OS and VMs.
 *
 * <p>Derived from Detector.java https://github.com/trustin/os-maven-plugin/ version 59fd029 on 21
 * Apr 2018, Copyright 2014 Trustin Heuiseung Lee.
 */
public class PlatformDetector {

  private static String _os;
  private static String _osType;
  private static String _vm;
  private static String _arch;
  private static String _glibc;
  private static String _jemalloc;

  /**
   * Gets OS type.
   *
   * @return the OS type
   */
  public static String getOSType() {
    if (_osType == null) {
      detect();
    }
    return _osType;
  }

  /**
   * Gets OS.
   *
   * @return the OS
   */
  public static String getOS() {
    if (_os == null) {
      detect();
    }
    return _os;
  }

  /**
   * Gets Arch.
   *
   * @return the Arch
   */
  public static String getArch() {
    if (_arch == null) {
      detect();
    }
    return _arch;
  }

  /**
   * Gets VM.
   *
   * @return the VM
   */
  public static String getVM() {
    if (_vm == null) {
      detect();
    }
    return _vm;
  }

  /**
   * Gets Glibc version.
   *
   * @return the Glibc version
   */
  public static String getGlibc() {
    if (_glibc == null) {
      detectGlibc();
    }

    return _glibc;
  }

  /**
   * Gets jemalloc version.
   *
   * @throws UnsatisfiedLinkError if the library cannot be found or dependent libraries are missing.
   * @return the jemalloc version
   */
  public static String getJemalloc() {
    if (_jemalloc == null) {
      detectJemalloc();
    }

    return _jemalloc;
  }

  private static final String UNKNOWN = "unknown";

  private static void detect() {
    final String detectedOS = normalizeOS(normalize("os.name"));
    final String detectedArch = normalizeArch(normalize("os.arch"));
    final String detectedVM = normalizeVM(normalize("java.vendor"), normalize("java.vm.name"));
    final String detectedJavaVersion = normalizeJavaVersion("java.specification.version");

    _os = detectedOS + '-' + detectedArch;
    _arch = detectedArch;
    _osType = detectedOS;
    _vm = detectedVM + "-java-" + detectedJavaVersion;
  }

  private static String normalizeOS(final String osName) {
    if (osName.startsWith("aix")) {
      return "aix";
    }
    if (osName.startsWith("hpux")) {
      return "hpux";
    }
    if (osName.startsWith("os400")) {
      // Avoid the names such as os4000
      if (osName.length() <= 5 || !Character.isDigit(osName.charAt(5))) {
        return "os400";
      }
    }
    if (osName.startsWith("linux")) {
      return "linux";
    }
    if (osName.startsWith("macosx") || osName.startsWith("osx")) {
      return "osx";
    }
    if (osName.startsWith("freebsd")) {
      return "freebsd";
    }
    if (osName.startsWith("openbsd")) {
      return "openbsd";
    }
    if (osName.startsWith("netbsd")) {
      return "netbsd";
    }
    if (osName.startsWith("solaris") || osName.startsWith("sunos")) {
      return "sunos";
    }
    if (osName.startsWith("windows")) {
      return "windows";
    }

    return UNKNOWN;
  }

  private static String normalizeArch(final String osArch) {
    if (osArch.matches("^(x8664|amd64|ia32e|em64t|x64)$")) {
      return "x86_64";
    }
    if (osArch.matches("^(x8632|x86|i[3-6]86|ia32|x32)$")) {
      return "x86_32";
    }
    if (osArch.matches("^(ia64w?|itanium64)$")) {
      return "itanium_64";
    }
    if ("ia64n".equals(osArch)) {
      return "itanium_32";
    }
    if (osArch.matches("^(sparc|sparc32)$")) {
      return "sparc_32";
    }
    if (osArch.matches("^(sparcv9|sparc64)$")) {
      return "sparc_64";
    }
    if (osArch.matches("^(arm|arm32)$")) {
      return "arm_32";
    }
    if ("aarch64".equals(osArch)) {
      return "aarch_64";
    }
    if (osArch.matches("^(mips|mips32)$")) {
      return "mips_32";
    }
    if (osArch.matches("^(mipsel|mips32el)$")) {
      return "mipsel_32";
    }
    if ("mips64".equals(osArch)) {
      return "mips_64";
    }
    if ("mips64el".equals(osArch)) {
      return "mipsel_64";
    }
    if (osArch.matches("^(ppc|ppc32)$")) {
      return "ppc_32";
    }
    if (osArch.matches("^(ppcle|ppc32le)$")) {
      return "ppcle_32";
    }
    if ("ppc64".equals(osArch)) {
      return "ppc_64";
    }
    if ("ppc64le".equals(osArch)) {
      return "ppcle_64";
    }
    if ("s390".equals(osArch)) {
      return "s390_32";
    }
    if ("s390x".equals(osArch)) {
      return "s390_64";
    }

    return UNKNOWN;
  }

  /**
   * Normalize VM version string.
   *
   * @param javaVendor the java vendor
   * @param javaVmName the java vm name
   * @return the string
   */
  static String normalizeVM(final String javaVendor, final String javaVmName) {
    if (javaVmName.contains("graalvm") || javaVendor.contains("graalvm")) {
      return "graalvm";
    }
    if (javaVendor.contains("oracle")) {
      if (javaVmName.contains("openjdk")) {
        return "oracle_openjdk";
      } else {
        return "oracle";
      }
    }
    if (javaVendor.contains("adoptopenjdk")) {
      return "adoptopenjdk";
    }
    if (javaVendor.contains("openj9")) {
      return "openj9";
    }
    if (javaVendor.contains("azul")) {
      if (javaVmName.contains("zing")) {
        return "zing";
      } else {
        return "zulu";
      }
    }
    if (javaVendor.contains("amazoncominc")) {
      return "corretto";
    }
    if (javaVmName.contains("openjdk")) {
      return "openjdk";
    }

    return "-" + javaVendor + "-" + javaVmName;
  }

  /**
   * Normalize java version string.
   *
   * @param javaVersion the java version
   * @return the string
   */
  static String normalizeJavaVersion(final String javaVersion) {
    // These are already normalized.
    return System.getProperty(javaVersion);
  }

  private static String normalize(final String value) {
    if (value == null) {
      return "";
    }
    return System.getProperty(value).toLowerCase(Locale.US).replaceAll("[^a-z0-9]+", "");
  }

  private static void detectGlibc() {
    final ProcessBuilder processBuilder =
        new ProcessBuilder("/bin/bash").command("/usr/bin/ldd", "--version");
    processBuilder.redirectErrorStream(true);

    final StringBuilder rawGlibcVersionBuilder;
    try {
      final Process process = processBuilder.start();
      rawGlibcVersionBuilder = readGlibcVersionStream(process.getInputStream());
    } catch (IOException e) {
      return;
    }

    _glibc = normalizeGLibcVersion(rawGlibcVersionBuilder.toString());
  }

  private static StringBuilder readGlibcVersionStream(final InputStream iStream)
      throws IOException {
    final StringBuilder builder = new StringBuilder();
    String line;

    try (BufferedReader bufferedReader =
        new BufferedReader(new InputStreamReader(iStream, Charset.defaultCharset()))) {
      while ((line = bufferedReader.readLine()) != null) {
        builder.append(line);
        builder.append(System.getProperty("line.separator"));
      }
    }

    return builder;
  }

  private static String normalizeGLibcVersion(final String rawGlibcVersion) {
    final Pattern pattern = Pattern.compile("[-+]?[0-9]*\\.?[0-9]+");
    final Matcher matcher = pattern.matcher(rawGlibcVersion);

    return matcher.find() ? matcher.group() : null;
  }

  private static void detectJemalloc() {
    interface JemallocLib extends Library {
      int mallctl(
          String property,
          PointerByReference value,
          IntByReference len,
          String newValue,
          int newLen);
    }

    final JemallocLib jemallocLib = Native.load("jemalloc", JemallocLib.class);

    PointerByReference pVersion = new PointerByReference();
    IntByReference pSize = new IntByReference(Native.POINTER_SIZE);
    jemallocLib.mallctl("version", pVersion, pSize, null, 0);

    _jemalloc = pVersion.getValue().getString(0);
  }
}
