/*
 * Copyright (C) 2019 The Android Open Source Project
 *
 * 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 com.android.zipflinger;

import com.android.annotations.NonNull;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.FileChannel;

class EndOfCentralDirectory {
    private static final int SIGNATURE = 0x06054b50;
    static final int SIZE = 22;
    private static final long MAX_SIZE = Ints.USHRT_MAX + SIZE;
    static final short DISK_NUMBER = 0;

    private int numEntries;
    private Location location;
    private Location cdLocation;

    private EndOfCentralDirectory() {
        this.numEntries = 0;
        this.location = Location.INVALID;
        this.cdLocation = Location.INVALID;
    }

    private void parse(@NonNull ByteBuffer buffer) {
        // skip diskNumber (2) + cdDiskNumber (2) + #entries (2)
        buffer.position(buffer.position() + 6);
        numEntries = Ints.ushortToInt(buffer.getShort());
        long cdSize = Ints.uintToLong(buffer.getInt());
        long cdOffset = Ints.uintToLong(buffer.getInt());
        cdLocation = new Location(cdOffset, cdSize);
        buffer.position(buffer.position() + 2); // Skip comment length
    }

    @NonNull
    public Location getLocation() {
        return location;
    }

    @NonNull
    public Location getCdLocation() {
        return cdLocation;
    }

    public int numEntries() {
        return numEntries;
    }

    public void setLocation(@NonNull Location location) {
        this.location = location;
    }

    // Search the EOCD. If not found the returned object location will be set to Location.INVALID.
    @NonNull
    public static EndOfCentralDirectory find(@NonNull FileChannel channel) throws IOException {
        long fileSize = channel.size();

        EndOfCentralDirectory eocd = new EndOfCentralDirectory();
        if (fileSize < SIZE) {
            return eocd;
        }

        int sizeToRead = Math.toIntExact(Math.min(fileSize, MAX_SIZE));
        long offset = fileSize - sizeToRead;

        ByteBuffer buffer = ByteBuffer.allocate(sizeToRead).order(ByteOrder.LITTLE_ENDIAN);
        channel.read(buffer, offset);
        buffer.position(buffer.capacity() - SIZE);
        while (true) {
            int signature = buffer.getInt(); // Read 4 bytes.
            if (signature == EndOfCentralDirectory.SIGNATURE) {
                eocd.parse(buffer);
                eocd.setLocation(new Location(offset + buffer.position() - SIZE, SIZE));
                break;
            }
            if (buffer.position() <= 4) {
                break;
            }
            buffer.position(buffer.position() - Integer.BYTES - 1); // Backtrack  5 bytes.
        }
        return eocd;
    }

    @NonNull
    public static Location write(
            @NonNull ZipWriter writer, @NonNull Location cdLocation, long entriesCount)
            throws IOException {
        boolean isZip64 = Zip64.needZip64Footer(entriesCount, cdLocation);

        short numEntries = isZip64 ? Zip64.SHORT_MAGIC : Ints.longToUshort(entriesCount);
        int eocdSize = isZip64 ? Zip64.INT_MAGIC : Ints.longToUint(cdLocation.size());
        int eocdOffset = isZip64 ? Zip64.INT_MAGIC : Ints.longToUint(cdLocation.first);

        ByteBuffer eocd = ByteBuffer.allocate(SIZE).order(ByteOrder.LITTLE_ENDIAN);
        eocd.putInt(SIGNATURE);
        eocd.putShort(DISK_NUMBER);
        eocd.putShort((short) 0); // cd disk number
        eocd.putShort(numEntries);
        eocd.putShort(numEntries);
        eocd.putInt(eocdSize);
        eocd.putInt(eocdOffset);
        eocd.putShort((short) 0); // comment size

        eocd.rewind();
        long position = writer.position();
        writer.write(eocd);

        return new Location(position, SIZE);
    }

}
