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

import org.apache.iotdb.commons.auth.entity.PathPrivilege;
import org.apache.iotdb.commons.auth.entity.PriPrivilegeType;
import org.apache.iotdb.commons.auth.entity.Role;
import org.apache.iotdb.commons.exception.IllegalPathException;
import org.apache.iotdb.commons.path.PartialPath;

import com.google.common.base.Supplier;
import org.apache.tsfile.utils.Pair;

import java.io.DataInputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;

public class IOUtils {

  private IOUtils() {}

  /**
   * Write a string into the given stream.
   *
   * @param outputStream the destination to insert.
   * @param str the string to be written.
   * @param encoding string encoding like 'utf-8'.
   * @param encodingBufferLocal a ThreadLocal buffer may be passed to avoid frequently memory
   *     allocations. A null may also be passed to use a local buffer.
   * @throws IOException when an exception raised during operating the stream.
   */
  public static void writeString(
      OutputStream outputStream,
      String str,
      String encoding,
      ThreadLocal<ByteBuffer> encodingBufferLocal)
      throws IOException {
    if (str != null) {
      byte[] strBuffer = str.getBytes(encoding);
      writeInt(outputStream, strBuffer.length, encodingBufferLocal);
      outputStream.write(strBuffer);
    } else {
      writeInt(outputStream, 0, encodingBufferLocal);
    }
  }

  /**
   * Write an integer into the given stream.
   *
   * @param outputStream the destination to insert.
   * @param i the integer to be written.
   * @param encodingBufferLocal a ThreadLocal buffer may be passed to avoid frequently memory
   *     allocations. A null may also be passed to use a local buffer.
   * @throws IOException when an exception raised during operating the stream.
   */
  public static void writeInt(
      OutputStream outputStream, int i, ThreadLocal<ByteBuffer> encodingBufferLocal)
      throws IOException {
    ByteBuffer encodingBuffer;
    if (encodingBufferLocal != null) {
      encodingBuffer = encodingBufferLocal.get();
      if (encodingBuffer == null) {
        // set to 8 because this buffer may be applied to other types
        encodingBuffer = ByteBuffer.allocate(8);
        encodingBufferLocal.set(encodingBuffer);
      }
    } else {
      encodingBuffer = ByteBuffer.allocate(4);
    }
    encodingBuffer.clear();
    encodingBuffer.putInt(i);
    outputStream.write(encodingBuffer.array(), 0, Integer.BYTES);
  }

  /**
   * Read a string from the given stream.
   *
   * @param inputStream the source to read.
   * @param encoding string encoding like 'utf-8'.
   * @param strBufferLocal a ThreadLocal buffer may be passed to avoid frequently memory
   *     allocations. A null may also be passed to use a local buffer.
   * @return a string read from the stream.
   * @throws IOException when an exception raised during operating the stream.
   */
  public static String readString(
      DataInputStream inputStream, String encoding, ThreadLocal<byte[]> strBufferLocal)
      throws IOException {
    byte[] strBuffer;
    int length = inputStream.readInt();
    if (length > 0) {
      if (strBufferLocal != null) {
        strBuffer = strBufferLocal.get();
        if (strBuffer == null || length > strBuffer.length) {
          strBuffer = new byte[length];
          strBufferLocal.set(strBuffer);
        }
      } else {
        strBuffer = new byte[length];
      }

      inputStream.read(strBuffer, 0, length);
      return new String(strBuffer, 0, length, encoding);
    }
    return null;
  }

  /**
   * To distinguish between permissions files of the new and old versions, in files generated by the
   * new version, the length of the first string will be stored as a negative number.
   *
   * <p>Only for LocalFileUserAccessor/LocalFileRoleAccessor. We save our roles/users' name string
   * in a different format.
   *
   * @param inputStream the source to read.
   * @param encoding string encoding like "utf-8"
   * @param strBufferLocal a ThreadLocal buffer may be passed to avoid frequently memory.
   *     allocations. A null may also be passed to use a local buffer.
   * @return a pair contains one string and boolean. The string is the auth string we get from input
   *     stream. The boolean is true means length of the auth string is greater than 0.
   * @throws IOException when an exception raised during operating the stream.
   */
  public static Pair<String, Boolean> readAuthString(
      DataInputStream inputStream, String encoding, ThreadLocal<byte[]> strBufferLocal)
      throws IOException {
    byte[] strBuffer;
    int length = inputStream.readInt();
    int absLength = Math.abs(length);
    if (absLength != 0) {
      if (strBufferLocal != null) {
        strBuffer = strBufferLocal.get();
        if (strBuffer == null || absLength > strBuffer.length) {
          strBuffer = new byte[absLength];
          strBufferLocal.set(strBuffer);
        }
      } else {
        strBuffer = new byte[absLength];
      }

      inputStream.read(strBuffer, 0, absLength);
      Pair<String, Boolean> result =
          new Pair<>(new String(strBuffer, 0, absLength, encoding), length > 0);
      return result;
    }
    return null;
  }

  /**
   * Read a PathPrivilege from the given stream.
   *
   * @param inputStream the source to read.
   * @param encoding string encoding like 'utf-8'.
   * @param strBufferLocal a ThreadLocal buffer may be passed to avoid frequently memory
   *     allocations. A null may also be passed to use a local buffer.
   * @param compatible A boolean value used to indicate whether the old version of permission
   *     deserialization method should be employed.
   * @return a PathPrivilege read from the stream.
   * @throws IOException when an exception raised during operating the stream.
   */
  public static PathPrivilege readPathPrivilege(
      DataInputStream inputStream,
      String encoding,
      ThreadLocal<byte[]> strBufferLocal,
      boolean compatible)
      throws IOException, IllegalPathException {
    String path = IOUtils.readString(inputStream, encoding, strBufferLocal);
    if (compatible) {
      int privilegeNum = inputStream.readInt();
      PathPrivilege pathPrivilege = new PathPrivilege(new PartialPath(path));
      for (int i = 0; i < privilegeNum; i++) {
        // Need to check the corresponding relationship of permissions
        pathPrivilege.getPrivileges().add(inputStream.readInt());
      }
      return pathPrivilege;
    } else {
      PathPrivilege pathPrivilege = new PathPrivilege(new PartialPath(path));
      int privileges = inputStream.readInt();
      pathPrivilege.setAllPrivileges(privileges);
      return pathPrivilege;
    }
  }

  // Because pre version's privilege will stored by path + privilege
  // This func will turn the privilege info into role's path privileges.
  // for the global privilege, they were stored by root + privilege.

  public static void loadRolePrivilege(
      Role role, DataInputStream inputStream, String encoding, ThreadLocal<byte[]> strBufferLocal)
      throws IOException, IllegalPathException {
    role.setSysPrivilegeSet(0);
    int pathPriNum = inputStream.readInt();
    List<PathPrivilege> pathPrivilegeList = new ArrayList<>();
    for (int i = 0; i < pathPriNum; i++) {
      String path = IOUtils.readString(inputStream, encoding, strBufferLocal);
      PartialPath ppath = new PartialPath(path);
      PathPrivilege pathPriv = new PathPrivilege(ppath);
      int priNum = inputStream.readInt();
      for (int j = 0; j < priNum; j++) {
        PriPrivilegeType priType = PriPrivilegeType.values()[inputStream.readInt()];
        if (priType.isAccept()) {
          pathPriv.grantPrivilege(priType.ordinal(), false);
        }
      }
      pathPrivilegeList.add(pathPriv);
    }
    role.setPrivilegeList(pathPrivilegeList);
    role.setServiceReady(false);
  }

  /**
   * Write a PathPrivilege to the given stream.
   *
   * @param outputStream the destination to insert.
   * @param pathPrivilege the PathPrivilege to be written.
   * @param encoding string encoding like 'utf-8'.
   * @param encodingBufferLocal a ThreadLocal buffer may be passed to avoid frequently memory
   *     allocations. A null may also be passed to use a local buffer.
   * @throws IOException when an exception raised during operating the stream.
   */
  public static void writePathPrivilege(
      OutputStream outputStream,
      PathPrivilege pathPrivilege,
      String encoding,
      ThreadLocal<ByteBuffer> encodingBufferLocal)
      throws IOException {
    writeString(outputStream, pathPrivilege.getPath().getFullPath(), encoding, encodingBufferLocal);
    writeInt(outputStream, pathPrivilege.getAllPrivileges(), encodingBufferLocal);
  }

  /**
   * Replace newFile with oldFile. If the file system does not support atomic file replacement then
   * delete the old file first.
   *
   * @param newFile the new file.
   * @param oldFile the file to be replaced.
   */
  public static void replaceFile(File newFile, File oldFile) throws IOException {
    if (!newFile.renameTo(oldFile)) {
      // some OSs need to delete the old file before renaming to it
      if (!oldFile.delete()) {
        throw new IOException(String.format("Cannot delete old user file : %s", oldFile.getPath()));
      }
      if (!newFile.renameTo(oldFile)) {
        throw new IOException(
            String.format("Cannot replace old user file with new one : %s", newFile.getPath()));
      }
    }
  }

  public static ByteBuffer clone(ByteBuffer original) {
    ByteBuffer clone = ByteBuffer.allocate(original.capacity());
    original.rewind();
    clone.put(original);
    original.rewind();
    clone.flip();
    return clone;
  }

  /**
   * Retry a method at most 'maxRetry' times, each time calling 'retryFunc' and passing the result
   * to 'validator'. If 'validator' returns true, returns the result.
   *
   * @param maxRetry maximum number of retires
   * @param retryIntervalMS retry interval in milliseconds
   * @param retryFunc function to be retried
   * @param validator validating the result of 'retryFunc'
   * @return true if the result from 'retryFunc' passes validation, false if all retries fail or is
   *     interrupted
   * @param <T>
   */
  public static <T> Optional<T> retryNoException(
      int maxRetry, long retryIntervalMS, Supplier<T> retryFunc, Function<T, Boolean> validator) {
    for (int i = 0; i < maxRetry; i++) {
      T result = retryFunc.get();
      if (Boolean.TRUE.equals(validator.apply(result))) {
        return Optional.of(result);
      }
      try {
        Thread.sleep(retryIntervalMS);
      } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        return Optional.empty();
      }
    }
    return Optional.empty();
  }
}
