/*
 * IzPack - Copyright 2001-2008 Julien Ponge, All Rights Reserved.
 * 
 * http://izpack.org/ http://izpack.codehaus.org/
 * 
 * Copyright 2007 Dennis Reil
 * 
 * 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.izforge.izpack.io;

import com.izforge.izpack.util.Debug;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.zip.GZIPInputStream;

/**
 * An inputstream which transparently spans over multiple volumes. The amount of volumes has to be
 * specified
 *
 * @author Dennis Reil, <Dennis.Reil@reddot.de>
 */
public class FileSpanningInputStream extends InputStream
{

    private static final int EOF = -1;

    protected FileInputStream fileinputstream;

    protected String volumename;

    protected int currentvolumeindex;

    protected int volumestotal;

    protected static boolean nextvolumenotfound = false;

    protected long filepointer;

    protected GZIPInputStream zippedinputstream;

    protected byte[] magicnumber;

    public FileSpanningInputStream(File volume, int volumestotal) throws IOException
    {
        fileinputstream = new FileInputStream(volume);
        zippedinputstream = new GZIPInputStream(fileinputstream);
        currentvolumeindex = 0;
        volumename = volume.getAbsolutePath();
        this.volumestotal = volumestotal;
        filepointer = 0;

        // read magic number
        this.magicnumber = new byte[FileSpanningOutputStream.MAGIC_NUMER_LENGTH];
        zippedinputstream.read(this.magicnumber);
        // this.read(this.magicnumber);
        Debug.trace("Opening stream to " + volume + " magicnr is " + magicnumber);
        // reset filepointer
        filepointer = 0;
    }

    public FileSpanningInputStream(String volumename, int volumestotal) throws IOException
    {
        this(new File(volumename), volumestotal);
    }

    /**
     * checks if the MagicNumber of this stream is valid. The stream has to be opened right before.
     *
     * @return wether the magic number is valid or not
     * @throws IOException
     */
    private boolean isMagicNumberValid() throws IOException
    {
        Debug.trace("trying to read magic number");
        boolean valid = false;
        byte[] magicnumberofvolume = new byte[FileSpanningOutputStream.MAGIC_NUMER_LENGTH];
        long oldfilepointer = this.filepointer;
        // this.read(magicnumberofvolume);
        this.zippedinputstream.read(magicnumberofvolume);
        this.filepointer = oldfilepointer;
        Debug.trace("MagicNr is " + magicnumberofvolume);
        if ((magicnumberofvolume != null) && (this.magicnumber != null))
        {
            if (magicnumberofvolume.length != this.magicnumber.length)
            {
                // magicnumbers aren't valid
                valid = false;
            }
            else
            {
                boolean errorfound = false;
                // check if magicnumbers are identical
                for (int i = 0; i < magicnumberofvolume.length; i++)
                {
                    byte op1 = magicnumberofvolume[i];
                    byte op2 = this.magicnumber[i];
                    if (op1 != op2)
                    {
                        errorfound = true;
                        break;
                    }
                }
                valid = !errorfound;
            }
        }
        return valid;
    }

    /**
     * creates an inputstream to the next volume
     *
     * @return true - an inputstream to the next volume has been created false - the last volume was
     *         reached
     * @throws IOException
     */
    private boolean createInputStreamToNextVolume() throws IOException
    {
        currentvolumeindex++;
        // have we reached the last volume?
        if (currentvolumeindex >= volumestotal)
        {
            Debug.trace("last volume reached.");
            return false;
        }
        // the next volume name
        String nextvolumename = volumename + "." + currentvolumeindex;
        Debug.trace("Trying to use next volume: " + nextvolumename);
        File nextvolumefile = new File(nextvolumename);
        if (!nextvolumefile.exists())
        {
            currentvolumeindex--;
            nextvolumenotfound = true;
            Debug.trace("volume not found");
            throw new VolumeNotFoundException(nextvolumename + "was not found.", nextvolumename);
        }
        Debug.trace("next volume found.");
        // try to open new stream to next volume
        fileinputstream = new FileInputStream(nextvolumefile);
        zippedinputstream = new GZIPInputStream(fileinputstream);
        // check magic number
        if (!this.isMagicNumberValid())
        {
            currentvolumeindex--;
            nextvolumenotfound = true;
            Debug
                    .trace("volume found, but magic number incorrect. Maybe not a volume of the same version.");
            throw new CorruptVolumeException(nextvolumename
                    + "was found, but has magic number error. Maybe not the right version?",
                    nextvolumename);
        }
        // everything fine
        nextvolumenotfound = false;
        return true;
    }

    /*
     * (non-Javadoc)
     * 
     * @see java.io.InputStream#available()
     */
    public int available() throws IOException
    {
        if (nextvolumenotfound)
        {
            createInputStreamToNextVolume();
        }
        // return fileinputstream.available();
        return zippedinputstream.available();
    }

    /*
     * (non-Javadoc)
     * 
     * @see java.io.InputStream#close()
     */
    public void close() throws IOException
    {
        zippedinputstream.close();
        fileinputstream.close();
    }

    /*
     * (non-Javadoc)
     * 
     * @see java.io.InputStream#read()
     */
    public int read() throws IOException
    {
        if (nextvolumenotfound)
        {
            // the next volume was not found, so try to create a new input stream to next volume
            createInputStreamToNextVolume();
        }
        int nextbyte = zippedinputstream.read();
        filepointer++;
        if (nextbyte == EOF)
        {
            // if end of file is reached, try to open InputStream to next volume
            // close the inputstream
            try
            {
                zippedinputstream.close();
            }
            catch (Exception e)
            {
                // do nothing
            }

            if (createInputStreamToNextVolume())
            {
                // try to read next byte
                nextbyte = zippedinputstream.read();
                filepointer++;
            }
        }
        return nextbyte;
    }

    /*
     * (non-Javadoc)
     * 
     * @see java.io.InputStream#read(byte[], int, int)
     */
    public int read(byte[] b, int off, int len) throws IOException
    {
        if (nextvolumenotfound)
        {
            // the next volume was not found, so try to create a new input stream to next volume
            createInputStreamToNextVolume();
        }
        int bytesread = zippedinputstream.read(b, off, len);
        filepointer += bytesread;
        if (bytesread == EOF)
        {
            filepointer++; // bytesread was -1;
            System.out.println("EOF reached.");
            // close the inputstream
            try
            {
                zippedinputstream.close();
            }
            catch (Exception e)
            {
                // do nothing
            }
            // try to open next volume
            if (createInputStreamToNextVolume())
            {
                // try to read next bytes
                Debug.trace("next volume opened, continuing read");
                bytesread = zippedinputstream.read(b, off, len);
                filepointer += bytesread;
                // System.out.println("read into buffer: " + bytesread + " Bytes");
            }
        }
        // System.out.println("return from read into buffer: " + bytesread + " Bytes");
        return bytesread;
    }

    /*
     * (non-Javadoc)
     * 
     * @see java.io.InputStream#read(byte[])
     */
    public int read(byte[] b) throws IOException
    {
        return this.read(b, 0, b.length);
    }

    /*
     * (non-Javadoc)
     * 
     * @see java.io.InputStream#skip(long)
     */
    public long skip(long n) throws IOException
    {
        if (nextvolumenotfound)
        {
            // the next volume was not found, so try to create a new input stream to next volume
            createInputStreamToNextVolume();
        }
        long bytesskipped = 0;
        byte[] buffer = new byte[4096];
        try
        {
            while (bytesskipped < n)
            {
                int maxBytes = (int) Math.min(n - bytesskipped, buffer.length);

                int bytesInBuffer = this.read(buffer, 0, maxBytes);
                if (bytesInBuffer == -1)
                {
                    throw new IOException("Unexpected end of stream (installer corrupted?)");
                }

                bytesskipped += bytesInBuffer;
            }
        }
        catch (VolumeNotFoundException vnfe)
        {
            vnfe.setAlreadyskippedbytes(bytesskipped);
            throw vnfe;
        }
        return bytesskipped;
    }

    /**
     * Returns the name of the volume
     *
     * @return the name of the volume
     */
    public String getVolumename()
    {
        return volumename;
    }

    /**
     * Sets the volumename
     *
     * @param volumename
     */
    public void setVolumename(String volumename)
    {
        Debug.trace("new volumename: " + volumename);
        // try to get the volumename from the given volume file
        // the first volume has no suffix, additional volumes have a .INDEX# suffix
        String volumesuffix = "." + currentvolumeindex;
        String nextvolumesuffix = "." + (currentvolumeindex + 1);
        if (volumename.endsWith(volumesuffix))
        {
            this.volumename = volumename.substring(0, volumename.lastIndexOf(volumesuffix));
        }
        else if (volumename.endsWith(nextvolumesuffix))
        {
            this.volumename = volumename.substring(0, volumename.lastIndexOf(nextvolumesuffix));
        }
        else
        {
            this.volumename = volumename;
        }
        Debug.trace("Set volumename to: " + this.volumename);
    }

    /**
     * Returns the current position in the file. Notice: this is the global position in all volumes.
     *
     * @return the current position in file.
     */
    public long getFilepointer()
    {
        return filepointer;
    }

}
