/*
 * @(#)AnimMerger.java  1.0  2004-09-07
 *
 * Copyright (c) 2004 Werner Randelshofer, Immensee, Switzerland.
 * All rights reserved.
 *
 * You may not use, copy or modify this file, except in compliance with the
 * license agreement you entered into with Werner Randelshofer.
 * For details see accompanying license terms.
 */

package org.monte.media.iff;

import org.monte.media.AbortException;
import org.monte.media.ParseException;
import org.monte.media.gui.Worker;
import java.text.NumberFormat;
import javax.swing.tree.*;
import javax.swing.*;
import java.io.*;
import java.util.*;
/**
 *
 * @author  werni
 */
public class AnimMerger extends javax.swing.JFrame {
    private File file1, file2;
    private JFileChooser chooser;
    private static NumberFormat numFormat = NumberFormat.getInstance();
    
    /** Creates new form AnimMerger */
    public AnimMerger() {
        initComponents();
    }
    
    public final static int CMAP_ID = IFFParser.stringToID("CMAP");
    public final static int ILBM_ID = IFFParser.stringToID("ILBM");
    public final static int ANHD_ID = IFFParser.stringToID("ANHD");
    
    private final static int[] sequence = {
        IFFParser.stringToID("ANIM"),
        IFFParser.stringToID("8SVX"),
        IFFParser.stringToID("ILBM"),
        IFFParser.stringToID("VHDR"),
        IFFParser.stringToID("ANNO"),
        IFFParser.stringToID("CAMG"),
        IFFParser.stringToID("BMHD"),
        IFFParser.stringToID("ANHD"),
        IFFParser.stringToID("CHAN"),
        IFFParser.stringToID("SCTL"),
        IFFParser.stringToID("CMAP"),
        IFFParser.stringToID("DPPS"),
        IFFParser.stringToID("DPAN"),
        IFFParser.stringToID("DLTA"),
        IFFParser.stringToID("BODY"),
    };
    
    private static Comparator nodeComparator = new Comparator() {
        public int compare(Object o1, Object o2) {
            return compare((IFFChunkNode) o1, (IFFChunkNode) o2);
        }
        public int compare(IFFChunkNode o1, IFFChunkNode o2) {
            return o1.getSeqIndex() - o2.getSeqIndex();
        }
    };
    
    protected static class IFFChunkNode extends DefaultMutableTreeNode {
        private int type;
        private int id;
        private int size;
        private int offset;
        private byte[] data;
        
        public int getPaddedChunkSize() {
            return size + (size % 2) + 8;
        }
        
        public void write(DataOutputStream out) throws IOException {
            System.out.println("write "+IFFParser.idToString(id)+" size="+size);
            if (data != null) {
                out.writeInt(id);
                out.writeInt(size);
                out.write(data);
                if (data.length % 2 == 1) {
                    out.writeByte(0);
                }
            } else {
                out.writeInt(type);
                out.writeInt(size);
                out.writeInt(id);
                for (Enumeration i = children(); i.hasMoreElements(); ) {
                    IFFChunkNode child = (IFFChunkNode) i.nextElement();
                    child.write(out);
                }
            }
        }
        
        public void mergeFrom(IFFChunkNode that) {
            if (data != null) {
                if (id == ANHD_ID) mergeANHD(that);
            } else {
                
                if (this.isSameAs(that)) {
                    final ArrayList mergedChildren = new ArrayList(Math.max(this.getChildCount(), that.getChildCount()));
                    Collections.sort(this.children, nodeComparator);
                    Collections.sort(that.children, nodeComparator);
                    
                    int thatCount = that.getChildCount();
                    int thisCount = this.getChildCount();
                    int thisIndex = 0, thatIndex = 0;
                    int comparison;
                    while (thisIndex < thisCount || thatIndex < thatCount) {
                        if (thisIndex >= thisCount) {
                            comparison = 1;
                        } else if (thatIndex >= thatCount) {
                            comparison = -1;
                        } else {
                            comparison = ((IFFChunkNode) this.getChildAt(thisIndex)).compareTo((IFFChunkNode) that.getChildAt(thatIndex));
                        }
                        
                        if (comparison < 0) {
                            System.out.println("Inserting from File1:"+this.getChildAt(thisIndex)+" into "+this);
                            mergedChildren.add(this.getChildAt(thisIndex));
                            thisIndex++;
                        } else if (comparison == 0) {
                            ((IFFChunkNode) this.getChildAt(thisIndex)).mergeFrom((IFFChunkNode) that.getChildAt(thatIndex));
                            mergedChildren.add(this.getChildAt(thisIndex));
                            thatIndex++; thisIndex++;
                        } else {
                            IFFChunkNode thatNode = (IFFChunkNode) that.getChildAt(thatIndex);
                            if (thatNode.id == CMAP_ID || thatNode.id == ILBM_ID) {
                                System.out.println("Skipping from File2:"+that.getChildAt(thatIndex));
                            } else {
                                System.out.println("Inserting from File2:"+that.getChildAt(thatIndex)+" into "+this);
                                mergedChildren.add(that.getChildAt(thatIndex));
                            }
                            thatIndex++;
                        }
                    }
                    
                    this.removeAllChildren();
                    this.size = 4;
                    for (Iterator i = mergedChildren.iterator(); i.hasNext(); ) {
                        IFFChunkNode newChild = (IFFChunkNode) i.next();
                        this.size += newChild.getPaddedChunkSize();
                        this.add(newChild);
                    }
                } else {
                    System.out.println(this+"!="+that);
                }
            }
        }
        
        public void mergeANHD(IFFChunkNode that) {
            System.out.print(IFFParser.idToString(id)+" "+IFFParser.idToString(that.id)+" reltime=");
            for (int i=0; i < 4; i++) {
                this.data[10+i]=0;
            }
            for (int i=0; i < 4; i++) {
                this.data[14+i]=that.data[14+i];
                System.out.print(that.data[14+i]+" ");
            }
            System.out.println();
        }
        
        public int compareTo(IFFChunkNode that) {
            return this.getSeqIndex() - that.getSeqIndex();
        }
        
        public int getSeqIndex() {
            for (int i=0; i < sequence.length; i++) {
                if (id == sequence[i]) return i;
            }
            throw new ArrayIndexOutOfBoundsException("no index for "+IFFParser.idToString(id));
            //return -1;
        }
        
        public IFFChunkNode(int type, int id, int size, int offset, byte[] data) {
            this.type = type;
            this.id = id;
            this.size = size;
            this.offset = offset;
            this.data = data;
        }
        public String getType() {
            return IFFParser.idToString(type);
        }
        public String getID() {
            return IFFParser.idToString(id);
        }
        public int getSize() {
            return size;
        }
        public int getOffset() {
            return offset;
        }
        public byte[] getRawData() {
            return data;
        }
        
        public boolean isSameAs(IFFChunkNode that) {
            return this.type == that.type && this.id == that.id;
        }
        
        public String toString() {
            return IFFParser.idToString(type) + " "+IFFParser.idToString(id);
        }
        
        
        public void dump(int depth) {
            StringBuffer buf = new StringBuffer(depth);
            for (int i=0; i < depth; i++) {
                buf.append('.');
            }
            buf.append(IFFParser.idToString(type) + " "+IFFParser.idToString(id)+" "+numFormat.format(size));
            System.out.println(buf.toString());
            
            for (Enumeration i=children(); i.hasMoreElements(); ) {
                IFFChunkNode child = (IFFChunkNode) i.nextElement();
                child.dump(depth + 1);
            }
        }
    }
    
    
    protected static class Loader implements IFFVisitor {
        private IFFChunkNode root;
        private IFFChunkNode current;
        
        public Loader() {
        }
        
        public IFFChunkNode getRoot() {
            return root;
        }
        
        public void enterGroup(IFFChunk group) throws ParseException, AbortException {
            IFFChunkNode groupNode = new IFFChunkNode(
            group.getID(),
            group.getType(),
            (int) group.getSize(),
            (int) group.getScan(),
            null
            );
            
            if (current == null) {
                root = current = groupNode;
            } else {
                current.add(groupNode);
                current = groupNode;
            }
        }
        
        public void leaveGroup(IFFChunk group) throws ParseException, AbortException {
            current = (IFFChunkNode) current.getParent();
        }
        
        public void visitChunk(IFFChunk group, IFFChunk chunk) throws ParseException, AbortException {
            IFFChunkNode chunkNode = new IFFChunkNode(
            group.getType(),
            chunk.getID(),
            (int) chunk.getSize(),
            (int) chunk.getScan(),
            chunk.getData()
            );
            current.add(chunkNode);
        }
    }
/*
    protected static class Merger implements IFFVisitor {
        private IFFChunkNode root;
        private IFFChunkNode current;
 
        public Merger(IFFChunkNode root) {
            this.root = root;
            this.current = null;
        }
 
        public IFFChunkNode getRoot() {
            return root;
        }
 
        public void enterGroup(IFFChunk group) throws ParseException, AbortException {
            IFFChunkNode groupNode = new IFFChunkNode(
            group.getID(),
            group.getType(),
            (int) group.getSize(),
            (int) group.getScan(),
            null
            );
            if (current == null) {
                if (! group.isSameAs(root)) {
                    throw new ParseException("Root nodes don't match "+groupNode);
                }
                current = root;
            } else {
                current.add(groupNode);
                current = groupNode;
            }
        }
 
        public void leaveGroup(IFFChunk group) throws ParseException, AbortException {
            current = (IFFChunkNode) current.getParent();
        }
 
        public void visitChunk(IFFChunk group, IFFChunk chunk) throws ParseException, AbortException {
            IFFChunkNode chunkNode = new IFFChunkNode(
            group.getType(),
            chunk.getID(),
            (int) chunk.getSize(),
            (int) chunk.getScan(),
            chunk.getData()
            );
            current.add(chunkNode);
        }
    }
 */
    private void merge(File f1, File f2) throws IOException {
        IFFChunkNode root1, root2;
        
        Loader loader = new Loader();
        InputStream in = new FileInputStream(f1);
        try {
            IFFParser iff = new IFFParser();
            iff.parse(in, loader);
        } catch (ParseException e) {
            throw new IOException(e.getMessage());
        } catch (AbortException e) {
            throw new IOException(e.getMessage());
        } finally {
            in.close(); 
        }
        
        root1 = loader.getRoot();
        
        loader = new Loader();
        in = new FileInputStream(f2);
        try {
            IFFParser iff = new IFFParser();
            iff.parse(in, loader);
        } catch (ParseException e) {
            throw new IOException(e.getMessage());
        } catch (AbortException e) {
            throw new IOException(e.getMessage());
        } finally {
            in.close();
        }
        
        root2 = loader.getRoot();
        
        root1.mergeFrom(root2);
        
        
        File file3 = new File(file1.getParentFile(), "merged"+file1.getName());
        
        DataOutputStream out = new DataOutputStream(new FileOutputStream(file3));
        try {
            root1.write(out);
            out.flush();
        } finally {
           out.close();
        }
//        root1.dump(0);
        
    }
    
    /** This method is called from within the constructor to
     * initialize the form.
     * WARNING: Do NOT modify this code. The content of this method is
     * always regenerated by the Form Editor.
     */
    private void initComponents() {//GEN-BEGIN:initComponents
        menuBar = new javax.swing.JMenuBar();
        fileMenu = new javax.swing.JMenu();
        mergeMenuItem = new javax.swing.JMenuItem();
        exitMenuItem = new javax.swing.JMenuItem();

        setTitle("ANIM Merger");
        fileMenu.setText("File");
        mergeMenuItem.setText("Merge...");
        mergeMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                merge(evt);
            }
        });

        fileMenu.add(mergeMenuItem);

        exitMenuItem.setText("Exit");
        exitMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                exitMenuItemActionPerformed(evt);
            }
        });

        fileMenu.add(exitMenuItem);

        menuBar.add(fileMenu);

        setJMenuBar(menuBar);

        pack();
    }//GEN-END:initComponents
    
    private void merge(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_merge
        // TODO add your handling code here:
        if (chooser == null) chooser = new JFileChooser();
        chooser.setDialogType(JFileChooser.OPEN_DIALOG);
        if (file1 != null) chooser.setSelectedFile(file1);
        if (chooser.showDialog(this, "Open File 1") == JFileChooser.APPROVE_OPTION) {
            file1 = chooser.getSelectedFile();
            if (file2 != null) chooser.setSelectedFile(file2);
            if (chooser.showDialog(this, "Open File 2") == JFileChooser.APPROVE_OPTION) {
                file2 = chooser.getSelectedFile();
                new Worker() {
                    @Override
                    public Object construct() {
                        try {
                            merge(file1, file2);
                            return null;
                        } catch (Throwable t) {
                            return t;
                        }
                    }
                    @Override
                    public void finished() {
                        Object result = getValue();
                        if (result instanceof Throwable) {
                            ((Throwable) result).printStackTrace();
                            JOptionPane.showMessageDialog(AnimMerger.this, "<html>Error merging files<br>"+result, "AnimMerger", JOptionPane.ERROR_MESSAGE);
                        } else {
                            JOptionPane.showMessageDialog(AnimMerger.this, "Done", "AnimMerger", JOptionPane.INFORMATION_MESSAGE);
                        }
                    }
                }.start();
            }
        }
        
    }//GEN-LAST:event_merge
    
    
    
    private void exitMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_exitMenuItemActionPerformed
        System.exit(0);
    }//GEN-LAST:event_exitMenuItemActionPerformed
    
    /**
     * @param args the command line arguments
     */
    public static void main(String args[]) {
        JFrame f = new AnimMerger();
        f.setSize(400, 300);
        f.setVisible(true);
    }
    
    // Variables declaration - do not modify//GEN-BEGIN:variables
    private javax.swing.JMenuItem exitMenuItem;
    private javax.swing.JMenu fileMenu;
    private javax.swing.JMenuBar menuBar;
    private javax.swing.JMenuItem mergeMenuItem;
    // End of variables declaration//GEN-END:variables
    
}
