/*
 * Created on Jun 7, 2004
 *
 * To change the template for this generated file go to
 * Window - Preferences - Java - Code Generation - Code and Comments
 */
package v4lk.lwbd.decoders.processing.jflac.apps;

import v4lk.lwbd.decoders.processing.jflac.Constants;
import v4lk.lwbd.decoders.processing.jflac.FLACDecoder;
import v4lk.lwbd.decoders.processing.jflac.io.BitOutputStream;
import v4lk.lwbd.decoders.processing.jflac.metadata.Metadata;
import v4lk.lwbd.decoders.processing.jflac.metadata.SeekPoint;
import v4lk.lwbd.decoders.processing.jflac.metadata.SeekTable;
import v4lk.lwbd.decoders.processing.jflac.metadata.StreamInfo;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.*;
import java.util.ArrayList;


/**
 * Assemble several FLAC files into one album.
 *
 * @author kc7bfi
 */
public class FlacPacker extends JFrame {

    private JTextArea textArea = new JTextArea(16, 50);
    private JButton addButton = new JButton("Add Files");
    private JButton makeButton = new JButton("Pack FLAC");

    private ArrayList flacFiles = new ArrayList();
    private ArrayList albumFiles = new ArrayList();
    private StreamInfo masterStreamInfo = null;
    private byte[] buffer = new byte[64 * 1024];

    /**
     * Constructor.
     *
     * @param title Frame title
     */
    public FlacPacker(String title) {
        super(title);
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        this.getContentPane().setLayout(new BorderLayout());

        // text area
        textArea.setText("");
        textArea.setAutoscrolls(true);
        this.getContentPane().add(textArea, BorderLayout.CENTER);

        // button pannel
        Panel buttonPanel = new Panel();
        buttonPanel.setLayout(new FlowLayout());
        buttonPanel.add(addButton);
        buttonPanel.add(makeButton);
        this.getContentPane().add(buttonPanel, BorderLayout.SOUTH);

        this.pack();

        addButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent event) {
                addFilesToList();
            }
        });

        makeButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent event) {
                try {
                    packFlac();
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        });
    }

    private void appendMsg(String msg) {
        textArea.setText(textArea.getText() + msg + "\n");
        textArea.repaint();
    }

    private void addFilesToList() {
        JFileChooser chooser = new JFileChooser();
        ExtensionFileFilter filter = new ExtensionFileFilter();
        filter.addExtension("flac");
        filter.setDescription("FLAC files");
        chooser.setMultiSelectionEnabled(true);
        chooser.setFileFilter(filter);
        chooser.setCurrentDirectory(new File("."));
        int returnVal = chooser.showOpenDialog(this);
        if (returnVal != JFileChooser.APPROVE_OPTION) return;
        File[] files = chooser.getSelectedFiles();
        for (int i = 0; i < files.length; i++) flacFiles.add(files[i]);
    }

    private File getOutputFile() {
        JFileChooser chooser = new JFileChooser();
        ExtensionFileFilter filter = new ExtensionFileFilter();
        filter.addExtension("flac");
        filter.setDescription("FLAC files");
        chooser.setFileFilter(filter);
        chooser.setCurrentDirectory(new File("."));
        int returnVal = chooser.showOpenDialog(this);
        if (returnVal != JFileChooser.APPROVE_OPTION) return null;
        File file = chooser.getSelectedFile();
        return file;
    }

    private SeekTable makeSeekTable() {
        long lastSampleNumber = 0;
        long lastStreamOffset = 0;

        // process selected files
        for (int i = 0; i < flacFiles.size(); i++) {
            File file = (File) flacFiles.get(i);
            try {
                FileInputStream is = new FileInputStream(file);
                FLACDecoder decoder = new FLACDecoder(is);
                decoder.readMetadata();
                StreamInfo info = decoder.getStreamInfo();
                if (masterStreamInfo == null) {
                    masterStreamInfo = info;
                    masterStreamInfo.setTotalSamples(0);
                }
                if (!info.compatiable(masterStreamInfo)) {
                    appendMsg("Bad StreamInfo " + file + ": " + info);
                    continue;
                }
                masterStreamInfo.addTotalSamples(info.getTotalSamples());

                SeekPoint seekPoint = new SeekPoint(lastSampleNumber, lastStreamOffset, 0);
                //decoder.processMetadata();
                long frameStartOffs = decoder.getTotalBytesRead();
                PackerFile aFile = new PackerFile(file, seekPoint, frameStartOffs);
                albumFiles.add(aFile);
                //System.out.println("Do decode " +i);
                try {
                    decoder.decodeFrames();
                } catch (EOFException e) {
                    //appendMsg("File " + file + ": " + e);
                }
                //System.out.println("Done decode");
                long frameEndOffs = decoder.getTotalBytesRead();
                //appendMsg(frameStartOffs + " " + frameEndOffs + " " + decoder.getSamplesDecoded());
                lastSampleNumber += decoder.getSamplesDecoded();
                lastStreamOffset += frameEndOffs - frameStartOffs;
            } catch (FileNotFoundException e) {
                appendMsg("File " + file + ": " + e);
            } catch (IOException e) {
                appendMsg("File " + file + ": " + e);
            }
        }

        // make Seek Table
        SeekPoint[] points = new SeekPoint[albumFiles.size()];
        SeekTable seekTable = new SeekTable(points, true);
        int metadataHeader = (Metadata.STREAM_METADATA_IS_LAST_LEN + Metadata.STREAM_METADATA_TYPE_LEN + Metadata.STREAM_METADATA_LENGTH_LEN) / 8;
        int metadataOffset = Constants.STREAM_SYNC_STRING.length + masterStreamInfo.calcLength() + seekTable.calcLength() + metadataHeader * 2;
        for (int i = 0; i < albumFiles.size(); i++) {
            PackerFile aFile = (PackerFile) albumFiles.get(i);
            appendMsg("SeekTable build " + i + " Offset=" + aFile.seekPoint.getStreamOffset() + " header=" + metadataOffset);
            aFile.seekPoint.setStreamOffset(aFile.seekPoint.getStreamOffset() + metadataOffset);
            points[i] = aFile.seekPoint;
        }

        return seekTable;
    }

    private void packFlac() throws IOException {
        // get output file
        File outFile = getOutputFile();
        if (outFile == null) return;
        BitOutputStream os = null;
        FileOutputStream fos;
        try {
            fos = new FileOutputStream(outFile);
            os = new BitOutputStream(fos);
        } catch (FileNotFoundException e1) {
            e1.printStackTrace();
            return;
        }

        // get seek table
        SeekTable seekTable = makeSeekTable();
        if (masterStreamInfo == null) return;

        // write FLAC marker
        os.writeByteBlock(Constants.STREAM_SYNC_STRING, Constants.STREAM_SYNC_STRING.length);

        // output StreamInfo
        masterStreamInfo.write(os, false);

        // output SeekTable
        seekTable.write(os, true);

        // generate output file
        for (int i = 0; i < albumFiles.size(); i++) {
            PackerFile aFile = (PackerFile) albumFiles.get(i);
            appendMsg("Process file " + i + ": " + aFile.file);
            try {
                RandomAccessFile raf = new RandomAccessFile(aFile.file, "r");
                raf.seek(aFile.firstFrameOffset);
                for (int bytes = raf.read(buffer); bytes > 0; bytes = raf.read(buffer)) {
                    fos.write(buffer, 0, bytes);
                }
                fos.flush();
            } catch (FileNotFoundException e) {
                appendMsg("File " + aFile.file + ": " + e);
            } catch (EOFException e) {
                appendMsg("File " + aFile.file + ": Done!");
            } catch (IOException e) {
                appendMsg("File " + aFile.file + ": " + e);
            }
        }
    }

    /**
     * Main routine.
     *
     * @param args Command line arguments
     */
    public static void main(String[] args) {
        FlacPacker app = new FlacPacker("FLAC Album Maker");
        app.show(true);
    }

    /**
     * This class holds the fiels and their seek points.
     *
     * @author kc7bfi
     */
    private class PackerFile {
        protected File file;
        protected SeekPoint seekPoint;
        protected long firstFrameOffset;

        /**
         * The constructor.
         *
         * @param file      The file
         * @param seekPoint The SeekPoint
         */
        public PackerFile(File file, SeekPoint seekPoint, long firstFrameOffset) {
            this.file = file;
            this.seekPoint = seekPoint;
            this.firstFrameOffset = firstFrameOffset;
        }
    }
}
