/*
 * 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.iotdb.db.utils;

import org.apache.iotdb.db.conf.IoTDBConfig;
import org.apache.iotdb.db.conf.IoTDBConstant;
import org.apache.iotdb.db.conf.IoTDBDescriptor;
import org.apache.iotdb.db.conf.directories.DirectoryManager;

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

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Collections;
import java.util.EnumMap;
import java.util.List;

// Notice : statistics in this class may not be accurate because of limited user authority.
public class OpenFileNumUtil {

  private static final Logger logger = LoggerFactory.getLogger(OpenFileNumUtil.class);
  private static final int PID_ERROR_CODE = -1;
  private static final int UNSUPPORTED_OS_ERROR_CODE = -2;
  private static final int UNKNOWN_STATISTICS_ERROR_CODE = -3;
  private static final String IOTDB_PROCESS_KEY_WORD = IoTDBConstant.GLOBAL_DB_NAME;
  private static final String LINUX_OS_NAME = "linux";
  private static final String MAC_OS_NAME = "mac";
  private static final String SEARCH_PID_LINUX = "ps -aux | grep -i %s | grep -v grep";
  private static final String SEARCH_PID_MAC = "ps aux | grep -i %s | grep -v grep";
  // command 'lsof -p' is available on most Linux distro except CentOS.
  private static final String SEARCH_OPEN_DATA_FILE_BY_PID = "lsof -p %d";

  private static IoTDBConfig config = IoTDBDescriptor.getInstance().getConfig();
  private static DirectoryManager directoryManager = DirectoryManager.getInstance();
  private static final String[] COMMAND_TEMPLATE = {"/bin/bash", "-c", ""};
  private static boolean isOutputValid = false;
  private int pid;

  /** constructor, process key word is defined by IOTDB_PROCESS_KEY_WORD. */
  private OpenFileNumUtil() {
    pid = getIotdbPid();
  }

  /**
   * singleton instance.
   *
   * @return instance
   */
  public static OpenFileNumUtil getInstance() {
    return OpenFileNumUtilHolder.INSTANCE;
  }

  /**
   * check if the string is numeric.
   *
   * @param str string need to be checked
   * @return whether the string is a number
   */
  private static boolean isNumeric(String str) {
    if (str == null || "".equals(str)) {
      return false;
    } else {
      for (int i = str.length(); --i >= 0; ) {
        if (!Character.isDigit(str.charAt(i))) {
          return false;
        }
      }
    }

    return true;
  }

  /**
   * get IoTDB server process ID by executing command.
   *
   * @return pid of IoTDB server process
   */
  private static int getIotdbPid() {
    int iotdbPid = -1;
    Process pro1;
    Runtime r = Runtime.getRuntime();
    // System.getProperty("os.name") can detect which type of OS is using now.
    // this code can detect Windows, Mac, Unix and Solaris.
    String os = System.getProperty("os.name");
    String osName = os.toLowerCase();
    if (osName.startsWith(LINUX_OS_NAME) || osName.startsWith(MAC_OS_NAME)) {
      try {
        String command;
        if (osName.startsWith(LINUX_OS_NAME)) {
          command = String.format(SEARCH_PID_LINUX, IOTDB_PROCESS_KEY_WORD);
        } else {
          command = String.format(SEARCH_PID_MAC, IOTDB_PROCESS_KEY_WORD);
        }
        COMMAND_TEMPLATE[2] = command;
        pro1 = r.exec(COMMAND_TEMPLATE);
        BufferedReader in1 = new BufferedReader(new InputStreamReader(pro1.getInputStream()));
        String line;
        while ((line = in1.readLine()) != null) {
          line = line.trim();
          String[] temp = line.split("\\s+");
          if (temp.length > 1 && isNumeric(temp[1])) {
            iotdbPid = Integer.parseInt(temp[1]);
            break;
          }
        }
        in1.close();
        pro1.destroy();
      } catch (IOException e) {
        logger.error("Cannot get PID of IoTDB process because ", e);
      }
    } else {
      logger.warn("Unsupported OS {} for OpenFileNumUtil to get the PID of IoTDB.", os);
    }
    return iotdbPid;
  }

  /**
   * set pid.
   *
   * @param pid is the process ID of IoTDB service process
   */
  void setPid(int pid) {
    this.pid = pid;
  }

  /**
   * return statistic Map, whose key belongs to enum OpenFileNumStatistics: TOTAL_OPEN_FILE_NUM is
   * the current total open file number of IoTDB service process; SEQUENCE_FILE_OPEN_NUM is the
   * current open file number under data directory; DELTA_OPEN_FILE_NUM is the current open file
   * number of TsFile; UNSEQUENCE_FILE_OPEN_NUM is the current open file number of unsequence file;
   * WAL_OPEN_FILE_NUM is the current open file number of WAL file; METADATA_OPEN_FILE_NUM is the
   * current open file number of metadata; DIGEST_OPEN_FILE_NUM is the current open file number of
   * fileNodeDir; SOCKET_OPEN_FILE_NUM is the current open socket connection of IoTDB service
   * process.
   *
   * @param pid : IoTDB service pid
   * @return list : statistics list
   */
  private static EnumMap<OpenFileNumStatistics, Integer> getOpenFile(int pid) {
    EnumMap<OpenFileNumStatistics, Integer> resultMap = new EnumMap<>(OpenFileNumStatistics.class);
    // initialize resultMap
    for (OpenFileNumStatistics openFileNumStatistics : OpenFileNumStatistics.values()) {
      resultMap.put(openFileNumStatistics, 0);
    }
    Process pro;
    int lineCount = 0;
    Runtime r = Runtime.getRuntime();
    try {
      String command = String.format(SEARCH_OPEN_DATA_FILE_BY_PID, pid);
      COMMAND_TEMPLATE[2] = command;
      pro = r.exec(COMMAND_TEMPLATE);
      String line;

      try (BufferedReader in = new BufferedReader(new InputStreamReader(pro.getInputStream()))) {
        while ((line = in.readLine()) != null) {
          lineCount++;
          countOneFile(line, pid, resultMap);
        }
      }
      if (lineCount < OpenFileNumStatistics.values().length) {
        isOutputValid = false;
        for (OpenFileNumStatistics statistics : OpenFileNumStatistics.values()) {
          resultMap.put(statistics, UNSUPPORTED_OS_ERROR_CODE);
        }
      } else {
        isOutputValid = true;
      }
      pro.destroy();
    } catch (Exception e) {
      logger.error("Cannot get open file number of IoTDB process because ", e);
    }
    return resultMap;
  }

  private static void countOneFile(
      String line, int pid, EnumMap<OpenFileNumStatistics, Integer> resultMap) {
    String[] temp = line.split("\\s+");
    if (!line.contains(Integer.toString(pid)) || temp.length <= 8) {
      return;
    }
    int oldValue = resultMap.get(OpenFileNumStatistics.TOTAL_OPEN_FILE_NUM);
    resultMap.put(OpenFileNumStatistics.TOTAL_OPEN_FILE_NUM, oldValue + 1);
    for (OpenFileNumStatistics openFileNumStatistics : OpenFileNumStatistics.values()) {
      if (openFileNumStatistics.path == null) {
        continue;
      }
      for (String path : openFileNumStatistics.path) {
        if (temp[8].contains(path)) {
          oldValue = resultMap.get(openFileNumStatistics);
          resultMap.put(openFileNumStatistics, oldValue + 1);
        }
      }
    }
    if (temp[7].contains("TCP") || temp[7].contains("UDP")) {
      oldValue = resultMap.get(OpenFileNumStatistics.SOCKET_OPEN_FILE_NUM);
      resultMap.put(OpenFileNumStatistics.SOCKET_OPEN_FILE_NUM, oldValue + 1);
    }
  }

  /**
   * Check if runtime OS is supported then return the result list. If pid is abnormal then all
   * statistics returns -1, if OS is not supported then all statistics returns -2
   *
   * @return map
   */
  private EnumMap<OpenFileNumStatistics, Integer> getStatisticMap() {
    EnumMap<OpenFileNumStatistics, Integer> resultMap = new EnumMap<>(OpenFileNumStatistics.class);
    String osName = System.getProperty("os.name").toLowerCase();
    // get runtime OS name, currently only support Linux and MacOS
    if (osName.startsWith(LINUX_OS_NAME) || osName.startsWith(MAC_OS_NAME)) {
      // if pid is normal, then get statistics
      if (pid > 0) {
        resultMap = getOpenFile(pid);
      } else {
        // pid is abnormal, give all statistics abnormal value -1
        for (OpenFileNumStatistics statistics : OpenFileNumStatistics.values()) {
          resultMap.put(statistics, PID_ERROR_CODE);
        }
      }
    } else {
      // operation system not supported, give all statistics abnormal value -2
      for (OpenFileNumStatistics statistics : OpenFileNumStatistics.values()) {
        resultMap.put(statistics, UNSUPPORTED_OS_ERROR_CODE);
      }
    }
    return resultMap;
  }

  /**
   * get statistics.
   *
   * @param statistics get what statistics of open file number
   * @return open file number
   */
  public int get(OpenFileNumStatistics statistics) {
    EnumMap<OpenFileNumStatistics, Integer> statisticsMap = getStatisticMap();
    return statisticsMap.getOrDefault(statistics, UNKNOWN_STATISTICS_ERROR_CODE);
  }

  boolean isCommandValid() {
    return isOutputValid;
  }

  public enum OpenFileNumStatistics {
    TOTAL_OPEN_FILE_NUM(null),
    SEQUENCE_FILE_OPEN_NUM(directoryManager.getAllSequenceFileFolders()),
    UNSEQUENCE_FILE_OPEN_NUM(directoryManager.getAllUnSequenceFileFolders()),
    WAL_OPEN_FILE_NUM(Collections.singletonList(config.getWalDir())),
    DIGEST_OPEN_FILE_NUM(Collections.singletonList(config.getSystemDir())),
    SOCKET_OPEN_FILE_NUM(null);

    // path is a list of directory corresponding to the OpenFileNumStatistics enum element,
    // e.g. data/data/ for SEQUENCE_FILE_OPEN_NUM
    private List<String> path;

    OpenFileNumStatistics(List<String> path) {
      this.path = path;
    }

    public List<String> getPath() {
      return path;
    }
  }

  private static class OpenFileNumUtilHolder {

    private OpenFileNumUtilHolder() {}

    private static final OpenFileNumUtil INSTANCE = new OpenFileNumUtil();
  }
}
