/*
 * Copyright (C) 2010 Ken Ellinwood
 *
 * 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 kellinwood.zipio;

import kellinwood.logging.LoggerInterface;
import kellinwood.logging.LoggerManager;

import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.jar.Manifest;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 *
 */
public class ZipInput implements Closeable {
    static LoggerInterface log;

    public String inputFilename;
    RandomAccessFile in = null;
    long fileLength;
    int scanIterations = 0;

    Map<String, ZioEntry> zioEntries = new LinkedHashMap<String, ZioEntry>();
    CentralEnd centralEnd;
    Manifest manifest;

    public ZipInput(String filename) throws IOException {
        this.inputFilename = filename;
        in = new RandomAccessFile(new File(inputFilename), "r");
        fileLength = in.length();
    }

    private static LoggerInterface getLogger() {
        if (log == null) log = LoggerManager.getLogger(ZipInput.class.getName());
        return log;
    }

    public String getFilename() {
        return inputFilename;
    }

    public long getFileLength() {
        return fileLength;
    }

    public static ZipInput read(String filename) throws IOException {
        ZipInput zipInput = new ZipInput(filename);
        zipInput.doRead();
        return zipInput;
    }


    public ZioEntry getEntry(String filename) {
        return zioEntries.get(filename);
    }

    public Map<String, ZioEntry> getEntries() {
        return zioEntries;
    }

    /**
     * Returns the names of immediate children in the directory with the given name.
     * The path value must end with a "/" character.  Use a value of "/"
     * to get the root entries.
     */
    public Collection<String> list(String path) {
        if (!path.endsWith("/")) throw new IllegalArgumentException("Invalid path -- does not end with '/'");

        if (path.startsWith("/")) path = path.substring(1);

        Pattern p = Pattern.compile(String.format("^%s([^/]+/?).*", path));

        Set<String> names = new TreeSet<String>();

        for (String name : zioEntries.keySet()) {
            Matcher m = p.matcher(name);
            if (m.matches()) names.add(m.group(1));
        }
        return names;
    }

    public Manifest getManifest() throws IOException {
        if (manifest == null) {
            ZioEntry e = zioEntries.get("META-INF/MANIFEST.MF");
            if (e != null) {
                manifest = new Manifest(e.getInputStream());
            }
        }
        return manifest;
    }

    /**
     * Scan the end of the file for the end of central directory record (EOCDR).
     * Returns the file offset of the EOCD signature.  The size parameter is an
     * initial buffer size (e.g., 256).
     */
    public long scanForEOCDR(int size) throws IOException {
        if (size > fileLength || size > 65536)
            throw new IllegalStateException("End of central directory not found in " + inputFilename);

        int scanSize = (int) Math.min(fileLength, size);

        byte[] scanBuf = new byte[scanSize];

        in.seek(fileLength - scanSize);

        in.readFully(scanBuf);

        for (int i = scanSize - 22; i >= 0; i--) {
            scanIterations += 1;
            if (scanBuf[i] == 0x50 && scanBuf[i + 1] == 0x4b && scanBuf[i + 2] == 0x05 && scanBuf[i + 3] == 0x06) {
                return fileLength - scanSize + i;
            }
        }

        return scanForEOCDR(size * 2);
    }


    private void doRead() {
        try {

            long posEOCDR = scanForEOCDR(256);
            in.seek(posEOCDR);
            centralEnd = CentralEnd.read(this);

            boolean debug = getLogger().isDebugEnabled();
            if (debug) {
                getLogger().debug(String.format(Locale.ENGLISH, "EOCD found in %d iterations", scanIterations));
                getLogger().debug(String.format(Locale.ENGLISH, "Directory entries=%d, size=%d, offset=%d/0x%08x", centralEnd.totalCentralEntries,
                        centralEnd.centralDirectorySize, centralEnd.centralStartOffset, centralEnd.centralStartOffset));

                ZipListingHelper.listHeader(getLogger());
            }

            in.seek(centralEnd.centralStartOffset);

            for (int i = 0; i < centralEnd.totalCentralEntries; i++) {
                ZioEntry entry = ZioEntry.read(this);
                zioEntries.put(entry.getName(), entry);
                if (debug) ZipListingHelper.listEntry(getLogger(), entry);
            }

        } catch (Throwable t) {
            t.printStackTrace();
        }
    }

    @Override
    public void close() {
        if (in != null) try {
            in.close();
        } catch (Throwable t) {
        }
    }

    public long getFilePointer() throws IOException {
        return in.getFilePointer();
    }

    public void seek(long position) throws IOException {
        in.seek(position);
    }

    public byte readByte() throws IOException {
        return in.readByte();
    }

    public int readInt() throws IOException {
        int result = 0;
        for (int i = 0; i < 4; i++) {
            result |= (in.readUnsignedByte() << (8 * i));
        }
        return result;
    }

    public short readShort() throws IOException {
        short result = 0;
        for (int i = 0; i < 2; i++) {
            result |= (in.readUnsignedByte() << (8 * i));
        }
        return result;
    }

    public String readString(int length) throws IOException {

        byte[] buffer = new byte[length];
        for (int i = 0; i < length; i++) {
            buffer[i] = in.readByte();
        }
        return new String(buffer);
    }

    public byte[] readBytes(int length) throws IOException {

        byte[] buffer = new byte[length];
        for (int i = 0; i < length; i++) {
            buffer[i] = in.readByte();
        }
        return buffer;
    }

    public int read(byte[] b, int offset, int length) throws IOException {
        return in.read(b, offset, length);
    }
}


