/*
 * Copyright 2010 Srikanth Reddy Lingala
 *
 * 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.
 */

package net.lingala.zip4j.util;

import net.lingala.zip4j.exception.ZipException;
import net.lingala.zip4j.model.AbstractFileHeader;
import net.lingala.zip4j.model.enums.CompressionMethod;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.util.Calendar;

public class Zip4jUtil {

  private static final long DOSTIME_BEFORE_1980 = (1 << 21) | (1 << 16);
  private static final int MAX_RAW_READ_FULLY_RETRY_ATTEMPTS = 15;

  public static boolean isStringNullOrEmpty(String str) {
    return str == null || str.trim().length() == 0;
  }

  public static boolean isStringNotNullAndNotEmpty(String str) {
    return str != null && str.trim().length() > 0;
  }

  public static boolean createDirectoryIfNotExists(File file) throws ZipException {
    if (file == null) {
      throw new ZipException("output path is null");
    }

    if (file.exists()) {
      if (!file.isDirectory()) {
        throw new ZipException("output directory is not valid");
      }
    } else {
      if (!file.mkdirs()) {
        throw new ZipException("Cannot create output directories");
      }
    }

    return true;
  }

  public static long epochToExtendedDosTime(long time) {
    if (time < 0) {
      return DOSTIME_BEFORE_1980;
    }
    long dostime = epochToDosTime(time);
    return (dostime != DOSTIME_BEFORE_1980)
            ? dostime + ((time % 2000) << 32)
            : DOSTIME_BEFORE_1980;
  }

  private static long epochToDosTime(long time) {
    Calendar cal = Calendar.getInstance();
    cal.setTimeInMillis(time);

    int year = cal.get(Calendar.YEAR);

    if (year < 1980) {
      return DOSTIME_BEFORE_1980;
    }
    return (year - 1980) << 25 | (cal.get(Calendar.MONTH) + 1) << 21 |
            cal.get(Calendar.DATE) << 16 | cal.get(Calendar.HOUR_OF_DAY) << 11 | cal.get(Calendar.MINUTE) << 5 |
            cal.get(Calendar.SECOND) >> 1;
  }

  public static long dosToExtendedEpochTme(long dosTime) {
    long time = dosToEpochTime(dosTime);
    return time + (dosTime >> 32);
  }

  private static long dosToEpochTime(long dosTime) {
    int sec = (int) ((dosTime << 1) & 0x3e);
    int min = (int) ((dosTime >> 5) & 0x3f);
    int hrs = (int) ((dosTime >> 11) & 0x1f);
    int day = (int) ((dosTime >> 16) & 0x1f);
    int mon = (int) (((dosTime >> 21) & 0xf) - 1);
    int year = (int) (((dosTime >> 25) & 0x7f) + 1980);

    Calendar cal = Calendar.getInstance();
    cal.set(year, mon, day, hrs, min, sec);
    cal.set(Calendar.MILLISECOND, 0);
    return cal.getTime().getTime();
  }

  public static byte[] convertCharArrayToByteArray(char[] charArray, boolean useUtf8Charset) {
    return useUtf8Charset
            ? convertCharArrayToByteArrayUsingUtf8(charArray)
            : convertCharArrayToByteArrayUsingDefaultCharset(charArray);
  }

  public static CompressionMethod getCompressionMethod(AbstractFileHeader localFileHeader) throws ZipException {
    if (localFileHeader.getCompressionMethod() != CompressionMethod.AES_INTERNAL_ONLY) {
      return localFileHeader.getCompressionMethod();
    }

    if (localFileHeader.getAesExtraDataRecord() == null) {
      throw new ZipException("AesExtraDataRecord not present in local header for aes encrypted data");
    }

    return localFileHeader.getAesExtraDataRecord().getCompressionMethod();
  }

  public static int readFully(InputStream inputStream, byte[] bufferToReadInto) throws IOException {

    int readLen = inputStream.read(bufferToReadInto);

    if (readLen == -1) {
      throw new IOException("Unexpected EOF reached when trying to read stream");
    }

    if (readLen != bufferToReadInto.length) {
      readLen = readUntilBufferIsFull(inputStream, bufferToReadInto, readLen);

      if (readLen != bufferToReadInto.length) {
        throw new IOException("Cannot read fully into byte buffer");
      }
    }

    return readLen;
  }

  public static int readFully(InputStream inputStream, byte[] b, int offset, int length) throws IOException {
    int numberOfBytesRead = 0;

    if (offset < 0) {
      throw new IllegalArgumentException("Negative offset");
    }

    if (length < 0) {
      throw new IllegalArgumentException("Negative length");
    }

    if (length == 0) {
      return 0;
    }

    if (offset + length > b.length) {
      throw new IllegalArgumentException("Length greater than buffer size");
    }

    while (numberOfBytesRead != length) {
      int currentReadLength = inputStream.read(b, offset + numberOfBytesRead, length - numberOfBytesRead);
      if (currentReadLength == -1) {
        if (numberOfBytesRead == 0) {
          return -1;
        }
        return numberOfBytesRead;
      }

      numberOfBytesRead += currentReadLength;
    }

    return numberOfBytesRead;
  }

  private static int readUntilBufferIsFull(InputStream inputStream, byte[] bufferToReadInto, int readLength)
      throws IOException {
    if (readLength < 0) {
      throw new IOException("Invalid readLength");
    }

    if (readLength == 0) {
      return 0;
    }

    int remainingLength = bufferToReadInto.length - readLength;
    int loopReadLength = 0;
    int retryAttempt = 1; // first attempt is already done before this method is called

    while (readLength < bufferToReadInto.length
        && loopReadLength != -1
        && retryAttempt < MAX_RAW_READ_FULLY_RETRY_ATTEMPTS) {

      loopReadLength = inputStream.read(bufferToReadInto, readLength, remainingLength);

      if (loopReadLength > 0) {
        readLength += loopReadLength;
        remainingLength -= loopReadLength;
      }

      retryAttempt++;
    }

    return readLength;
  }

  private static byte[] convertCharArrayToByteArrayUsingUtf8(char[] charArray) {
    try {
      ByteBuffer buf = InternalZipConstants.CHARSET_UTF_8.encode(CharBuffer.wrap(charArray));
      byte[] bytes = new byte[buf.limit()];
      buf.get(bytes);
      return bytes;
    } catch (Exception e) {
      return convertCharArrayToByteArrayUsingDefaultCharset(charArray);
    }
  }

  private static byte[] convertCharArrayToByteArrayUsingDefaultCharset(char[] charArray) {
    byte[] bytes = new byte[charArray.length];
    for (int i = 0; i < charArray.length; i++) {
      bytes[i] = (byte) charArray[i];
    }
    return bytes;
  }

}
