/**
 * This file is part of muCommander, http://www.mucommander.com
 *
 * muCommander is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * muCommander is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.mucommander.commons.file.archive.ar;

import com.mucommander.commons.file.archive.ArchiveEntry;
import com.mucommander.commons.file.archive.ArchiveEntryIterator;
import com.mucommander.commons.io.StreamUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.InputStream;

/**
 * An <code>ArchiveEntryIterator</code> that iterates through an AR archive. 
 *
 * @author Maxence Bernard
 */
class ArArchiveEntryIterator implements ArchiveEntryIterator {
    private static final Logger LOGGER = LoggerFactory.getLogger(ArArchiveEntryIterator.class);

    /** InputStream to the the archive file */
    private InputStream in;

    /** The current entry, where the stream is currently positionned */
    private ArchiveEntry currentEntry;

    /** GNU variant: extended filenames contained in the special // entry's data */
    private byte gnuExtendedNames[];


    /**
     * Creates a new <code>ArArchiveEntryIterator</code> that parses the given AR <code>InputStream</code>.
     * The <code>InputStream</code> will be closed by {@link #close()}.
     *
     * @param in an AR archive <code>InputStream</code>
     * @throws IOException if an I/O error occurred while initializing this iterator
     */
    ArArchiveEntryIterator(InputStream in) throws IOException {
        this.in = in;

        // Skip the global header: "!<arch>" string followed by LF char (8 characters in total).
        StreamUtils.skipFully(in, 8);
    }


    /**
     * Reads the next file header and returns an {@link ArchiveEntry} representing the entry.
     *
     * @return an ArchiveEntry representing the entry
     * @throws IOException if an error occurred
     */
    ArchiveEntry getNextEntry() throws IOException {
        byte fileHeader[] = new byte[60];

        try {
            // Fully read the 60 file header bytes. If it cannot be read, it most likely means we've reached
            // the end of the archive.
            StreamUtils.readFully(in, fileHeader);
        }
        catch(IOException e) {
            return null;
        }

        try {
            // Read the 16 filename characters and trim string to remove any trailing white space
            String name = new String(fileHeader, 0, 16).trim();

            // Read the 12 file date characters, trim string to remove any trailing white space
            // and parse date as a long.
            // If the entry is the special // GNU one (see below), date is null and thus should not be parsed
            // (would throw a NumberFormatException)
            long date = name.equals("//")?0:Long.parseLong(new String(fileHeader, 16, 12).trim()) * 1000;

            // No use for file's Owner ID, Group ID and mode at the moment, skip them

            // Read the 10 file size characters, trim string to remove any trailing white space
            // and parse size as a long
            long size = Long.parseLong(new String(fileHeader, 48, 10).trim());

            // BSD variant : BSD ar store extended filenames by placing the string "#1/" followed by the file name length
            // in the file name field, and appending the real filename to the file header.
            if(name.startsWith("#1/")) {
                // Read extended name
                int extendedNameLength = Integer.parseInt(name.substring(3, name.length()));
                name = new String(StreamUtils.readFully(in, new byte[extendedNameLength])).trim();
                // Decrease remaining file size
                size -= extendedNameLength;
            }
            // GNU variant: GNU ar stores multiple extended filenames in the data section of a file with the name "//",
            // this record is referred to by future headers. A header references an extended filename by storing a "/"
            // followed by a decimal offset to the start of the filename in the extended filename data section.
            // This entry appears first in the archive, i.e. before any other entries.
            else if(name.equals("//")) {
                this.gnuExtendedNames = StreamUtils.readFully(in, new byte[(int)size]);

                // Skip one padding byte if size is odd
                if(size%2!=0)
                    StreamUtils.skipFully(in, 1);

                // Don't return this entry which should not be visible, but recurse to return next entry instead
                return getNextEntry();
            }
            // GNU variant: entry with an extended name, look up extended name in // entry
            else if(this.gnuExtendedNames!=null && name.startsWith("/")) {
                int off = Integer.parseInt(name.substring(1, name.length()));
                name = "";
                byte b;
                while((b=this.gnuExtendedNames[off++])!='/')
                    name += (char)b;
            }

            return new ArchiveEntry(name, false, date, size, true);
        }
        // Re-throw IOException
        catch(IOException e) {
            LOGGER.info("Caught IOException", e);

            throw e;
        }
        // Catch any other exceptions (NumberFormatException for instance) and throw an IOException instead
        catch(Exception e2) {
            LOGGER.info("Caught Exception", e2);

            throw new IOException();
        }
    }


    /////////////////////////////////////////
    // ArchiveEntryIterator implementation //
    /////////////////////////////////////////

    @Override
    public ArchiveEntry nextEntry() throws IOException {
        if(currentEntry!=null) {
            // Skip the current entry's data, plus 1 padding byte if size is odd
            long size = currentEntry.getSize();
            StreamUtils.skipFully(in, size + (size%2));
        }

        // Get the next entry, if any
        currentEntry = getNextEntry();

        return currentEntry;
    }

    @Override
    public void close() throws IOException {
        in.close();
    }
}
