/*
 * Copyright 2015 Confluent Inc.
 *
 * 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.
 */

/*
 * Original license:
 *
 * 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 io.confluent.common.utils;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Utils {
  private static final Pattern HOST_PORT_PATTERN = Pattern.compile("\\[?(.+?)\\]?:(\\d+)");

  public static String NL = System.getProperty("line.separator");

  /**
   * Turn the given UTF8 byte array into a string
   *
   * @param bytes The byte array
   * @return The string
   */
  public static String utf8(byte[] bytes) {
    try {
      return new String(bytes, "UTF8");
    } catch (UnsupportedEncodingException e) {
      throw new RuntimeException("This shouldn't happen.", e);
    }
  }

  /**
   * Turn a string into a utf8 byte[]
   *
   * @param string The string
   * @return The byte[]
   */
  public static byte[] utf8(String string) {
    try {
      return string.getBytes("UTF8");
    } catch (UnsupportedEncodingException e) {
      throw new RuntimeException("This shouldn't happen.", e);
    }
  }

  /**
   * Read an unsigned integer from the current position in the buffer, incrementing the position by
   * 4 bytes
   *
   * @param buffer The buffer to read from
   * @return The integer read, as a long to avoid signedness
   */
  public static long readUnsignedInt(ByteBuffer buffer) {
    return buffer.getInt() & 0xffffffffL;
  }

  /**
   * Read an unsigned integer from the given position without modifying the buffers position
   *
   * @param buffer the buffer to read from
   * @param index  the index from which to read the integer
   * @return The integer read, as a long to avoid signedness
   */
  public static long readUnsignedInt(ByteBuffer buffer, int index) {
    return buffer.getInt(index) & 0xffffffffL;
  }

  /**
   * Read an unsigned integer stored in little-endian format from the {@link InputStream}.
   *
   * @param in The stream to read from
   * @return The integer read (MUST BE TREATED WITH SPECIAL CARE TO AVOID SIGNEDNESS)
   */
  public static int readUnsignedIntLE(InputStream in) throws IOException {
    return (in.read() << 8 * 0)
           | (in.read() << 8 * 1)
           | (in.read() << 8 * 2)
           | (in.read() << 8 * 3);
  }

  /**
   * Read an unsigned integer stored in little-endian format from a byte array at a given offset.
   *
   * @param buffer The byte array to read from
   * @param offset The position in buffer to read from
   * @return The integer read (MUST BE TREATED WITH SPECIAL CARE TO AVOID SIGNEDNESS)
   */
  public static int readUnsignedIntLE(byte[] buffer, int offset) {
    return (buffer[offset++] << 8 * 0)
           | (buffer[offset++] << 8 * 1)
           | (buffer[offset++] << 8 * 2)
           | (buffer[offset] << 8 * 3);
  }

  /**
   * Write the given long value as a 4 byte unsigned integer. Overflow is ignored.
   *
   * @param buffer The buffer to write to
   * @param value  The value to write
   */
  public static void writetUnsignedInt(ByteBuffer buffer, long value) {
    buffer.putInt((int) (value & 0xffffffffL));
  }

  /**
   * Write the given long value as a 4 byte unsigned integer. Overflow is ignored.
   *
   * @param buffer The buffer to write to
   * @param index  The position in the buffer at which to begin writing
   * @param value  The value to write
   */
  public static void writeUnsignedInt(ByteBuffer buffer, int index, long value) {
    buffer.putInt(index, (int) (value & 0xffffffffL));
  }

  /**
   * Write an unsigned integer in little-endian format to the {@link OutputStream}.
   *
   * @param out   The stream to write to
   * @param value The value to write
   */
  public static void writeUnsignedIntLE(OutputStream out, int value) throws IOException {
    out.write(value >>> 8 * 0);
    out.write(value >>> 8 * 1);
    out.write(value >>> 8 * 2);
    out.write(value >>> 8 * 3);
  }

  /**
   * Write an unsigned integer in little-endian format to a byte array at a given offset.
   *
   * @param buffer The byte array to write to
   * @param offset The position in buffer to write to
   * @param value  The value to write
   */
  public static void writeUnsignedIntLE(byte[] buffer, int offset, int value) {
    buffer[offset++] = (byte) (value >>> 8 * 0);
    buffer[offset++] = (byte) (value >>> 8 * 1);
    buffer[offset++] = (byte) (value >>> 8 * 2);
    buffer[offset] = (byte) (value >>> 8 * 3);
  }


  /**
   * Get the absolute value of the given number. If the number is Int.MinValue return 0. This is
   * different from java.lang.Math.abs or scala.math.abs in that they return Int.MinValue (!).
   */
  public static int abs(int n) {
    return n & 0x7fffffff;
  }

  /**
   * Get the length for UTF8-encoding a string without encoding it first
   *
   * @param s The string to calculate the length for
   * @return The length when serialized
   */
  public static int utf8Length(CharSequence s) {
    int count = 0;
    for (int i = 0, len = s.length(); i < len; i++) {
      char ch = s.charAt(i);
      if (ch <= 0x7F) {
        count++;
      } else if (ch <= 0x7FF) {
        count += 2;
      } else if (Character.isHighSurrogate(ch)) {
        count += 4;
        ++i;
      } else {
        count += 3;
      }
    }
    return count;
  }

  /**
   * Read the given byte buffer into a byte array
   */
  public static byte[] toArray(ByteBuffer buffer) {
    return toArray(buffer, 0, buffer.limit());
  }

  /**
   * Read a byte array from the given offset and size in the buffer
   */
  public static byte[] toArray(ByteBuffer buffer, int offset, int size) {
    byte[] dest = new byte[size];
    if (buffer.hasArray()) {
      System.arraycopy(buffer.array(), buffer.arrayOffset() + offset, dest, 0, size);
    } else {
      int pos = buffer.position();
      buffer.get(dest);
      buffer.position(pos);
    }
    return dest;
  }

  /**
   * Check that the parameter t is not null
   *
   * @param t The object to check
   * @return t if it isn't null
   * @throws NullPointerException if t is null.
   */
  public static <T> T notNull(T t) {
    if (t == null) {
      throw new NullPointerException();
    } else {
      return t;
    }
  }

  /**
   * Instantiate the class
   */
  public static Object newInstance(Class<?> c) {
    try {
      return c.newInstance();
    } catch (IllegalAccessException e) {
      throw new RuntimeException("Could not instantiate class " + c.getName(), e);
    } catch (InstantiationException e) {
      throw new RuntimeException("Could not instantiate class " + c.getName()
                                 + " Does it have a public no-argument constructor?", e);
    }
  }

  /**
   * Generates 32 bit murmur2 hash from byte array
   *
   * @param data byte array to hash
   * @return 32 bit hash of the given array
   */
  public static int murmur2(final byte[] data) {
    int length = data.length;
    int seed = 0x9747b28c;
    // 'm' and 'r' are mixing constants generated offline.
    // They're not really 'magic', they just happen to work well.
    final int m = 0x5bd1e995;
    final int r = 24;

    // Initialize the hash to a random value
    int h = seed ^ length;
    int length4 = length / 4;

    for (int i = 0; i < length4; i++) {
      final int i4 = i * 4;
      int
          k =
          (data[i4 + 0] & 0xff) + ((data[i4 + 1] & 0xff) << 8) + ((data[i4 + 2] & 0xff) << 16) + (
              (data[i4 + 3] & 0xff) << 24);
      k *= m;
      k ^= k >>> r;
      k *= m;
      h *= m;
      h ^= k;
    }

    // Handle the last few bytes of the input array
    switch (length % 4) {
      case 3:
        h ^= (data[(length & ~3) + 2] & 0xff) << 16;
      case 2:
        h ^= (data[(length & ~3) + 1] & 0xff) << 8;
      case 1:
        h ^= (data[length & ~3] & 0xff);
        h *= m;
    }

    h ^= h >>> 13;
    h *= m;
    h ^= h >>> 15;

    return h;
  }

  /**
   * Extracts the hostname from a "host:port" address string.
   *
   * @param address address string to parse
   * @return hostname or null if the given address is incorrect
   */
  public static String getHost(String address) {
    Matcher matcher = HOST_PORT_PATTERN.matcher(address);
    return matcher.matches() ? matcher.group(1) : null;
  }

  /**
   * Extracts the port number from a "host:port" address string.
   *
   * @param address address string to parse
   * @return port number or null if the given address is incorrect
   */
  public static Integer getPort(String address) {
    Matcher matcher = HOST_PORT_PATTERN.matcher(address);
    return matcher.matches() ? Integer.parseInt(matcher.group(2)) : null;
  }

  /**
   * Formats hostname and port number as a "host:port" address string, surrounding IPv6 addresses
   * with braces '[', ']'
   *
   * @param host hostname
   * @param port port number
   * @return address string
   */
  public static String formatAddress(String host, Integer port) {
    return host.contains(":")
           ? "[" + host + "]:" + port // IPv6
           : host + ":" + port;
  }

  /**
   * Read a properties file from the given path
   * @param filename The path of the file to read
   */
  public static Properties loadProps(String filename) throws IOException {
    Properties props = new Properties();
    InputStream propStream = null;
    try {
      propStream = new FileInputStream(filename);
      props.load(propStream);
    } finally {
      if(propStream != null)
        propStream.close();
    }
    return props;
  }

  /**
   * Recursively delete the given file/directory and any subfiles (if any exist)
   *
   * @param file The root file at which to begin deleting
   */
  public static void delete(final File file) throws IOException {
    if (file == null) {
      return;
    }
    Files.walkFileTree(file.toPath(), new SimpleFileVisitor<Path>() {
      @Override
      public FileVisitResult visitFileFailed(Path path, IOException exc) throws IOException {
        // If the root path did not exist, ignore the error; otherwise throw it.
        if (exc instanceof NoSuchFileException && path.toFile().equals(file)) {
          return FileVisitResult.TERMINATE;
        }
        throw exc;
      }

      @Override
      public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) throws IOException {
        Files.delete(path);
        return FileVisitResult.CONTINUE;
      }

      @Override
      public FileVisitResult postVisitDirectory(Path path, IOException exc) throws IOException {
        Files.delete(path);
        return FileVisitResult.CONTINUE;
      }
    });
  }
}
