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

import net.lingala.zip4j.exception.ZipException;
import net.lingala.zip4j.io.outputstream.CountingOutputStream;
import net.lingala.zip4j.io.outputstream.OutputStreamWithSplitZipSupport;
import net.lingala.zip4j.io.outputstream.SplitOutputStream;
import net.lingala.zip4j.model.AESExtraDataRecord;
import net.lingala.zip4j.model.ExtraDataRecord;
import net.lingala.zip4j.model.FileHeader;
import net.lingala.zip4j.model.LocalFileHeader;
import net.lingala.zip4j.model.Zip64EndOfCentralDirectoryLocator;
import net.lingala.zip4j.model.Zip64EndOfCentralDirectoryRecord;
import net.lingala.zip4j.model.ZipModel;
import net.lingala.zip4j.util.InternalZipConstants;
import net.lingala.zip4j.util.RawIO;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.util.List;

import static net.lingala.zip4j.headers.HeaderUtil.getBytesFromString;
import static net.lingala.zip4j.util.FileUtils.getZipFileNameWithoutExtension;
import static net.lingala.zip4j.util.InternalZipConstants.ZIP_64_NUMBER_OF_ENTRIES_LIMIT;
import static net.lingala.zip4j.util.InternalZipConstants.ZIP_64_SIZE_LIMIT;
import static net.lingala.zip4j.util.Zip4jUtil.isStringNotNullAndNotEmpty;

public class HeaderWriter {

  private static final short ZIP64_EXTRA_DATA_RECORD_SIZE_LFH = 16;
  private static final short ZIP64_EXTRA_DATA_RECORD_SIZE_FH = 28;
  private static final short AES_EXTRA_DATA_RECORD_SIZE = 11;

  private final RawIO rawIO = new RawIO();
  private final byte[] longBuff = new byte[8];
  private final byte[] intBuff = new byte[4];

  public void writeLocalFileHeader(ZipModel zipModel, LocalFileHeader localFileHeader, OutputStream outputStream,
                                   Charset charset) throws IOException {

    try(ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
      rawIO.writeIntLittleEndian(byteArrayOutputStream, (int) localFileHeader.getSignature().getValue());
      rawIO.writeShortLittleEndian(byteArrayOutputStream, localFileHeader.getVersionNeededToExtract());
      byteArrayOutputStream.write(localFileHeader.getGeneralPurposeFlag());
      rawIO.writeShortLittleEndian(byteArrayOutputStream, localFileHeader.getCompressionMethod().getCode());

      rawIO.writeLongLittleEndian(longBuff, 0, localFileHeader.getLastModifiedTime());
      byteArrayOutputStream.write(longBuff, 0, 4);

      rawIO.writeLongLittleEndian(longBuff, 0, localFileHeader.getCrc());
      byteArrayOutputStream.write(longBuff, 0, 4);

      boolean writeZip64Header = localFileHeader.getCompressedSize() >= ZIP_64_SIZE_LIMIT
          || localFileHeader.getUncompressedSize() >= ZIP_64_SIZE_LIMIT;

      if (writeZip64Header) {
        rawIO.writeLongLittleEndian(longBuff, 0, ZIP_64_SIZE_LIMIT);

        //Set the uncompressed size to ZipConstants.ZIP_64_SIZE_LIMIT as
        //these values will be stored in Zip64 extra record
        byteArrayOutputStream.write(longBuff, 0, 4);
        byteArrayOutputStream.write(longBuff, 0, 4);

        zipModel.setZip64Format(true);
        localFileHeader.setWriteCompressedSizeInZip64ExtraRecord(true);
      } else {
        rawIO.writeLongLittleEndian(longBuff, 0, localFileHeader.getCompressedSize());
        byteArrayOutputStream.write(longBuff, 0, 4);

        rawIO.writeLongLittleEndian(longBuff, 0, localFileHeader.getUncompressedSize());
        byteArrayOutputStream.write(longBuff, 0, 4);

        localFileHeader.setWriteCompressedSizeInZip64ExtraRecord(false);
      }

      byte[] fileNameBytes = new byte[0];
      if (isStringNotNullAndNotEmpty(localFileHeader.getFileName())) {
        fileNameBytes = getBytesFromString(localFileHeader.getFileName(), charset);
      }
      rawIO.writeShortLittleEndian(byteArrayOutputStream, fileNameBytes.length);

      int extraFieldLength = 0;
      if (writeZip64Header) {
        extraFieldLength += ZIP64_EXTRA_DATA_RECORD_SIZE_LFH + 4; // 4 for signature + size of record
      }
      if (localFileHeader.getAesExtraDataRecord() != null) {
        extraFieldLength += AES_EXTRA_DATA_RECORD_SIZE;
      }
      rawIO.writeShortLittleEndian(byteArrayOutputStream, extraFieldLength);

      if (fileNameBytes.length > 0) {
        byteArrayOutputStream.write(fileNameBytes);
      }

      //Zip64 should be the first extra data record that should be written
      //This is NOT according to any specification but if this is changed
      //corresponding logic for updateLocalFileHeader for compressed size
      //has to be modified as well
      if (writeZip64Header) {
        rawIO.writeShortLittleEndian(byteArrayOutputStream,
            (int) HeaderSignature.ZIP64_EXTRA_FIELD_SIGNATURE.getValue());
        rawIO.writeShortLittleEndian(byteArrayOutputStream, ZIP64_EXTRA_DATA_RECORD_SIZE_LFH);
        rawIO.writeLongLittleEndian(byteArrayOutputStream, localFileHeader.getUncompressedSize());
        rawIO.writeLongLittleEndian(byteArrayOutputStream, localFileHeader.getCompressedSize());
      }

      if (localFileHeader.getAesExtraDataRecord() != null) {
        AESExtraDataRecord aesExtraDataRecord = localFileHeader.getAesExtraDataRecord();
        rawIO.writeShortLittleEndian(byteArrayOutputStream, (int) aesExtraDataRecord.getSignature().getValue());
        rawIO.writeShortLittleEndian(byteArrayOutputStream, aesExtraDataRecord.getDataSize());
        rawIO.writeShortLittleEndian(byteArrayOutputStream, aesExtraDataRecord.getAesVersion().getVersionNumber());
        byteArrayOutputStream.write(getBytesFromString(aesExtraDataRecord.getVendorID(), charset));

        byte[] aesStrengthBytes = new byte[1];
        aesStrengthBytes[0] = (byte) aesExtraDataRecord.getAesKeyStrength().getRawCode();
        byteArrayOutputStream.write(aesStrengthBytes);

        rawIO.writeShortLittleEndian(byteArrayOutputStream, aesExtraDataRecord.getCompressionMethod().getCode());
      }

      outputStream.write(byteArrayOutputStream.toByteArray());
    }
  }

  public void writeExtendedLocalHeader(LocalFileHeader localFileHeader, OutputStream outputStream)
      throws IOException {

    if (localFileHeader == null || outputStream == null) {
      throw new ZipException("input parameters is null, cannot write extended local header");
    }

    try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
      rawIO.writeIntLittleEndian(byteArrayOutputStream, (int) HeaderSignature.EXTRA_DATA_RECORD.getValue());

      rawIO.writeLongLittleEndian(longBuff, 0, localFileHeader.getCrc());
      byteArrayOutputStream.write(longBuff, 0, 4);

      if (localFileHeader.isWriteCompressedSizeInZip64ExtraRecord()) {
        rawIO.writeLongLittleEndian(byteArrayOutputStream, localFileHeader.getCompressedSize());
        rawIO.writeLongLittleEndian(byteArrayOutputStream, localFileHeader.getUncompressedSize());
      } else {
        rawIO.writeLongLittleEndian(longBuff, 0, localFileHeader.getCompressedSize());
        byteArrayOutputStream.write(longBuff, 0, 4);

        rawIO.writeLongLittleEndian(longBuff, 0, localFileHeader.getUncompressedSize());
        byteArrayOutputStream.write(longBuff, 0, 4);
      }

      outputStream.write(byteArrayOutputStream.toByteArray());
    }
  }

  public void finalizeZipFile(ZipModel zipModel, OutputStream outputStream, Charset charset) throws IOException {
    if (zipModel == null || outputStream == null) {
      throw new ZipException("input parameters is null, cannot finalize zip file");
    }

    try(ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
      processHeaderData(zipModel, outputStream);
      long offsetCentralDir = getOffsetOfCentralDirectory(zipModel);
      writeCentralDirectory(zipModel, byteArrayOutputStream, rawIO, charset);
      int sizeOfCentralDir = byteArrayOutputStream.size();

      if (zipModel.isZip64Format() || offsetCentralDir >= InternalZipConstants.ZIP_64_SIZE_LIMIT
          || zipModel.getCentralDirectory().getFileHeaders().size() >= InternalZipConstants.ZIP_64_NUMBER_OF_ENTRIES_LIMIT) {

        if (zipModel.getZip64EndOfCentralDirectoryRecord() == null) {
          zipModel.setZip64EndOfCentralDirectoryRecord(new Zip64EndOfCentralDirectoryRecord());
        }
        if (zipModel.getZip64EndOfCentralDirectoryLocator() == null) {
          zipModel.setZip64EndOfCentralDirectoryLocator(new Zip64EndOfCentralDirectoryLocator());
        }

        zipModel.getZip64EndOfCentralDirectoryLocator().setOffsetZip64EndOfCentralDirectoryRecord(offsetCentralDir
            + sizeOfCentralDir);

        if (isSplitZipFile(outputStream)) {
          int currentSplitFileCounter = getCurrentSplitFileCounter(outputStream);
          zipModel.getZip64EndOfCentralDirectoryLocator().setNumberOfDiskStartOfZip64EndOfCentralDirectoryRecord(
              currentSplitFileCounter);
          zipModel.getZip64EndOfCentralDirectoryLocator().setTotalNumberOfDiscs(currentSplitFileCounter + 1);
        } else {
          zipModel.getZip64EndOfCentralDirectoryLocator().setNumberOfDiskStartOfZip64EndOfCentralDirectoryRecord(0);
          zipModel.getZip64EndOfCentralDirectoryLocator().setTotalNumberOfDiscs(1);
        }

        Zip64EndOfCentralDirectoryRecord zip64EndOfCentralDirectoryRecord = buildZip64EndOfCentralDirectoryRecord(zipModel,
            sizeOfCentralDir, offsetCentralDir);
        zipModel.setZip64EndOfCentralDirectoryRecord(zip64EndOfCentralDirectoryRecord);
        writeZip64EndOfCentralDirectoryRecord(zip64EndOfCentralDirectoryRecord, byteArrayOutputStream,rawIO);
        writeZip64EndOfCentralDirectoryLocator(zipModel.getZip64EndOfCentralDirectoryLocator(), byteArrayOutputStream, rawIO);
      }

      writeEndOfCentralDirectoryRecord(zipModel, sizeOfCentralDir, offsetCentralDir, byteArrayOutputStream, rawIO, charset);
      writeZipHeaderBytes(zipModel, outputStream, byteArrayOutputStream.toByteArray(), charset);
    }
  }

  public void finalizeZipFileWithoutValidations(ZipModel zipModel, OutputStream outputStream, Charset charset) throws IOException {

    if (zipModel == null || outputStream == null) {
      throw new ZipException("input parameters is null, cannot finalize zip file without validations");
    }

    try(ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
      long offsetCentralDir = getOffsetOfCentralDirectory(zipModel);
      writeCentralDirectory(zipModel, byteArrayOutputStream, rawIO, charset);
      int sizeOfCentralDir = byteArrayOutputStream.size();

      if (zipModel.isZip64Format() || offsetCentralDir >= InternalZipConstants.ZIP_64_SIZE_LIMIT
          || zipModel.getCentralDirectory().getFileHeaders().size() >= InternalZipConstants.ZIP_64_NUMBER_OF_ENTRIES_LIMIT) {

        if (zipModel.getZip64EndOfCentralDirectoryRecord() == null) {
          zipModel.setZip64EndOfCentralDirectoryRecord(new Zip64EndOfCentralDirectoryRecord());
        }
        if (zipModel.getZip64EndOfCentralDirectoryLocator() == null) {
          zipModel.setZip64EndOfCentralDirectoryLocator(new Zip64EndOfCentralDirectoryLocator());
        }

        zipModel.getZip64EndOfCentralDirectoryLocator().setOffsetZip64EndOfCentralDirectoryRecord(offsetCentralDir
            + sizeOfCentralDir);

        Zip64EndOfCentralDirectoryRecord zip64EndOfCentralDirectoryRecord = buildZip64EndOfCentralDirectoryRecord(zipModel,
            sizeOfCentralDir, offsetCentralDir);
        zipModel.setZip64EndOfCentralDirectoryRecord(zip64EndOfCentralDirectoryRecord);
        writeZip64EndOfCentralDirectoryRecord(zip64EndOfCentralDirectoryRecord, byteArrayOutputStream,rawIO);
        writeZip64EndOfCentralDirectoryLocator(zipModel.getZip64EndOfCentralDirectoryLocator(), byteArrayOutputStream, rawIO);
      }

      writeEndOfCentralDirectoryRecord(zipModel, sizeOfCentralDir, offsetCentralDir, byteArrayOutputStream, rawIO, charset);
      writeZipHeaderBytes(zipModel, outputStream, byteArrayOutputStream.toByteArray(), charset);
    }
  }

  public void updateLocalFileHeader(FileHeader fileHeader, ZipModel zipModel, SplitOutputStream outputStream)
      throws IOException {

    if (fileHeader == null || zipModel == null) {
      throw new ZipException("invalid input parameters, cannot update local file header");
    }

    boolean closeFlag = false;
    SplitOutputStream currOutputStream;

    if (fileHeader.getDiskNumberStart() != outputStream.getCurrentSplitFileCounter()) {
      String parentFile = zipModel.getZipFile().getParent();
      String fileNameWithoutExt = getZipFileNameWithoutExtension(zipModel.getZipFile().getName());
      String fileName = "";
      if (parentFile != null) {
        fileName = parentFile + System.getProperty("file.separator");
      }
      if (fileHeader.getDiskNumberStart() < 9) {
        fileName += fileNameWithoutExt + ".z0" + (fileHeader.getDiskNumberStart() + 1);
      } else {
        fileName += fileNameWithoutExt + ".z" + (fileHeader.getDiskNumberStart() + 1);
      }
      currOutputStream = new SplitOutputStream(new File(fileName));
      closeFlag = true;
    } else {
      currOutputStream = outputStream;
    }

    long currOffset = currOutputStream.getFilePointer();

    currOutputStream.seek(fileHeader.getOffsetLocalHeader() + InternalZipConstants.UPDATE_LFH_CRC);
    rawIO.writeLongLittleEndian(longBuff, 0, fileHeader.getCrc());
    currOutputStream.write(longBuff, 0, 4);

    updateFileSizesInLocalFileHeader(currOutputStream, fileHeader);

    if (closeFlag) {
      currOutputStream.close();
    } else {
      outputStream.seek(currOffset);
    }
  }

  private void updateFileSizesInLocalFileHeader(SplitOutputStream outputStream, FileHeader fileHeader)
      throws IOException {

    if (fileHeader.getUncompressedSize() >= ZIP_64_SIZE_LIMIT) {
      rawIO.writeLongLittleEndian(longBuff, 0, ZIP_64_SIZE_LIMIT);
      outputStream.write(longBuff, 0, 4);
      outputStream.write(longBuff, 0, 4);

      //2 - file name length
      //2 - extra field length
      //variable - file name which can be determined by fileNameLength
      //2 - Zip64 signature
      //2 - size of zip64 data
      //8 - uncompressed size
      //8 - compressed size
      int zip64CompressedSizeOffset = 2 + 2 + fileHeader.getFileNameLength() + 2 + 2;
      if (outputStream.skipBytes(zip64CompressedSizeOffset) != zip64CompressedSizeOffset) {
        throw new ZipException("Unable to skip " + zip64CompressedSizeOffset + " bytes to update LFH");
      }
      rawIO.writeLongLittleEndian(outputStream, fileHeader.getUncompressedSize());
      rawIO.writeLongLittleEndian(outputStream, fileHeader.getCompressedSize());
    } else {
      rawIO.writeLongLittleEndian(longBuff, 0, fileHeader.getCompressedSize());
      outputStream.write(longBuff, 0, 4);

      rawIO.writeLongLittleEndian(longBuff, 0, fileHeader.getUncompressedSize());
      outputStream.write(longBuff, 0, 4);
    }
  }

  private boolean isSplitZipFile(OutputStream outputStream) {
    if (outputStream instanceof SplitOutputStream) {
      return ((SplitOutputStream) outputStream).isSplitZipFile();
    } else if (outputStream instanceof CountingOutputStream) {
      return ((CountingOutputStream) outputStream).isSplitZipFile();
    }

    return false;
  }

  private int getCurrentSplitFileCounter(OutputStream outputStream) {
    if (outputStream instanceof SplitOutputStream) {
      return ((SplitOutputStream) outputStream).getCurrentSplitFileCounter();
    }
    return ((CountingOutputStream) outputStream).getCurrentSplitFileCounter();
  }

  private void writeZipHeaderBytes(ZipModel zipModel, OutputStream outputStream, byte[] buff, Charset charset)
      throws IOException {

    if (buff == null) {
      throw new ZipException("invalid buff to write as zip headers");
    }

    if (outputStream instanceof CountingOutputStream) {
      if (((CountingOutputStream) outputStream).checkBuffSizeAndStartNextSplitFile(buff.length)) {
        finalizeZipFile(zipModel, outputStream, charset);
        return;
      }
    }

    outputStream.write(buff);
  }

  private void processHeaderData(ZipModel zipModel, OutputStream outputStream) throws IOException {
    int currentSplitFileCounter = 0;
    if (outputStream instanceof OutputStreamWithSplitZipSupport) {
      zipModel.getEndOfCentralDirectoryRecord().setOffsetOfStartOfCentralDirectory(
          ((OutputStreamWithSplitZipSupport) outputStream).getFilePointer());
      currentSplitFileCounter = ((OutputStreamWithSplitZipSupport) outputStream).getCurrentSplitFileCounter();
    }

    if (zipModel.isZip64Format()) {
      if (zipModel.getZip64EndOfCentralDirectoryRecord() == null) {
        zipModel.setZip64EndOfCentralDirectoryRecord(new Zip64EndOfCentralDirectoryRecord());
      }
      if (zipModel.getZip64EndOfCentralDirectoryLocator() == null) {
        zipModel.setZip64EndOfCentralDirectoryLocator(new Zip64EndOfCentralDirectoryLocator());
      }

      zipModel.getZip64EndOfCentralDirectoryRecord().setOffsetStartCentralDirectoryWRTStartDiskNumber(
          zipModel.getEndOfCentralDirectoryRecord().getOffsetOfStartOfCentralDirectory());
      zipModel.getZip64EndOfCentralDirectoryLocator().setNumberOfDiskStartOfZip64EndOfCentralDirectoryRecord(
          currentSplitFileCounter);
      zipModel.getZip64EndOfCentralDirectoryLocator().setTotalNumberOfDiscs(currentSplitFileCounter + 1);
    }
    zipModel.getEndOfCentralDirectoryRecord().setNumberOfThisDisk(currentSplitFileCounter);
    zipModel.getEndOfCentralDirectoryRecord().setNumberOfThisDiskStartOfCentralDir(currentSplitFileCounter);
  }

  private void writeCentralDirectory(ZipModel zipModel, ByteArrayOutputStream byteArrayOutputStream, RawIO rawIO,
                                     Charset charset) throws ZipException {

    if (zipModel.getCentralDirectory() == null || zipModel.getCentralDirectory().getFileHeaders() == null
        || zipModel.getCentralDirectory().getFileHeaders().size() <= 0) {
      return;
    }

    for (FileHeader fileHeader: zipModel.getCentralDirectory().getFileHeaders()) {
      writeFileHeader(zipModel, fileHeader, byteArrayOutputStream, rawIO, charset);
    }
  }

  private void writeFileHeader(ZipModel zipModel, FileHeader fileHeader, ByteArrayOutputStream byteArrayOutputStream,
                              RawIO rawIO, Charset charset) throws ZipException {
    if (fileHeader == null) {
      throw new ZipException("input parameters is null, cannot write local file header");
    }

    try {
      final byte[] emptyShortByte = {0, 0};
      boolean writeZip64ExtendedInfo = isZip64Entry(fileHeader);

      rawIO.writeIntLittleEndian(byteArrayOutputStream, (int) fileHeader.getSignature().getValue());
      rawIO.writeShortLittleEndian(byteArrayOutputStream, fileHeader.getVersionMadeBy());
      rawIO.writeShortLittleEndian(byteArrayOutputStream, fileHeader.getVersionNeededToExtract());
      byteArrayOutputStream.write(fileHeader.getGeneralPurposeFlag());
      rawIO.writeShortLittleEndian(byteArrayOutputStream, fileHeader.getCompressionMethod().getCode());

      rawIO.writeLongLittleEndian(longBuff, 0, fileHeader.getLastModifiedTime());
      byteArrayOutputStream.write(longBuff, 0, 4);

      rawIO.writeLongLittleEndian(longBuff, 0, fileHeader.getCrc());
      byteArrayOutputStream.write(longBuff, 0, 4);

      if (writeZip64ExtendedInfo) {
        rawIO.writeLongLittleEndian(longBuff, 0, ZIP_64_SIZE_LIMIT);
        byteArrayOutputStream.write(longBuff, 0, 4);
        byteArrayOutputStream.write(longBuff, 0, 4);
        zipModel.setZip64Format(true);
      } else {
        rawIO.writeLongLittleEndian(longBuff, 0, fileHeader.getCompressedSize());
        byteArrayOutputStream.write(longBuff, 0, 4);
        rawIO.writeLongLittleEndian(longBuff, 0, fileHeader.getUncompressedSize());
        byteArrayOutputStream.write(longBuff, 0, 4);
      }

      byte[] fileNameBytes = new byte[0];
      if (isStringNotNullAndNotEmpty(fileHeader.getFileName())) {
        fileNameBytes = getBytesFromString(fileHeader.getFileName(), charset);
      }
      rawIO.writeShortLittleEndian(byteArrayOutputStream, fileNameBytes.length);

      //Compute offset bytes before extra field is written for Zip64 compatibility
      //NOTE: this data is not written now, but written at a later point
      byte[] offsetLocalHeaderBytes = new byte[4];
      if (writeZip64ExtendedInfo) {
        rawIO.writeLongLittleEndian(longBuff, 0, ZIP_64_SIZE_LIMIT);
        System.arraycopy(longBuff, 0, offsetLocalHeaderBytes, 0, 4);
      } else {
        rawIO.writeLongLittleEndian(longBuff, 0, fileHeader.getOffsetLocalHeader());
        System.arraycopy(longBuff, 0, offsetLocalHeaderBytes, 0, 4);
      }

      int extraFieldLength = calculateExtraDataRecordsSize(fileHeader, writeZip64ExtendedInfo);
      rawIO.writeShortLittleEndian(byteArrayOutputStream, extraFieldLength);

      String fileComment = fileHeader.getFileComment();
      byte[] fileCommentBytes = new byte[0];
      if (isStringNotNullAndNotEmpty(fileComment)) {
        fileCommentBytes = getBytesFromString(fileComment, charset);
      }
      rawIO.writeShortLittleEndian(byteArrayOutputStream, fileCommentBytes.length);

      if (writeZip64ExtendedInfo) {
        rawIO.writeIntLittleEndian(intBuff, 0, ZIP_64_NUMBER_OF_ENTRIES_LIMIT);
        byteArrayOutputStream.write(intBuff, 0, 2);
      } else {
        rawIO.writeShortLittleEndian(byteArrayOutputStream, fileHeader.getDiskNumberStart());
      }

      byteArrayOutputStream.write(emptyShortByte);

      //External file attributes
      byteArrayOutputStream.write(fileHeader.getExternalFileAttributes());

      //offset local header - this data is computed above
      byteArrayOutputStream.write(offsetLocalHeaderBytes);

      if (fileNameBytes.length > 0) {
        byteArrayOutputStream.write(fileNameBytes);
      }

      if (writeZip64ExtendedInfo) {
        zipModel.setZip64Format(true);

        //Zip64 header
        rawIO.writeShortLittleEndian(byteArrayOutputStream,
            (int) HeaderSignature.ZIP64_EXTRA_FIELD_SIGNATURE.getValue());

        //size of data
        rawIO.writeShortLittleEndian(byteArrayOutputStream, ZIP64_EXTRA_DATA_RECORD_SIZE_FH);
        rawIO.writeLongLittleEndian(byteArrayOutputStream, fileHeader.getUncompressedSize());
        rawIO.writeLongLittleEndian(byteArrayOutputStream, fileHeader.getCompressedSize());
        rawIO.writeLongLittleEndian(byteArrayOutputStream, fileHeader.getOffsetLocalHeader());
        rawIO.writeIntLittleEndian(byteArrayOutputStream, fileHeader.getDiskNumberStart());
      }

      if (fileHeader.getAesExtraDataRecord() != null) {
        AESExtraDataRecord aesExtraDataRecord = fileHeader.getAesExtraDataRecord();
        rawIO.writeShortLittleEndian(byteArrayOutputStream, (int) aesExtraDataRecord.getSignature().getValue());
        rawIO.writeShortLittleEndian(byteArrayOutputStream, aesExtraDataRecord.getDataSize());
        rawIO.writeShortLittleEndian(byteArrayOutputStream, aesExtraDataRecord.getAesVersion().getVersionNumber());
        byteArrayOutputStream.write(getBytesFromString(aesExtraDataRecord.getVendorID(), charset));

        byte[] aesStrengthBytes = new byte[1];
        aesStrengthBytes[0] = (byte) aesExtraDataRecord.getAesKeyStrength().getRawCode();
        byteArrayOutputStream.write(aesStrengthBytes);

        rawIO.writeShortLittleEndian(byteArrayOutputStream, aesExtraDataRecord.getCompressionMethod().getCode());
      }

      writeRemainingExtraDataRecordsIfPresent(fileHeader, byteArrayOutputStream);

      if (fileCommentBytes.length > 0) {
        byteArrayOutputStream.write(fileCommentBytes);
      }
    } catch (Exception e) {
      throw new ZipException(e);
    }
  }

  private int calculateExtraDataRecordsSize(FileHeader fileHeader, boolean writeZip64ExtendedInfo) {
    int extraFieldLength = 0;

    if (writeZip64ExtendedInfo) {
      extraFieldLength += ZIP64_EXTRA_DATA_RECORD_SIZE_FH + 4; // 4 for signature + size of record
    }

    if (fileHeader.getAesExtraDataRecord() != null) {
      extraFieldLength += AES_EXTRA_DATA_RECORD_SIZE;
    }

    if (fileHeader.getExtraDataRecords() != null) {
      for (ExtraDataRecord extraDataRecord : fileHeader.getExtraDataRecords()) {
        if (extraDataRecord.getHeader() == HeaderSignature.AES_EXTRA_DATA_RECORD.getValue()
            || extraDataRecord.getHeader() == HeaderSignature.ZIP64_EXTRA_FIELD_SIGNATURE.getValue()) {
          continue;
        }

        extraFieldLength += 4 + extraDataRecord.getSizeOfData(); // 4  = 2 for header + 2 for size of data
      }
    }

    return extraFieldLength;
  }

  private void writeRemainingExtraDataRecordsIfPresent(FileHeader fileHeader, OutputStream outputStream)
      throws IOException {
    if (fileHeader.getExtraDataRecords() == null || fileHeader.getExtraDataRecords().size() == 0) {
      return;
    }

    for (ExtraDataRecord extraDataRecord : fileHeader.getExtraDataRecords()) {
      if (extraDataRecord.getHeader() == HeaderSignature.AES_EXTRA_DATA_RECORD.getValue()
            || extraDataRecord.getHeader() == HeaderSignature.ZIP64_EXTRA_FIELD_SIGNATURE.getValue()) {
        continue;
      }

      rawIO.writeShortLittleEndian(outputStream, (int) extraDataRecord.getHeader());
      rawIO.writeShortLittleEndian(outputStream, extraDataRecord.getSizeOfData());

      if (extraDataRecord.getSizeOfData() > 0 && extraDataRecord.getData() != null) {
        outputStream.write(extraDataRecord.getData());
      }
    }
  }

  private void writeZip64EndOfCentralDirectoryRecord(Zip64EndOfCentralDirectoryRecord zip64EndOfCentralDirectoryRecord,
                                                     ByteArrayOutputStream byteArrayOutputStream, RawIO rawIO) throws IOException {
    rawIO.writeIntLittleEndian(byteArrayOutputStream, (int) zip64EndOfCentralDirectoryRecord.getSignature().getValue());
    rawIO.writeLongLittleEndian(byteArrayOutputStream, zip64EndOfCentralDirectoryRecord.getSizeOfZip64EndCentralDirectoryRecord());
    rawIO.writeShortLittleEndian(byteArrayOutputStream, zip64EndOfCentralDirectoryRecord.getVersionMadeBy());
    rawIO.writeShortLittleEndian(byteArrayOutputStream, zip64EndOfCentralDirectoryRecord.getVersionNeededToExtract());
    rawIO.writeIntLittleEndian(byteArrayOutputStream, zip64EndOfCentralDirectoryRecord.getNumberOfThisDisk());
    rawIO.writeIntLittleEndian(byteArrayOutputStream, zip64EndOfCentralDirectoryRecord.getNumberOfThisDiskStartOfCentralDirectory());
    rawIO.writeLongLittleEndian(byteArrayOutputStream, zip64EndOfCentralDirectoryRecord.getTotalNumberOfEntriesInCentralDirectoryOnThisDisk());
    rawIO.writeLongLittleEndian(byteArrayOutputStream, zip64EndOfCentralDirectoryRecord.getTotalNumberOfEntriesInCentralDirectory());
    rawIO.writeLongLittleEndian(byteArrayOutputStream, zip64EndOfCentralDirectoryRecord.getSizeOfCentralDirectory());
    rawIO.writeLongLittleEndian(byteArrayOutputStream, zip64EndOfCentralDirectoryRecord.getOffsetStartCentralDirectoryWRTStartDiskNumber());
  }

  private void writeZip64EndOfCentralDirectoryLocator(Zip64EndOfCentralDirectoryLocator zip64EndOfCentralDirectoryLocator,
                                                      ByteArrayOutputStream byteArrayOutputStream,
                                                      RawIO rawIO) throws IOException {
    rawIO.writeIntLittleEndian(byteArrayOutputStream, (int) HeaderSignature.ZIP64_END_CENTRAL_DIRECTORY_LOCATOR.getValue());
    rawIO.writeIntLittleEndian(byteArrayOutputStream,
        zip64EndOfCentralDirectoryLocator.getNumberOfDiskStartOfZip64EndOfCentralDirectoryRecord());
    rawIO.writeLongLittleEndian(byteArrayOutputStream,
        zip64EndOfCentralDirectoryLocator.getOffsetZip64EndOfCentralDirectoryRecord());
    rawIO.writeIntLittleEndian(byteArrayOutputStream,
        zip64EndOfCentralDirectoryLocator.getTotalNumberOfDiscs());

  }

  private void writeEndOfCentralDirectoryRecord(ZipModel zipModel, int sizeOfCentralDir, long offsetCentralDir,
                                                ByteArrayOutputStream byteArrayOutputStream, RawIO rawIO,
                                                Charset charset)
      throws IOException {

    byte[] longByte = new byte[8];
    rawIO.writeIntLittleEndian(byteArrayOutputStream, (int) HeaderSignature.END_OF_CENTRAL_DIRECTORY.getValue());
    rawIO.writeShortLittleEndian(byteArrayOutputStream,
        zipModel.getEndOfCentralDirectoryRecord().getNumberOfThisDisk());
    rawIO.writeShortLittleEndian(byteArrayOutputStream,
        zipModel.getEndOfCentralDirectoryRecord().getNumberOfThisDiskStartOfCentralDir());

    long numEntries = zipModel.getCentralDirectory().getFileHeaders().size();
    long numEntriesOnThisDisk = numEntries;
    if (zipModel.isSplitArchive()) {
      numEntriesOnThisDisk = countNumberOfFileHeaderEntriesOnDisk(zipModel.getCentralDirectory().getFileHeaders(),
          zipModel.getEndOfCentralDirectoryRecord().getNumberOfThisDisk());
    }

    if (numEntriesOnThisDisk > InternalZipConstants.ZIP_64_NUMBER_OF_ENTRIES_LIMIT) {
      numEntriesOnThisDisk = InternalZipConstants.ZIP_64_NUMBER_OF_ENTRIES_LIMIT;
    }
    rawIO.writeShortLittleEndian(byteArrayOutputStream, (int) numEntriesOnThisDisk);

    if (numEntries > InternalZipConstants.ZIP_64_NUMBER_OF_ENTRIES_LIMIT) {
      numEntries = InternalZipConstants.ZIP_64_NUMBER_OF_ENTRIES_LIMIT;
    }
    rawIO.writeShortLittleEndian(byteArrayOutputStream, (int) numEntries);

    rawIO.writeIntLittleEndian(byteArrayOutputStream, sizeOfCentralDir);
    if (offsetCentralDir > ZIP_64_SIZE_LIMIT) {
      rawIO.writeLongLittleEndian(longByte, 0, ZIP_64_SIZE_LIMIT);
      byteArrayOutputStream.write(longByte, 0, 4);
    } else {
      rawIO.writeLongLittleEndian(longByte, 0, offsetCentralDir);
      byteArrayOutputStream.write(longByte, 0, 4);
    }

    String comment = zipModel.getEndOfCentralDirectoryRecord().getComment();
    if (isStringNotNullAndNotEmpty(comment)) {
      byte[] commentBytes = getBytesFromString(comment, charset);
      rawIO.writeShortLittleEndian(byteArrayOutputStream, commentBytes.length);
      byteArrayOutputStream.write(commentBytes);
    } else {
      rawIO.writeShortLittleEndian(byteArrayOutputStream, 0);
    }
  }

  private long countNumberOfFileHeaderEntriesOnDisk(List<FileHeader> fileHeaders, int numOfDisk) throws ZipException {
    if (fileHeaders == null) {
      throw new ZipException("file headers are null, cannot calculate number of entries on this disk");
    }

    int noEntries = 0;
    for (FileHeader fileHeader : fileHeaders) {
      if (fileHeader.getDiskNumberStart() == numOfDisk) {
        noEntries++;
      }
    }
    return noEntries;
  }

  private boolean isZip64Entry(FileHeader fileHeader) {
    return fileHeader.getCompressedSize() >= ZIP_64_SIZE_LIMIT
        || fileHeader.getUncompressedSize() >= ZIP_64_SIZE_LIMIT
        || fileHeader.getOffsetLocalHeader() >= ZIP_64_SIZE_LIMIT
        || fileHeader.getDiskNumberStart() >= ZIP_64_NUMBER_OF_ENTRIES_LIMIT;
  }

  private long getOffsetOfCentralDirectory(ZipModel zipModel) {
    if (zipModel.isZip64Format()
        && zipModel.getZip64EndOfCentralDirectoryRecord() != null
        && zipModel.getZip64EndOfCentralDirectoryRecord().getOffsetStartCentralDirectoryWRTStartDiskNumber() != -1) {
      return zipModel.getZip64EndOfCentralDirectoryRecord().getOffsetStartCentralDirectoryWRTStartDiskNumber();
    }

    return zipModel.getEndOfCentralDirectoryRecord().getOffsetOfStartOfCentralDirectory();
  }

  private Zip64EndOfCentralDirectoryRecord buildZip64EndOfCentralDirectoryRecord(ZipModel zipModel, int sizeOfCentralDir,
                                                                                 long offsetCentralDir) throws ZipException {

    Zip64EndOfCentralDirectoryRecord zip64EndOfCentralDirectoryRecord = new Zip64EndOfCentralDirectoryRecord();

    zip64EndOfCentralDirectoryRecord.setSignature(HeaderSignature.ZIP64_END_CENTRAL_DIRECTORY_RECORD);
    zip64EndOfCentralDirectoryRecord.setSizeOfZip64EndCentralDirectoryRecord(44);

    if (zipModel.getCentralDirectory() != null &&
        zipModel.getCentralDirectory().getFileHeaders() != null &&
        zipModel.getCentralDirectory().getFileHeaders().size() > 0) {
      FileHeader firstFileHeader = zipModel.getCentralDirectory().getFileHeaders().get(0);
      zip64EndOfCentralDirectoryRecord.setVersionMadeBy(firstFileHeader.getVersionMadeBy());
      zip64EndOfCentralDirectoryRecord.setVersionNeededToExtract(firstFileHeader.getVersionNeededToExtract());
    }

    zip64EndOfCentralDirectoryRecord.setNumberOfThisDisk(zipModel.getEndOfCentralDirectoryRecord().getNumberOfThisDisk());
    zip64EndOfCentralDirectoryRecord.setNumberOfThisDiskStartOfCentralDirectory(zipModel.getEndOfCentralDirectoryRecord()
        .getNumberOfThisDiskStartOfCentralDir());

    long numEntries = zipModel.getCentralDirectory().getFileHeaders().size();
    long numEntriesOnThisDisk = numEntries;
    if (zipModel.isSplitArchive()) {
      numEntriesOnThisDisk = countNumberOfFileHeaderEntriesOnDisk(zipModel.getCentralDirectory().getFileHeaders(),
          zipModel.getEndOfCentralDirectoryRecord().getNumberOfThisDisk());
    }

    zip64EndOfCentralDirectoryRecord.setTotalNumberOfEntriesInCentralDirectoryOnThisDisk(numEntriesOnThisDisk);
    zip64EndOfCentralDirectoryRecord.setTotalNumberOfEntriesInCentralDirectory(numEntries);
    zip64EndOfCentralDirectoryRecord.setSizeOfCentralDirectory(sizeOfCentralDir);
    zip64EndOfCentralDirectoryRecord.setOffsetStartCentralDirectoryWRTStartDiskNumber(offsetCentralDir);

    return zip64EndOfCentralDirectoryRecord;
  }
}
