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

import com.mucommander.commons.file.AbstractFile;
import com.mucommander.commons.io.BufferPool;
import com.mucommander.commons.io.RandomAccessInputStream;
import com.mucommander.commons.io.StreamUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.Calendar;
import java.util.Vector;

/**
 * Parses entries contained in an ISO/NRG file.
 * <p>
 * <pre>
 * Reference: http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-119.pdf
 * <p/>
 * Todo:
 *      * test with more images
 *      * rewrite/sanitize InputStream for cooked
 *      * add RockRidge, UDF and others extensions
 *      * add DiscJuggler & other weirdo file formats
 * </pre>
 * </p>
 *
 * @author Xavier Martin
 */
class IsoParser {
    private static final Logger LOGGER = LoggerFactory.getLogger(IsoParser.class);
    public static Vector<IsoArchiveEntry> getEntries(byte[] buffer, RandomAccessInputStream rais, int sectSize, long sector_offset, long shiftOffset) throws Exception {
        Vector<IsoArchiveEntry> entries = new Vector<IsoArchiveEntry>();

        Calendar calendar = Calendar.getInstance();
        int start = 16;
        isoPvd pvd = null;
        todo todo_idr = null;

        int level = 0;
        for (int i = 1; i < 17; i++) {  // fuzzy search, can have type=0 (bootable el torito), type=2 (svd)
            pvd = new isoPvd(buffer, rais, start + i, sectSize, shiftOffset);
            if (pvd.type[0] == 2 && pvd.id[0] == 'C' && pvd.id[1] == 'D' && pvd.id[2] == '0' && pvd.id[3] == '0' && pvd.id[4] == '1') {
                // gotta read docs a little more about those UCS-2 Escape Sequences
                switch (pvd.unused3[2]) {
                    case 0x40:
                        level = 1;
                        break;
                    case 0x43:
                        level = 2;
                        break;
                    case 0x45:
                        level = 3;
                }
                break;
            }
        }

        if (level == 0) // if no SVD with Joliet, fallback to plain-old ISO9660
            pvd = new isoPvd(buffer, rais, start, sectSize, shiftOffset);

        isoDr idr = new isoDr(pvd.root_directory_record, 0);
        todo_idr = parse_dir(todo_idr, "", isonum_733(idr.extent), isonum_733(idr.size), rais, buffer, entries, sectSize, level, shiftOffset, sector_offset, calendar);
        while (todo_idr != null) {
            todo_idr = parse_dir(todo_idr, todo_idr.name, todo_idr.extent, todo_idr.length, rais, buffer, entries, sectSize, level, shiftOffset, sector_offset, calendar);
            todo_idr = todo_idr.next;
        }

        return entries;
    }

    /**
     * Parses the given ISO file and returns the list of entries it contains. The specified stream will *not* be closed
     * by this method.
     *
     * @param file the ISO file to parse
     * @param rais random access stream to read the ISO file. It will *not* be closed by this method.
     * @return the list of entries contained by the ISO file
     * @throws IOException if an I/O error occurs
     */
    static Vector<IsoArchiveEntry> getEntries(AbstractFile file, RandomAccessInputStream rais) throws IOException {
        byte[] buffer = BufferPool.getByteArray(IsoUtil.MODE1_2048);

        try {
            if ("nrg".equals(file.getExtension())) {
                return NrgParser.getEntries(buffer, file, rais);
            }

            int sectSize = IsoUtil.guessSectorSize(file);

            // sector shift : 0 most of the time
            long sector_offset = 0;

            // bytes : depend if there's earlier track we discard
            long shiftOffset = 0;

            return getEntries(buffer, rais, sectSize, sector_offset, shiftOffset);

            /*
            if ("cdi".equals(file.getExtension())) {
                // WIP
                // http://cvs.berlios.de/cgi-bin/viewcvs.cgi/libdiscmage/libdiscmage/src/filter/cdi.c?revision=1.3&view=markup
                int len = rais.available();
                long offset = -1;

                rais.seek(len - 7);
                rais.read(buffer, 0, 8);
                long version = IsoUtil.toDwordBE(buffer, 0);
                offset = IsoUtil.toDwordBE(buffer, 4);
                FileLogger.finest("cdi root " + Long.toHexString(offset) + " version " + Long.toHexString(version));

            }
            */
        }
        catch (Exception e) {
            LOGGER.info("Exception caught while parsing iso, throwing IOException", e);

            throw new IOException();
        }
        finally {
            // Release the buffer
            BufferPool.releaseByteArray(buffer);
        }
    }


    private static void newString(byte b[], int len, int level, StringBuffer name) throws Exception {
        name.append((level == 0) ? new String(b, 0, len) : new String(b, 0, len, "UnicodeBigUnmarked"));
    }

    public static todo parse_dir(todo todo_idr, String rootname, int extent, int len, RandomAccessInputStream rais, byte[] buffer, Vector<IsoArchiveEntry> entries, int sectSize, int level, long shiftOffset, long sector_offset, Calendar calendar) throws Exception {
        todo td;
        int i;
        isoDr idr;

        while (len > 0) {
            rais.seek(IsoUtil.offsetInSector(extent - sector_offset, sectSize, false) + shiftOffset);
            StreamUtils.readFully(rais, buffer);
            len -= buffer.length;
            extent++;
            i = 0;
            while (true) {
                idr = new isoDr(buffer, i);
                if (idr.length[0] == 0) break;
                stat fstat_buf = new stat();
                StringBuffer name_buf = new StringBuffer();
                fstat_buf.st_size = isonum_733(idr.size);
                if ((idr.flags[0] & 2) > 0)
                    fstat_buf.st_mode |= S_IFDIR;
                else
                    fstat_buf.st_mode |= S_IFREG;
                if (idr.name_len[0] == 1 && idr.name[0] == 0)
                    name_buf.append(".");
                else if (idr.name_len[0] == 1 && idr.name[0] == 1)
                    name_buf.append("..");
                else {
                    newString(idr.name, idr.name_len[0] & 0xff, level, name_buf);
                    //if (level == 0) { // strip ;VERSION
                    int p = name_buf.lastIndexOf(";");
                    if (p != -1)
                        name_buf.setLength(p);
                    p = name_buf.lastIndexOf("."); // strip empty extension
                    if (p != -1) {
                        int s = name_buf.length() - 1;
                        if (p == s)
                            name_buf.setLength(s);
                    }
                    //}
                }

                if ((idr.flags[0] & 2) != 0
                        && (idr.name_len[0] != 1
                        || (idr.name[0] != 0 && idr.name[0] != 1))) {
                    td = todo_idr;
                    if (td != null) {
                        while (td.next != null) td = td.next;
                        td.next = new todo();
                        td = td.next;
                    } else {
                        todo_idr = td = new todo();
                    }
                    td.next = null;
                    td.extent = isonum_733(idr.extent);
                    td.length = isonum_733(idr.size);
                    td.name = rootname + name_buf + "/";
                }
                boolean dir = false;
                String n = name_buf.toString();
                if (!(".".equals(n) || "..".equals(n))) {
                    StringBuffer name = new StringBuffer(rootname);
                    name.append(n);

                    if (S_ISDIR(fstat_buf.st_mode)) {
                        dir = true;
                        if (!n.endsWith("/")) {
                            name.append('/');
                        }
                    }
                    calendar.set((idr.date[0] & 0xff) + 1900, idr.date[1] - 1, idr.date[2], idr.date[3], idr.date[4], idr.date[5]);
                    // date_buf[6]
                    // offset from Greenwich Mean Time, in 15-minute intervals, as a twos complement signed number,
                    // positive for time zones east of Greenwich, and negative for time zones
                    calendar.setTimeZone(new java.util.SimpleTimeZone(15 * 60 * 1000 * idr.date[6], ""));
                    entries.add(
                            new IsoArchiveEntry(
                                    name.toString(),
                                    dir,
                                    calendar.getTimeInMillis(),
                                    fstat_buf.st_size,
                                    isonum_733(idr.extent) - sector_offset,
                                    sectSize,
                                    shiftOffset,
                                    false)
                    );
                }

                i += (buffer[i] & 0xff);
                if (i > IsoUtil.MODE1_2048 - idr.s_length) break;
            }
        }

        return todo_idr;
    }

    // ======================================
    private static int ISODCL(int start, int end) {
        return (end - start + 1);
    }

    private static int isonum_731(byte p[]) {
        return IsoUtil.toDwordBE(p, 0);
    }

    public static int isonum_733(byte p[]) {
        return (isonum_731(p));
    }

    private static boolean S_ISDIR(int m) {
        return ((m & S_IFDIR) == S_IFDIR);
    }

    // ======================================
    private static int S_IFREG = 0100000;
    private static int S_IFDIR = 0040000;

    private static class stat {
        int st_size;
        int st_mode;
    }

    public static class todo {
        public todo next;
        public String name;
        public int extent;
        public int length;
    }

    public static class isoDr {
        public byte[] length = new byte[ISODCL(1, 1)];
        public byte[] ext_attr_length = new byte[ISODCL(2, 2)];
        public byte[] extent = new byte[ISODCL(3, 10)];
        public byte[] size = new byte[ISODCL(11, 18)];
        public byte[] date = new byte[ISODCL(19, 25)];
        public byte[] flags = new byte[ISODCL(26, 26)];
        public byte[] file_unit_size = new byte[ISODCL(27, 27)];
        public byte[] interleave = new byte[ISODCL(28, 28)];
        public byte[] volume_sequence_number = new byte[ISODCL(29, 32)];
        public byte[] name_len = new byte[ISODCL(33, 33)];
        public byte[] name = new byte[/*38*/128];   // quickly bumped to 128 for Joliet : doesn't lead to a crash yet :)

        public int s_length = 34;

        public byte dataDr[][] = {length, ext_attr_length, extent, size,
                date, flags, file_unit_size, interleave,
                volume_sequence_number, name_len, name};

        public isoDr(byte src[], int pos) {
            for (int i = 0, max = dataDr.length; i < max; i++) {
                int l = dataDr[i].length;
                if ((src.length - pos) < dataDr[i].length && i == 10)
                    l = src.length - pos;
                System.arraycopy(src, pos, dataDr[i], 0, l);
                pos += dataDr[i].length;
            }
        }
    }

    public static class isoPvd {
        public byte[] type = new byte[ISODCL(1, 1)];
        public byte[] id = new byte[ISODCL(2, 6)];
        public byte[] version = new byte[ISODCL(7, 7)];
        public byte[] unused1 = new byte[ISODCL(8, 8)];
        public byte[] system_id = new byte[ISODCL(9, 40)];
        public byte[] volume_id = new byte[ISODCL(41, 72)];
        public byte[] unused2 = new byte[ISODCL(73, 80)];
        public byte[] volume_space_size = new byte[ISODCL(81, 88)];
        public byte[] unused3 = new byte[ISODCL(89, 120)];
        public byte[] volume_set_size = new byte[ISODCL(121, 124)];
        public byte[] volume_sequence_number = new byte[ISODCL(125, 128)];
        public byte[] logical_block_size = new byte[ISODCL(129, 132)];
        public byte[] path_table_size = new byte[ISODCL(133, 140)];
        public byte[] type_l_path_table = new byte[ISODCL(141, 144)];
        public byte[] opt_type_l_path_table = new byte[ISODCL(145, 148)];
        public byte[] type_m_path_table = new byte[ISODCL(149, 152)];
        public byte[] opt_type_m_path_table = new byte[ISODCL(153, 156)];
        public byte[] root_directory_record = new byte[ISODCL(157, 190)];
        public byte[] volume_set_id = new byte[ISODCL(191, 318)];
        public byte[] publisher_id = new byte[ISODCL(319, 446)];
        public byte[] preparer_id = new byte[ISODCL(447, 574)];
        public byte[] application_id = new byte[ISODCL(575, 702)];
        public byte[] copyright_file_id = new byte[ISODCL(703, 739)];
        public byte[] abstract_file_id = new byte[ISODCL(740, 776)];
        public byte[] bibliographic_file_id = new byte[ISODCL(777, 813)];
        public byte[] creation_date = new byte[ISODCL(814, 830)];
        public byte[] modification_date = new byte[ISODCL(831, 847)];
        public byte[] expiration_date = new byte[ISODCL(848, 864)];
        public byte[] effective_date = new byte[ISODCL(865, 881)];
        public byte[] file_structure_version = new byte[ISODCL(882, 882)];
        public byte[] unused4 = new byte[ISODCL(883, 883)];
        public byte[] application_data = new byte[ISODCL(884, 1395)];
        public byte[] unused5 = new byte[ISODCL(1396, IsoUtil.MODE1_2048)];

        byte dataPvr[][] = {type, id, version, unused1,
                system_id, volume_id, unused2, volume_space_size,
                unused3, volume_set_size, volume_sequence_number, logical_block_size,
                path_table_size, type_l_path_table, opt_type_l_path_table, type_m_path_table,
                opt_type_m_path_table, root_directory_record, volume_set_id, publisher_id,
                preparer_id, application_id, copyright_file_id, abstract_file_id,
                bibliographic_file_id, creation_date, modification_date, expiration_date,
                effective_date, file_structure_version, unused4, application_data,
                unused5};

        public isoPvd(byte[] pvd, RandomAccessInputStream rais, int start, int sectSize, long shiftOffset) throws IOException {
            rais.seek(IsoUtil.offsetInSector(start, sectSize, false) + shiftOffset);
            if (rais.read(pvd) == -1)
                throw new IOException("unable to read PVD");
            load(pvd);
        }

        void load(byte src[]) {
            for (int i = 0, pos = 0, max = dataPvr.length; i < max; i++) {
                System.arraycopy(src, pos, dataPvr[i], 0, dataPvr[i].length);
                pos += dataPvr[i].length;
            }
        }
    }
}
