/*
 * Copyright (c) 2007, 2013, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code 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 General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */
package com.sun.media.sound;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Stack;

import javax.sound.midi.Instrument;
import javax.sound.midi.Patch;
import javax.sound.midi.Soundbank;
import javax.sound.midi.SoundbankResource;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.AudioFormat.Encoding;

/**
 * A DLS Level 1 and Level 2 soundbank reader (from files/url/streams).
 *
 * @author Karl Helgason
 */
public final class DLSSoundbank implements Soundbank {

    static private class DLSID {
        long i1;
        int s1;
        int s2;
        int x1;
        int x2;
        int x3;
        int x4;
        int x5;
        int x6;
        int x7;
        int x8;

        private DLSID() {
        }

        DLSID(long i1, int s1, int s2, int x1, int x2, int x3, int x4,
                int x5, int x6, int x7, int x8) {
            this.i1 = i1;
            this.s1 = s1;
            this.s2 = s2;
            this.x1 = x1;
            this.x2 = x2;
            this.x3 = x3;
            this.x4 = x4;
            this.x5 = x5;
            this.x6 = x6;
            this.x7 = x7;
            this.x8 = x8;
        }

        public static DLSID read(RIFFReader riff) throws IOException {
            DLSID d = new DLSID();
            d.i1 = riff.readUnsignedInt();
            d.s1 = riff.readUnsignedShort();
            d.s2 = riff.readUnsignedShort();
            d.x1 = riff.readUnsignedByte();
            d.x2 = riff.readUnsignedByte();
            d.x3 = riff.readUnsignedByte();
            d.x4 = riff.readUnsignedByte();
            d.x5 = riff.readUnsignedByte();
            d.x6 = riff.readUnsignedByte();
            d.x7 = riff.readUnsignedByte();
            d.x8 = riff.readUnsignedByte();
            return d;
        }

        public int hashCode() {
            return (int)i1;
        }

        public boolean equals(Object obj) {
            if (!(obj instanceof DLSID)) {
                return false;
            }
            DLSID t = (DLSID) obj;
            return i1 == t.i1 && s1 == t.s1 && s2 == t.s2
                && x1 == t.x1 && x2 == t.x2 && x3 == t.x3 && x4 == t.x4
                && x5 == t.x5 && x6 == t.x6 && x7 == t.x7 && x8 == t.x8;
        }
    }

    /** X = X & Y */
    private static final int DLS_CDL_AND = 0x0001;
    /** X = X | Y */
    private static final int DLS_CDL_OR = 0x0002;
    /** X = X ^ Y */
    private static final int DLS_CDL_XOR = 0x0003;
    /** X = X + Y */
    private static final int DLS_CDL_ADD = 0x0004;
    /** X = X - Y */
    private static final int DLS_CDL_SUBTRACT = 0x0005;
    /** X = X * Y */
    private static final int DLS_CDL_MULTIPLY = 0x0006;
    /** X = X / Y */
    private static final int DLS_CDL_DIVIDE = 0x0007;
    /** X = X && Y */
    private static final int DLS_CDL_LOGICAL_AND = 0x0008;
    /** X = X || Y */
    private static final int DLS_CDL_LOGICAL_OR = 0x0009;
    /** X = (X < Y) */
    private static final int DLS_CDL_LT = 0x000A;
    /** X = (X <= Y) */
    private static final int DLS_CDL_LE = 0x000B;
    /** X = (X > Y) */
    private static final int DLS_CDL_GT = 0x000C;
    /** X = (X >= Y) */
    private static final int DLS_CDL_GE = 0x000D;
    /** X = (X == Y) */
    private static final int DLS_CDL_EQ = 0x000E;
    /** X = !X */
    private static final int DLS_CDL_NOT = 0x000F;
    /** 32-bit constant */
    private static final int DLS_CDL_CONST = 0x0010;
    /** 32-bit value returned from query */
    private static final int DLS_CDL_QUERY = 0x0011;
    /** 32-bit value returned from query */
    private static final int DLS_CDL_QUERYSUPPORTED = 0x0012;

    private static final DLSID DLSID_GMInHardware = new DLSID(0x178f2f24,
            0xc364, 0x11d1, 0xa7, 0x60, 0x00, 0x00, 0xf8, 0x75, 0xac, 0x12);
    private static final DLSID DLSID_GSInHardware = new DLSID(0x178f2f25,
            0xc364, 0x11d1, 0xa7, 0x60, 0x00, 0x00, 0xf8, 0x75, 0xac, 0x12);
    private static final DLSID DLSID_XGInHardware = new DLSID(0x178f2f26,
            0xc364, 0x11d1, 0xa7, 0x60, 0x00, 0x00, 0xf8, 0x75, 0xac, 0x12);
    private static final DLSID DLSID_SupportsDLS1 = new DLSID(0x178f2f27,
            0xc364, 0x11d1, 0xa7, 0x60, 0x00, 0x00, 0xf8, 0x75, 0xac, 0x12);
    private static final DLSID DLSID_SupportsDLS2 = new DLSID(0xf14599e5,
            0x4689, 0x11d2, 0xaf, 0xa6, 0x0, 0xaa, 0x0, 0x24, 0xd8, 0xb6);
    private static final DLSID DLSID_SampleMemorySize = new DLSID(0x178f2f28,
            0xc364, 0x11d1, 0xa7, 0x60, 0x00, 0x00, 0xf8, 0x75, 0xac, 0x12);
    private static final DLSID DLSID_ManufacturersID = new DLSID(0xb03e1181,
            0x8095, 0x11d2, 0xa1, 0xef, 0x0, 0x60, 0x8, 0x33, 0xdb, 0xd8);
    private static final DLSID DLSID_ProductID = new DLSID(0xb03e1182,
            0x8095, 0x11d2, 0xa1, 0xef, 0x0, 0x60, 0x8, 0x33, 0xdb, 0xd8);
    private static final DLSID DLSID_SamplePlaybackRate = new DLSID(0x2a91f713,
            0xa4bf, 0x11d2, 0xbb, 0xdf, 0x0, 0x60, 0x8, 0x33, 0xdb, 0xd8);

    private long major = -1;
    private long minor = -1;

    private final DLSInfo info = new DLSInfo();

    private final List<DLSInstrument> instruments = new ArrayList<DLSInstrument>();
    private final List<DLSSample> samples = new ArrayList<DLSSample>();

    private boolean largeFormat = false;
    private File sampleFile;

    public DLSSoundbank() {
    }

    public DLSSoundbank(URL url) throws IOException {
        InputStream is = url.openStream();
        try {
            readSoundbank(is);
        } finally {
            is.close();
        }
    }

    public DLSSoundbank(File file) throws IOException {
        largeFormat = true;
        sampleFile = file;
        InputStream is = new FileInputStream(file);
        try {
            readSoundbank(is);
        } finally {
            is.close();
        }
    }

    public DLSSoundbank(InputStream inputstream) throws IOException {
        readSoundbank(inputstream);
    }

    private void readSoundbank(InputStream inputstream) throws IOException {
        RIFFReader riff = new RIFFReader(inputstream);
        if (!riff.getFormat().equals("RIFF")) {
            throw new RIFFInvalidFormatException(
                    "Input stream is not a valid RIFF stream!");
        }
        if (!riff.getType().equals("DLS ")) {
            throw new RIFFInvalidFormatException(
                    "Input stream is not a valid DLS soundbank!");
        }
        while (riff.hasNextChunk()) {
            RIFFReader chunk = riff.nextChunk();
            if (chunk.getFormat().equals("LIST")) {
                if (chunk.getType().equals("INFO"))
                    readInfoChunk(chunk);
                if (chunk.getType().equals("lins"))
                    readLinsChunk(chunk);
                if (chunk.getType().equals("wvpl"))
                    readWvplChunk(chunk);
            } else {
                if (chunk.getFormat().equals("cdl ")) {
                    if (!readCdlChunk(chunk)) {
                        throw new RIFFInvalidFormatException(
                                "DLS file isn't supported!");
                    }
                }
                if (chunk.getFormat().equals("colh")) {
                    // skipped because we will load the entire bank into memory
                    // long instrumentcount = chunk.readUnsignedInt();
                    // System.out.println("instrumentcount = "+ instrumentcount);
                }
                if (chunk.getFormat().equals("ptbl")) {
                    // Pool Table Chunk
                    // skipped because we will load the entire bank into memory
                }
                if (chunk.getFormat().equals("vers")) {
                    major = chunk.readUnsignedInt();
                    minor = chunk.readUnsignedInt();
                }
            }
        }

        for (Map.Entry<DLSRegion, Long> entry : temp_rgnassign.entrySet()) {
            entry.getKey().sample = samples.get((int)entry.getValue().longValue());
        }

        temp_rgnassign = null;
    }

    private boolean cdlIsQuerySupported(DLSID uuid) {
        return uuid.equals(DLSID_GMInHardware)
            || uuid.equals(DLSID_GSInHardware)
            || uuid.equals(DLSID_XGInHardware)
            || uuid.equals(DLSID_SupportsDLS1)
            || uuid.equals(DLSID_SupportsDLS2)
            || uuid.equals(DLSID_SampleMemorySize)
            || uuid.equals(DLSID_ManufacturersID)
            || uuid.equals(DLSID_ProductID)
            || uuid.equals(DLSID_SamplePlaybackRate);
    }

    private long cdlQuery(DLSID uuid) {
        if (uuid.equals(DLSID_GMInHardware))
            return 1;
        if (uuid.equals(DLSID_GSInHardware))
            return 0;
        if (uuid.equals(DLSID_XGInHardware))
            return 0;
        if (uuid.equals(DLSID_SupportsDLS1))
            return 1;
        if (uuid.equals(DLSID_SupportsDLS2))
            return 1;
        if (uuid.equals(DLSID_SampleMemorySize))
            return Runtime.getRuntime().totalMemory();
        if (uuid.equals(DLSID_ManufacturersID))
            return 0;
        if (uuid.equals(DLSID_ProductID))
            return 0;
        if (uuid.equals(DLSID_SamplePlaybackRate))
            return 44100;
        return 0;
    }


    // Reading cdl-ck Chunk
    // "cdl " chunk can only appear inside : DLS,lart,lar2,rgn,rgn2
    private boolean readCdlChunk(RIFFReader riff) throws IOException {

        DLSID uuid;
        long x;
        long y;
        Stack<Long> stack = new Stack<Long>();

        while (riff.available() != 0) {
            int opcode = riff.readUnsignedShort();
            switch (opcode) {
            case DLS_CDL_AND:
                x = stack.pop();
                y = stack.pop();
                stack.push(Long.valueOf(((x != 0) && (y != 0)) ? 1 : 0));
                break;
            case DLS_CDL_OR:
                x = stack.pop();
                y = stack.pop();
                stack.push(Long.valueOf(((x != 0) || (y != 0)) ? 1 : 0));
                break;
            case DLS_CDL_XOR:
                x = stack.pop();
                y = stack.pop();
                stack.push(Long.valueOf(((x != 0) ^ (y != 0)) ? 1 : 0));
                break;
            case DLS_CDL_ADD:
                x = stack.pop();
                y = stack.pop();
                stack.push(Long.valueOf(x + y));
                break;
            case DLS_CDL_SUBTRACT:
                x = stack.pop();
                y = stack.pop();
                stack.push(Long.valueOf(x - y));
                break;
            case DLS_CDL_MULTIPLY:
                x = stack.pop();
                y = stack.pop();
                stack.push(Long.valueOf(x * y));
                break;
            case DLS_CDL_DIVIDE:
                x = stack.pop();
                y = stack.pop();
                stack.push(Long.valueOf(x / y));
                break;
            case DLS_CDL_LOGICAL_AND:
                x = stack.pop();
                y = stack.pop();
                stack.push(Long.valueOf(((x != 0) && (y != 0)) ? 1 : 0));
                break;
            case DLS_CDL_LOGICAL_OR:
                x = stack.pop();
                y = stack.pop();
                stack.push(Long.valueOf(((x != 0) || (y != 0)) ? 1 : 0));
                break;
            case DLS_CDL_LT:
                x = stack.pop();
                y = stack.pop();
                stack.push(Long.valueOf((x < y) ? 1 : 0));
                break;
            case DLS_CDL_LE:
                x = stack.pop();
                y = stack.pop();
                stack.push(Long.valueOf((x <= y) ? 1 : 0));
                break;
            case DLS_CDL_GT:
                x = stack.pop();
                y = stack.pop();
                stack.push(Long.valueOf((x > y) ? 1 : 0));
                break;
            case DLS_CDL_GE:
                x = stack.pop();
                y = stack.pop();
                stack.push(Long.valueOf((x >= y) ? 1 : 0));
                break;
            case DLS_CDL_EQ:
                x = stack.pop();
                y = stack.pop();
                stack.push(Long.valueOf((x == y) ? 1 : 0));
                break;
            case DLS_CDL_NOT:
                x = stack.pop();
                y = stack.pop();
                stack.push(Long.valueOf((x == 0) ? 1 : 0));
                break;
            case DLS_CDL_CONST:
                stack.push(Long.valueOf(riff.readUnsignedInt()));
                break;
            case DLS_CDL_QUERY:
                uuid = DLSID.read(riff);
                stack.push(cdlQuery(uuid));
                break;
            case DLS_CDL_QUERYSUPPORTED:
                uuid = DLSID.read(riff);
                stack.push(Long.valueOf(cdlIsQuerySupported(uuid) ? 1 : 0));
                break;
            default:
                break;
            }
        }
        if (stack.isEmpty())
            return false;

        return stack.pop() == 1;
    }

    private void readInfoChunk(RIFFReader riff) throws IOException {
        info.name = null;
        while (riff.hasNextChunk()) {
            RIFFReader chunk = riff.nextChunk();
            String format = chunk.getFormat();
            if (format.equals("INAM"))
                info.name = chunk.readString(chunk.available());
            else if (format.equals("ICRD"))
                info.creationDate = chunk.readString(chunk.available());
            else if (format.equals("IENG"))
                info.engineers = chunk.readString(chunk.available());
            else if (format.equals("IPRD"))
                info.product = chunk.readString(chunk.available());
            else if (format.equals("ICOP"))
                info.copyright = chunk.readString(chunk.available());
            else if (format.equals("ICMT"))
                info.comments = chunk.readString(chunk.available());
            else if (format.equals("ISFT"))
                info.tools = chunk.readString(chunk.available());
            else if (format.equals("IARL"))
                info.archival_location = chunk.readString(chunk.available());
            else if (format.equals("IART"))
                info.artist = chunk.readString(chunk.available());
            else if (format.equals("ICMS"))
                info.commissioned = chunk.readString(chunk.available());
            else if (format.equals("IGNR"))
                info.genre = chunk.readString(chunk.available());
            else if (format.equals("IKEY"))
                info.keywords = chunk.readString(chunk.available());
            else if (format.equals("IMED"))
                info.medium = chunk.readString(chunk.available());
            else if (format.equals("ISBJ"))
                info.subject = chunk.readString(chunk.available());
            else if (format.equals("ISRC"))
                info.source = chunk.readString(chunk.available());
            else if (format.equals("ISRF"))
                info.source_form = chunk.readString(chunk.available());
            else if (format.equals("ITCH"))
                info.technician = chunk.readString(chunk.available());
        }
    }

    private void readLinsChunk(RIFFReader riff) throws IOException {
        while (riff.hasNextChunk()) {
            RIFFReader chunk = riff.nextChunk();
            if (chunk.getFormat().equals("LIST")) {
                if (chunk.getType().equals("ins "))
                    readInsChunk(chunk);
            }
        }
    }

    private void readInsChunk(RIFFReader riff) throws IOException {
        DLSInstrument instrument = new DLSInstrument(this);

        while (riff.hasNextChunk()) {
            RIFFReader chunk = riff.nextChunk();
            String format = chunk.getFormat();
            if (format.equals("LIST")) {
                if (chunk.getType().equals("INFO")) {
                    readInsInfoChunk(instrument, chunk);
                }
                if (chunk.getType().equals("lrgn")) {
                    while (chunk.hasNextChunk()) {
                        RIFFReader subchunk = chunk.nextChunk();
                        if (subchunk.getFormat().equals("LIST")) {
                            if (subchunk.getType().equals("rgn ")) {
                                DLSRegion split = new DLSRegion();
                                if (readRgnChunk(split, subchunk))
                                    instrument.getRegions().add(split);
                            }
                            if (subchunk.getType().equals("rgn2")) {
                                // support for DLS level 2 regions
                                DLSRegion split = new DLSRegion();
                                if (readRgnChunk(split, subchunk))
                                    instrument.getRegions().add(split);
                            }
                        }
                    }
                }
                if (chunk.getType().equals("lart")) {
                    List<DLSModulator> modlist = new ArrayList<DLSModulator>();
                    while (chunk.hasNextChunk()) {
                        RIFFReader subchunk = chunk.nextChunk();
                        if (chunk.getFormat().equals("cdl ")) {
                            if (!readCdlChunk(chunk)) {
                                modlist.clear();
                                break;
                            }
                        }
                        if (subchunk.getFormat().equals("art1"))
                            readArt1Chunk(modlist, subchunk);
                    }
                    instrument.getModulators().addAll(modlist);
                }
                if (chunk.getType().equals("lar2")) {
                    // support for DLS level 2 ART
                    List<DLSModulator> modlist = new ArrayList<DLSModulator>();
                    while (chunk.hasNextChunk()) {
                        RIFFReader subchunk = chunk.nextChunk();
                        if (chunk.getFormat().equals("cdl ")) {
                            if (!readCdlChunk(chunk)) {
                                modlist.clear();
                                break;
                            }
                        }
                        if (subchunk.getFormat().equals("art2"))
                            readArt2Chunk(modlist, subchunk);
                    }
                    instrument.getModulators().addAll(modlist);
                }
            } else {
                if (format.equals("dlid")) {
                    instrument.guid = new byte[16];
                    chunk.readFully(instrument.guid);
                }
                if (format.equals("insh")) {
                    chunk.readUnsignedInt(); // Read Region Count - ignored

                    int bank = chunk.read();             // LSB
                    bank += (chunk.read() & 127) << 7;   // MSB
                    chunk.read(); // Read Reserved byte
                    int drumins = chunk.read();          // Drum Instrument

                    int id = chunk.read() & 127; // Read only first 7 bits
                    chunk.read(); // Read Reserved byte
                    chunk.read(); // Read Reserved byte
                    chunk.read(); // Read Reserved byte

                    instrument.bank = bank;
                    instrument.preset = (int) id;
                    instrument.druminstrument = (drumins & 128) > 0;
                    //System.out.println("bank="+bank+" drumkit="+drumkit
                    //        +" id="+id);
                }

            }
        }
        instruments.add(instrument);
    }

    private void readArt1Chunk(List<DLSModulator> modulators, RIFFReader riff)
            throws IOException {
        long size = riff.readUnsignedInt();
        long count = riff.readUnsignedInt();

        if (size - 8 != 0)
            riff.skip(size - 8);

        for (int i = 0; i < count; i++) {
            DLSModulator modulator = new DLSModulator();
            modulator.version = 1;
            modulator.source = riff.readUnsignedShort();
            modulator.control = riff.readUnsignedShort();
            modulator.destination = riff.readUnsignedShort();
            modulator.transform = riff.readUnsignedShort();
            modulator.scale = riff.readInt();
            modulators.add(modulator);
        }
    }

    private void readArt2Chunk(List<DLSModulator> modulators, RIFFReader riff)
            throws IOException {
        long size = riff.readUnsignedInt();
        long count = riff.readUnsignedInt();

        if (size - 8 != 0)
            riff.skip(size - 8);

        for (int i = 0; i < count; i++) {
            DLSModulator modulator = new DLSModulator();
            modulator.version = 2;
            modulator.source = riff.readUnsignedShort();
            modulator.control = riff.readUnsignedShort();
            modulator.destination = riff.readUnsignedShort();
            modulator.transform = riff.readUnsignedShort();
            modulator.scale = riff.readInt();
            modulators.add(modulator);
        }
    }

    private Map<DLSRegion, Long> temp_rgnassign = new HashMap<DLSRegion, Long>();

    private boolean readRgnChunk(DLSRegion split, RIFFReader riff)
            throws IOException {
        while (riff.hasNextChunk()) {
            RIFFReader chunk = riff.nextChunk();
            String format = chunk.getFormat();
            if (format.equals("LIST")) {
                if (chunk.getType().equals("lart")) {
                    List<DLSModulator> modlist = new ArrayList<DLSModulator>();
                    while (chunk.hasNextChunk()) {
                        RIFFReader subchunk = chunk.nextChunk();
                        if (chunk.getFormat().equals("cdl ")) {
                            if (!readCdlChunk(chunk)) {
                                modlist.clear();
                                break;
                            }
                        }
                        if (subchunk.getFormat().equals("art1"))
                            readArt1Chunk(modlist, subchunk);
                    }
                    split.getModulators().addAll(modlist);
                }
                if (chunk.getType().equals("lar2")) {
                    // support for DLS level 2 ART
                    List<DLSModulator> modlist = new ArrayList<DLSModulator>();
                    while (chunk.hasNextChunk()) {
                        RIFFReader subchunk = chunk.nextChunk();
                        if (chunk.getFormat().equals("cdl ")) {
                            if (!readCdlChunk(chunk)) {
                                modlist.clear();
                                break;
                            }
                        }
                        if (subchunk.getFormat().equals("art2"))
                            readArt2Chunk(modlist, subchunk);
                    }
                    split.getModulators().addAll(modlist);
                }
            } else {

                if (format.equals("cdl ")) {
                    if (!readCdlChunk(chunk))
                        return false;
                }
                if (format.equals("rgnh")) {
                    split.keyfrom = chunk.readUnsignedShort();
                    split.keyto = chunk.readUnsignedShort();
                    split.velfrom = chunk.readUnsignedShort();
                    split.velto = chunk.readUnsignedShort();
                    split.options = chunk.readUnsignedShort();
                    split.exclusiveClass = chunk.readUnsignedShort();
                }
                if (format.equals("wlnk")) {
                    split.fusoptions = chunk.readUnsignedShort();
                    split.phasegroup = chunk.readUnsignedShort();
                    split.channel = chunk.readUnsignedInt();
                    long sampleid = chunk.readUnsignedInt();
                    temp_rgnassign.put(split, sampleid);
                }
                if (format.equals("wsmp")) {
                    split.sampleoptions = new DLSSampleOptions();
                    readWsmpChunk(split.sampleoptions, chunk);
                }
            }
        }
        return true;
    }

    private void readWsmpChunk(DLSSampleOptions sampleOptions, RIFFReader riff)
            throws IOException {
        long size = riff.readUnsignedInt();
        sampleOptions.unitynote = riff.readUnsignedShort();
        sampleOptions.finetune = riff.readShort();
        sampleOptions.attenuation = riff.readInt();
        sampleOptions.options = riff.readUnsignedInt();
        long loops = riff.readInt();

        if (size > 20)
            riff.skip(size - 20);

        for (int i = 0; i < loops; i++) {
            DLSSampleLoop loop = new DLSSampleLoop();
            long size2 = riff.readUnsignedInt();
            loop.type = riff.readUnsignedInt();
            loop.start = riff.readUnsignedInt();
            loop.length = riff.readUnsignedInt();
            sampleOptions.loops.add(loop);
            if (size2 > 16)
                riff.skip(size2 - 16);
        }
    }

    private void readInsInfoChunk(DLSInstrument dlsinstrument, RIFFReader riff)
            throws IOException {
        dlsinstrument.info.name = null;
        while (riff.hasNextChunk()) {
            RIFFReader chunk = riff.nextChunk();
            String format = chunk.getFormat();
            if (format.equals("INAM")) {
                dlsinstrument.info.name = chunk.readString(chunk.available());
            } else if (format.equals("ICRD")) {
                dlsinstrument.info.creationDate =
                        chunk.readString(chunk.available());
            } else if (format.equals("IENG")) {
                dlsinstrument.info.engineers =
                        chunk.readString(chunk.available());
            } else if (format.equals("IPRD")) {
                dlsinstrument.info.product = chunk.readString(chunk.available());
            } else if (format.equals("ICOP")) {
                dlsinstrument.info.copyright =
                        chunk.readString(chunk.available());
            } else if (format.equals("ICMT")) {
                dlsinstrument.info.comments =
                        chunk.readString(chunk.available());
            } else if (format.equals("ISFT")) {
                dlsinstrument.info.tools = chunk.readString(chunk.available());
            } else if (format.equals("IARL")) {
                dlsinstrument.info.archival_location =
                        chunk.readString(chunk.available());
            } else if (format.equals("IART")) {
                dlsinstrument.info.artist = chunk.readString(chunk.available());
            } else if (format.equals("ICMS")) {
                dlsinstrument.info.commissioned =
                        chunk.readString(chunk.available());
            } else if (format.equals("IGNR")) {
                dlsinstrument.info.genre = chunk.readString(chunk.available());
            } else if (format.equals("IKEY")) {
                dlsinstrument.info.keywords =
                        chunk.readString(chunk.available());
            } else if (format.equals("IMED")) {
                dlsinstrument.info.medium = chunk.readString(chunk.available());
            } else if (format.equals("ISBJ")) {
                dlsinstrument.info.subject = chunk.readString(chunk.available());
            } else if (format.equals("ISRC")) {
                dlsinstrument.info.source = chunk.readString(chunk.available());
            } else if (format.equals("ISRF")) {
                dlsinstrument.info.source_form =
                        chunk.readString(chunk.available());
            } else if (format.equals("ITCH")) {
                dlsinstrument.info.technician =
                        chunk.readString(chunk.available());
            }
        }
    }

    private void readWvplChunk(RIFFReader riff) throws IOException {
        while (riff.hasNextChunk()) {
            RIFFReader chunk = riff.nextChunk();
            if (chunk.getFormat().equals("LIST")) {
                if (chunk.getType().equals("wave"))
                    readWaveChunk(chunk);
            }
        }
    }

    private void readWaveChunk(RIFFReader riff) throws IOException {
        DLSSample sample = new DLSSample(this);

        while (riff.hasNextChunk()) {
            RIFFReader chunk = riff.nextChunk();
            String format = chunk.getFormat();
            if (format.equals("LIST")) {
                if (chunk.getType().equals("INFO")) {
                    readWaveInfoChunk(sample, chunk);
                }
            } else {
                if (format.equals("dlid")) {
                    sample.guid = new byte[16];
                    chunk.readFully(sample.guid);
                }

                if (format.equals("fmt ")) {
                    int sampleformat = chunk.readUnsignedShort();
                    if (sampleformat != 1 && sampleformat != 3) {
                        throw new RIFFInvalidDataException(
                                "Only PCM samples are supported!");
                    }
                    int channels = chunk.readUnsignedShort();
                    long samplerate = chunk.readUnsignedInt();
                    // bytes per sec
                    /* long framerate = */ chunk.readUnsignedInt();
                    // block align, framesize
                    int framesize = chunk.readUnsignedShort();
                    int bits = chunk.readUnsignedShort();
                    AudioFormat audioformat = null;
                    if (sampleformat == 1) {
                        if (bits == 8) {
                            audioformat = new AudioFormat(
                                    Encoding.PCM_UNSIGNED, samplerate, bits,
                                    channels, framesize, samplerate, false);
                        } else {
                            audioformat = new AudioFormat(
                                    Encoding.PCM_SIGNED, samplerate, bits,
                                    channels, framesize, samplerate, false);
                        }
                    }
                    if (sampleformat == 3) {
                        audioformat = new AudioFormat(
                                Encoding.PCM_FLOAT, samplerate, bits,
                                channels, framesize, samplerate, false);
                    }

                    sample.format = audioformat;
                }

                if (format.equals("data")) {
                    if (largeFormat) {
                        sample.setData(new ModelByteBuffer(sampleFile,
                                chunk.getFilePointer(), chunk.available()));
                    } else {
                        byte[] buffer = new byte[chunk.available()];
                        //  chunk.read(buffer);
                        sample.setData(buffer);

                        int read = 0;
                        int avail = chunk.available();
                        while (read != avail) {
                            if (avail - read > 65536) {
                                chunk.readFully(buffer, read, 65536);
                                read += 65536;
                            } else {
                                chunk.readFully(buffer, read, avail - read);
                                read = avail;
                            }
                        }
                    }
                }

                if (format.equals("wsmp")) {
                    sample.sampleoptions = new DLSSampleOptions();
                    readWsmpChunk(sample.sampleoptions, chunk);
                }
            }
        }

        samples.add(sample);

    }

    private void readWaveInfoChunk(DLSSample dlssample, RIFFReader riff)
            throws IOException {
        dlssample.info.name = null;
        while (riff.hasNextChunk()) {
            RIFFReader chunk = riff.nextChunk();
            String format = chunk.getFormat();
            if (format.equals("INAM")) {
                dlssample.info.name = chunk.readString(chunk.available());
            } else if (format.equals("ICRD")) {
                dlssample.info.creationDate =
                        chunk.readString(chunk.available());
            } else if (format.equals("IENG")) {
                dlssample.info.engineers = chunk.readString(chunk.available());
            } else if (format.equals("IPRD")) {
                dlssample.info.product = chunk.readString(chunk.available());
            } else if (format.equals("ICOP")) {
                dlssample.info.copyright = chunk.readString(chunk.available());
            } else if (format.equals("ICMT")) {
                dlssample.info.comments = chunk.readString(chunk.available());
            } else if (format.equals("ISFT")) {
                dlssample.info.tools = chunk.readString(chunk.available());
            } else if (format.equals("IARL")) {
                dlssample.info.archival_location =
                        chunk.readString(chunk.available());
            } else if (format.equals("IART")) {
                dlssample.info.artist = chunk.readString(chunk.available());
            } else if (format.equals("ICMS")) {
                dlssample.info.commissioned =
                        chunk.readString(chunk.available());
            } else if (format.equals("IGNR")) {
                dlssample.info.genre = chunk.readString(chunk.available());
            } else if (format.equals("IKEY")) {
                dlssample.info.keywords = chunk.readString(chunk.available());
            } else if (format.equals("IMED")) {
                dlssample.info.medium = chunk.readString(chunk.available());
            } else if (format.equals("ISBJ")) {
                dlssample.info.subject = chunk.readString(chunk.available());
            } else if (format.equals("ISRC")) {
                dlssample.info.source = chunk.readString(chunk.available());
            } else if (format.equals("ISRF")) {
                dlssample.info.source_form = chunk.readString(chunk.available());
            } else if (format.equals("ITCH")) {
                dlssample.info.technician = chunk.readString(chunk.available());
            }
        }
    }

    public void save(String name) throws IOException {
        writeSoundbank(new RIFFWriter(name, "DLS "));
    }

    public void save(File file) throws IOException {
        writeSoundbank(new RIFFWriter(file, "DLS "));
    }

    public void save(OutputStream out) throws IOException {
        writeSoundbank(new RIFFWriter(out, "DLS "));
    }

    private void writeSoundbank(RIFFWriter writer) throws IOException {
        RIFFWriter colh_chunk = writer.writeChunk("colh");
        colh_chunk.writeUnsignedInt(instruments.size());

        if (major != -1 && minor != -1) {
            RIFFWriter vers_chunk = writer.writeChunk("vers");
            vers_chunk.writeUnsignedInt(major);
            vers_chunk.writeUnsignedInt(minor);
        }

        writeInstruments(writer.writeList("lins"));

        RIFFWriter ptbl = writer.writeChunk("ptbl");
        ptbl.writeUnsignedInt(8);
        ptbl.writeUnsignedInt(samples.size());
        long ptbl_offset = writer.getFilePointer();
        for (int i = 0; i < samples.size(); i++)
            ptbl.writeUnsignedInt(0);

        RIFFWriter wvpl = writer.writeList("wvpl");
        long off = wvpl.getFilePointer();
        List<Long> offsettable = new ArrayList<Long>();
        for (DLSSample sample : samples) {
            offsettable.add(Long.valueOf(wvpl.getFilePointer() - off));
            writeSample(wvpl.writeList("wave"), sample);
        }

        // small cheat, we are going to rewrite data back in wvpl
        long bak = writer.getFilePointer();
        writer.seek(ptbl_offset);
        writer.setWriteOverride(true);
        for (Long offset : offsettable)
            writer.writeUnsignedInt(offset.longValue());
        writer.setWriteOverride(false);
        writer.seek(bak);

        writeInfo(writer.writeList("INFO"), info);

        writer.close();
    }

    private void writeSample(RIFFWriter writer, DLSSample sample)
            throws IOException {

        AudioFormat audioformat = sample.getFormat();

        Encoding encoding = audioformat.getEncoding();
        float sampleRate = audioformat.getSampleRate();
        int sampleSizeInBits = audioformat.getSampleSizeInBits();
        int channels = audioformat.getChannels();
        int frameSize = audioformat.getFrameSize();
        float frameRate = audioformat.getFrameRate();
        boolean bigEndian = audioformat.isBigEndian();

        boolean convert_needed = false;

        if (audioformat.getSampleSizeInBits() == 8) {
            if (!encoding.equals(Encoding.PCM_UNSIGNED)) {
                encoding = Encoding.PCM_UNSIGNED;
                convert_needed = true;
            }
        } else {
            if (!encoding.equals(Encoding.PCM_SIGNED)) {
                encoding = Encoding.PCM_SIGNED;
                convert_needed = true;
            }
            if (bigEndian) {
                bigEndian = false;
                convert_needed = true;
            }
        }

        if (convert_needed) {
            audioformat = new AudioFormat(encoding, sampleRate,
                    sampleSizeInBits, channels, frameSize, frameRate, bigEndian);
        }

        // fmt
        RIFFWriter fmt_chunk = writer.writeChunk("fmt ");
        int sampleformat = 0;
        if (audioformat.getEncoding().equals(Encoding.PCM_UNSIGNED))
            sampleformat = 1;
        else if (audioformat.getEncoding().equals(Encoding.PCM_SIGNED))
            sampleformat = 1;
        else if (audioformat.getEncoding().equals(Encoding.PCM_FLOAT))
            sampleformat = 3;

        fmt_chunk.writeUnsignedShort(sampleformat);
        fmt_chunk.writeUnsignedShort(audioformat.getChannels());
        fmt_chunk.writeUnsignedInt((long) audioformat.getSampleRate());
        long srate = ((long)audioformat.getFrameRate())*audioformat.getFrameSize();
        fmt_chunk.writeUnsignedInt(srate);
        fmt_chunk.writeUnsignedShort(audioformat.getFrameSize());
        fmt_chunk.writeUnsignedShort(audioformat.getSampleSizeInBits());
        fmt_chunk.write(0);
        fmt_chunk.write(0);

        writeSampleOptions(writer.writeChunk("wsmp"), sample.sampleoptions);

        if (convert_needed) {
            RIFFWriter data_chunk = writer.writeChunk("data");
            AudioInputStream stream = AudioSystem.getAudioInputStream(
                    audioformat, (AudioInputStream)sample.getData());
            byte[] buff = new byte[1024];
            int ret;
            while ((ret = stream.read(buff)) != -1) {
                data_chunk.write(buff, 0, ret);
            }
        } else {
            RIFFWriter data_chunk = writer.writeChunk("data");
            ModelByteBuffer databuff = sample.getDataBuffer();
            databuff.writeTo(data_chunk);
            /*
            data_chunk.write(databuff.array(),
            databuff.arrayOffset(),
            databuff.capacity());
             */
        }

        writeInfo(writer.writeList("INFO"), sample.info);
    }

    private void writeInstruments(RIFFWriter writer) throws IOException {
        for (DLSInstrument instrument : instruments) {
            writeInstrument(writer.writeList("ins "), instrument);
        }
    }

    private void writeInstrument(RIFFWriter writer, DLSInstrument instrument)
            throws IOException {

        int art1_count = 0;
        int art2_count = 0;
        for (DLSModulator modulator : instrument.getModulators()) {
            if (modulator.version == 1)
                art1_count++;
            if (modulator.version == 2)
                art2_count++;
        }
        for (DLSRegion region : instrument.regions) {
            for (DLSModulator modulator : region.getModulators()) {
                if (modulator.version == 1)
                    art1_count++;
                if (modulator.version == 2)
                    art2_count++;
            }
        }

        int version = 1;
        if (art2_count > 0)
            version = 2;

        RIFFWriter insh_chunk = writer.writeChunk("insh");
        insh_chunk.writeUnsignedInt(instrument.getRegions().size());
        insh_chunk.writeUnsignedInt(instrument.bank +
                (instrument.druminstrument ? 2147483648L : 0));
        insh_chunk.writeUnsignedInt(instrument.preset);

        RIFFWriter lrgn = writer.writeList("lrgn");
        for (DLSRegion region: instrument.regions)
            writeRegion(lrgn, region, version);

        writeArticulators(writer, instrument.getModulators());

        writeInfo(writer.writeList("INFO"), instrument.info);

    }

    private void writeArticulators(RIFFWriter writer,
            List<DLSModulator> modulators) throws IOException {
        int art1_count = 0;
        int art2_count = 0;
        for (DLSModulator modulator : modulators) {
            if (modulator.version == 1)
                art1_count++;
            if (modulator.version == 2)
                art2_count++;
        }
        if (art1_count > 0) {
            RIFFWriter lar1 = writer.writeList("lart");
            RIFFWriter art1 = lar1.writeChunk("art1");
            art1.writeUnsignedInt(8);
            art1.writeUnsignedInt(art1_count);
            for (DLSModulator modulator : modulators) {
                if (modulator.version == 1) {
                    art1.writeUnsignedShort(modulator.source);
                    art1.writeUnsignedShort(modulator.control);
                    art1.writeUnsignedShort(modulator.destination);
                    art1.writeUnsignedShort(modulator.transform);
                    art1.writeInt(modulator.scale);
                }
            }
        }
        if (art2_count > 0) {
            RIFFWriter lar2 = writer.writeList("lar2");
            RIFFWriter art2 = lar2.writeChunk("art2");
            art2.writeUnsignedInt(8);
            art2.writeUnsignedInt(art2_count);
            for (DLSModulator modulator : modulators) {
                if (modulator.version == 2) {
                    art2.writeUnsignedShort(modulator.source);
                    art2.writeUnsignedShort(modulator.control);
                    art2.writeUnsignedShort(modulator.destination);
                    art2.writeUnsignedShort(modulator.transform);
                    art2.writeInt(modulator.scale);
                }
            }
        }
    }

    private void writeRegion(RIFFWriter writer, DLSRegion region, int version)
            throws IOException {
        RIFFWriter rgns = null;
        if (version == 1)
            rgns = writer.writeList("rgn ");
        if (version == 2)
            rgns = writer.writeList("rgn2");
        if (rgns == null)
            return;

        RIFFWriter rgnh = rgns.writeChunk("rgnh");
        rgnh.writeUnsignedShort(region.keyfrom);
        rgnh.writeUnsignedShort(region.keyto);
        rgnh.writeUnsignedShort(region.velfrom);
        rgnh.writeUnsignedShort(region.velto);
        rgnh.writeUnsignedShort(region.options);
        rgnh.writeUnsignedShort(region.exclusiveClass);

        if (region.sampleoptions != null)
            writeSampleOptions(rgns.writeChunk("wsmp"), region.sampleoptions);

        if (region.sample != null) {
            if (samples.indexOf(region.sample) != -1) {
                RIFFWriter wlnk = rgns.writeChunk("wlnk");
                wlnk.writeUnsignedShort(region.fusoptions);
                wlnk.writeUnsignedShort(region.phasegroup);
                wlnk.writeUnsignedInt(region.channel);
                wlnk.writeUnsignedInt(samples.indexOf(region.sample));
            }
        }
        writeArticulators(rgns, region.getModulators());
        rgns.close();
    }

    private void writeSampleOptions(RIFFWriter wsmp,
            DLSSampleOptions sampleoptions) throws IOException {
        wsmp.writeUnsignedInt(20);
        wsmp.writeUnsignedShort(sampleoptions.unitynote);
        wsmp.writeShort(sampleoptions.finetune);
        wsmp.writeInt(sampleoptions.attenuation);
        wsmp.writeUnsignedInt(sampleoptions.options);
        wsmp.writeInt(sampleoptions.loops.size());

        for (DLSSampleLoop loop : sampleoptions.loops) {
            wsmp.writeUnsignedInt(16);
            wsmp.writeUnsignedInt(loop.type);
            wsmp.writeUnsignedInt(loop.start);
            wsmp.writeUnsignedInt(loop.length);
        }
    }

    private void writeInfoStringChunk(RIFFWriter writer,
            String name, String value) throws IOException {
        if (value == null)
            return;
        RIFFWriter chunk = writer.writeChunk(name);
        chunk.writeString(value);
        int len = value.getBytes("ascii").length;
        chunk.write(0);
        len++;
        if (len % 2 != 0)
            chunk.write(0);
    }

    private void writeInfo(RIFFWriter writer, DLSInfo info) throws IOException {
        writeInfoStringChunk(writer, "INAM", info.name);
        writeInfoStringChunk(writer, "ICRD", info.creationDate);
        writeInfoStringChunk(writer, "IENG", info.engineers);
        writeInfoStringChunk(writer, "IPRD", info.product);
        writeInfoStringChunk(writer, "ICOP", info.copyright);
        writeInfoStringChunk(writer, "ICMT", info.comments);
        writeInfoStringChunk(writer, "ISFT", info.tools);
        writeInfoStringChunk(writer, "IARL", info.archival_location);
        writeInfoStringChunk(writer, "IART", info.artist);
        writeInfoStringChunk(writer, "ICMS", info.commissioned);
        writeInfoStringChunk(writer, "IGNR", info.genre);
        writeInfoStringChunk(writer, "IKEY", info.keywords);
        writeInfoStringChunk(writer, "IMED", info.medium);
        writeInfoStringChunk(writer, "ISBJ", info.subject);
        writeInfoStringChunk(writer, "ISRC", info.source);
        writeInfoStringChunk(writer, "ISRF", info.source_form);
        writeInfoStringChunk(writer, "ITCH", info.technician);
    }

    public DLSInfo getInfo() {
        return info;
    }

    public String getName() {
        return info.name;
    }

    public String getVersion() {
        return major + "." + minor;
    }

    public String getVendor() {
        return info.engineers;
    }

    public String getDescription() {
        return info.comments;
    }

    public void setName(String s) {
        info.name = s;
    }

    public void setVendor(String s) {
        info.engineers = s;
    }

    public void setDescription(String s) {
        info.comments = s;
    }

    public SoundbankResource[] getResources() {
        SoundbankResource[] resources = new SoundbankResource[samples.size()];
        int j = 0;
        for (int i = 0; i < samples.size(); i++)
            resources[j++] = samples.get(i);
        return resources;
    }

    public DLSInstrument[] getInstruments() {
        DLSInstrument[] inslist_array =
                instruments.toArray(new DLSInstrument[instruments.size()]);
        Arrays.sort(inslist_array, new ModelInstrumentComparator());
        return inslist_array;
    }

    public DLSSample[] getSamples() {
        return samples.toArray(new DLSSample[samples.size()]);
    }

    public Instrument getInstrument(Patch patch) {
        int program = patch.getProgram();
        int bank = patch.getBank();
        boolean percussion = false;
        if (patch instanceof ModelPatch)
            percussion = ((ModelPatch) patch).isPercussion();
        for (Instrument instrument : instruments) {
            Patch patch2 = instrument.getPatch();
            int program2 = patch2.getProgram();
            int bank2 = patch2.getBank();
            if (program == program2 && bank == bank2) {
                boolean percussion2 = false;
                if (patch2 instanceof ModelPatch)
                    percussion2 = ((ModelPatch) patch2).isPercussion();
                if (percussion == percussion2)
                    return instrument;
            }
        }
        return null;
    }

    public void addResource(SoundbankResource resource) {
        if (resource instanceof DLSInstrument)
            instruments.add((DLSInstrument) resource);
        if (resource instanceof DLSSample)
            samples.add((DLSSample) resource);
    }

    public void removeResource(SoundbankResource resource) {
        if (resource instanceof DLSInstrument)
            instruments.remove((DLSInstrument) resource);
        if (resource instanceof DLSSample)
            samples.remove((DLSSample) resource);
    }

    public void addInstrument(DLSInstrument resource) {
        instruments.add(resource);
    }

    public void removeInstrument(DLSInstrument resource) {
        instruments.remove(resource);
    }

    public long getMajor() {
        return major;
    }

    public void setMajor(long major) {
        this.major = major;
    }

    public long getMinor() {
        return minor;
    }

    public void setMinor(long minor) {
        this.minor = minor;
    }
}
