/*
 * @(#)LPFStructView.java  1.0  2011-08-18
 *
 * Copyright (c) 2011 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 ch.randelshofer.view;

import org.monte.media.ParseException;
import ch.randelshofer.io.CopyingInputStream;
import org.monte.media.binary.*;
import org.monte.media.io.UncachedImageInputStream;
import ch.randelshofer.util.*;

import java.awt.*;
import java.io.*;
import java.nio.ByteOrder;

import javax.activation.DataHandler;
import javax.imageio.stream.MemoryCacheImageInputStream;

import javax.swing.*;
import javax.swing.tree.*;

/**
 * Displays the structure of a Large Page Format "LPF" animations made with 
 * Deluxe Paint PC.
 *
 * @author  Werner Randelshofer
 * @version 1.0 2011-08-18 Created.
 */
public class LPFStructView extends AbstractStructView {

    private InputStream inputStream;
    private final static String HEADER_ID = "Header";
    private final static String COLOR_CYCLING_ID = "ColorCycling";
    private final static String COLOR_TABLE_ID = "ColorTable";
    private final static String PAGE_DESCRIPTOR_TABLE_ID = "PageDescriptorTable";
    private final static String PAGE_ID = "Page";
    private final static String PAGE_HEADER_ID = "PageHeader";
    private final static String RECORD_ID = "Record";
    private final static String FREE_ID = "Free";
    private final static String DATA_ID = "Data";

    protected static class LPFNode
            implements AbstractStructView.StructNode {

        private LPFModel model;
        private String id;
        private byte[] data;
        private int indexInParent = -1;
        private long offset;
        private long size;

        public LPFNode(LPFModel model, String id, long offset, byte[] data) {
            this.model = model;
            this.id = id;
            this.offset = offset;
            this.data = data;
        }

        @Override
        public String getName() {
            if (indexInParent == -1) {
                return model.getName(id);
            } else {
                return model.getName(id) + " " + indexInParent;
            }
        }

        @Override
        public StructTableModel getStructTableModel() {
            return model.getStructTableModel(this);
        }

        @Override
        public byte[] getRawData(long offset, int length) {
            if (data == null || offset == 0 && length == data.length) {
                return data;
            } else {
                byte[] bytes = new byte[length];
                System.arraycopy(data, 0, bytes, (int) offset, length);
                return bytes;
            }
        }

        @Override
        public java.util.List<BinaryPanel.Highlight> getRawDataHighlights() {
            return null;
        }

        @Override
        public String toString() {
            return getName();
        }

        @Override
        public String getId() {
            return id;
        }

        @Override
        public long getSize() {
            return data == null ? size : data.length;
        }

        public void setSize(long size) {
            this.size = size;
        }

        @Override
        public long getOffset() {
            return offset;
        }

        @Override
        public String getDescription() {
            return model.getDescription(id);
        }

        @Override
        public int getIndexInParent() {
            return indexInParent;
        }

        public void setIndexInParent(int cc) {
            indexInParent = cc;
        }

        @Override
        public boolean hasRawData() {
            return data != null;
        }

        @Override
        public File getFile() {
            return null;
        }
    }

    protected static class LPFModel
            extends DefaultTreeModel {

        private static StructParser structParser;

        public LPFModel(InputStream in)
                throws IOException {
            super(new DefaultMutableTreeNode());
            DefaultMutableTreeNode root = (DefaultMutableTreeNode) getRoot();

            // Parse the CyberPaintSeqStruct's definitions.
            if (structParser == null) {
                BufferedReader reader = null;
                try {
                    reader = new BufferedReader(
                            new InputStreamReader(
                            LPFStructView.class.getResourceAsStream("LPFStructs.txt")));
                    structParser = (reader == null) ? new StructParser() : new StructParser(reader);
                } catch (NullPointerException e) {
                    InternalError error = new InternalError(e.toString());
                    error.initCause(e);
                    throw error;
                } catch (ParseException e) {
                    InternalError error = new InternalError(e.toString());
                    error.initCause(e);
                    throw error;
                } finally {
                    if (reader != null) {
                        reader.close();
                    }
                }
            }

            // Parse the input stream
            if (!(in instanceof BufferedInputStream)) {
                in = new BufferedInputStream(in);
            }
            ByteArrayOutputStream buf = new ByteArrayOutputStream();
            CopyingInputStream cin = new CopyingInputStream(in, buf);
            UncachedImageInputStream din = new UncachedImageInputStream(cin);
            din.setByteOrder(ByteOrder.LITTLE_ENDIAN);

            // Parse the Header
            {
                buf.reset();
                long pos = din.getStreamPosition();
                din.skipBytes(128);
                LPFNode node = new LPFNode(this, HEADER_ID, pos, buf.toByteArray());
                DefaultMutableTreeNode treeNode = new DefaultMutableTreeNode(node);
                root.add(treeNode);
            }
            // Parse the CRNG chunks
            {
                buf.reset();
                long pos = din.getStreamPosition();
                din.skipBytes(128);
                LPFNode node = new LPFNode(this, COLOR_CYCLING_ID, pos, buf.toByteArray());
                DefaultMutableTreeNode treeNode = new DefaultMutableTreeNode(node);
                root.add(treeNode);
            }

            // Parse the color table 
            {
                buf.reset();
                long pos = din.getStreamPosition();
                din.skipBytes(1024);
                LPFNode node = new LPFNode(this, COLOR_TABLE_ID, pos, buf.toByteArray());
                DefaultMutableTreeNode treeNode = new DefaultMutableTreeNode(node);
                root.add(treeNode);
            }

            // Parse the page descriptors 
            int[] pdBaseRecord = new int[256];
            int[] pdNbRecords = new int[256];
            int[] pdNbBytes = new int[256];
            {
                buf.reset();
                long pos = din.getStreamPosition();
                for (int i = 0; i < 256; i++) {
                    pdBaseRecord[i] = din.readUnsignedShort();
                    pdNbRecords[i] = din.readUnsignedShort();
                    pdNbBytes[i] = din.readUnsignedShort();
                }
                LPFNode node = new LPFNode(this, PAGE_DESCRIPTOR_TABLE_ID, pos, buf.toByteArray());
                DefaultMutableTreeNode treeNode = new DefaultMutableTreeNode(node);
                root.add(treeNode);
            }

            // Parse the pages
            {
                for (int i = 0; i < 256 && pdNbRecords[i] > 0; i++) {
                    buf.reset();
                    long pos = din.getStreamPosition();
                    LPFNode page = new LPFNode(this, PAGE_ID, pos, null);
                    page.setSize(1<<16);
                    DefaultMutableTreeNode pageNode = new DefaultMutableTreeNode(page);
                        page.setIndexInParent(i);
                    root.add(pageNode);
                    
                    // Page header
                    int pgBaseRecord = din.readUnsignedShort();
                    int pgNbRecords = din.readUnsignedShort();
                    int pgNbBytes = din.readUnsignedShort();
                    int padding =din.readUnsignedShort();
                    int[] recordSizes=new int[pgNbRecords];
                    for (int j=0;j<pgNbRecords;j++) {
                        recordSizes[j]=din.readUnsignedShort();
                    }
                    LPFNode node = new LPFNode(this, PAGE_HEADER_ID, pos, buf.toByteArray());
                    DefaultMutableTreeNode treeNode = new DefaultMutableTreeNode(node);
                    pageNode.add(treeNode);
                    
                    // Page records
                    try {
                    for (int j = 0; j < pdNbRecords[i]; j++) {
                        pos = din.getStreamPosition();
                        int recNbBytes = recordSizes[j];
                        buf.reset();
                        din.skipBytes(recNbBytes);
                        node = new LPFNode(this, RECORD_ID, pos, buf.toByteArray());
                        node.setIndexInParent(j);
                        treeNode = new DefaultMutableTreeNode(node);
                        pageNode.add(treeNode);
                    }
                    if (din.getStreamPosition()<(i+1)*(1<<16)+0xb00) {
                        buf.reset();
                        pos = din.getStreamPosition();
                        din.skipBytes((i+1)*(1<<16)+0xb00-din.getStreamPosition());
                        node = new LPFNode(this, FREE_ID, pos, buf.toByteArray());
                        treeNode = new DefaultMutableTreeNode(node);
                        pageNode.add(treeNode);
                    }
                    } catch (EOFException e) {
                        page.setSize(din.getStreamPosition()-page.offset);
                        
                        return;
                    }

                }
            }

            // Parse the Data
            {
                buf.reset();
                din.skipBytes(32000);
                LPFNode node = new LPFNode(this, DATA_ID, 128, buf.toByteArray());
                DefaultMutableTreeNode treeNode = new DefaultMutableTreeNode(node);
                root.add(treeNode);
            }

        }

        public StructTableModel getStructTableModel(LPFNode node) {
            try {
                byte[] data = node.getRawData(0, (int) node.getSize());
                if (data == null) {
                    return null;
                }
                return structParser.readStruct(node.id, data);
            } catch (IOException e) {
                //e.printStackTrace();
                return null;
            }
        }

        public String getName(String id) {
            return structParser.getName(id);
        }

        public String getDescription(String id) {
            return structParser.getDescription(id);
        }
    }

    /** Creates new form PictStructView */
    public LPFStructView() {
        //initComponents();
    }

    @Override
    public void setCommandContext(String verb, DataHandler dh)
            throws IOException {
        inputStream = new BufferedInputStream(dh.getInputStream());
        setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));

        ch.randelshofer.gui.SwingWorker worker = new ch.randelshofer.gui.SwingWorker() {

            @Override
            public Object construct() {
                Object result;

                try {
                    result = new LPFModel(inputStream);
                } catch (IOException e) {
                    result = e;
                }
                try {
                    inputStream.close();
                } catch (IOException e) {
                }

                return result;
            }

            @Override
            public void finished() {
                Object result = get();
                if (result instanceof LPFModel) {
                    getTree().setModel((LPFModel) result);
                } else {
                    removeAll();
                    add(new JLabel(result.toString()));
                    if (result instanceof Throwable) {
                        ((Throwable) result).printStackTrace();
                    }
                }
                setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
                getTree().expandRow(0);
                getTree().setSelectionRow(0);
                getSplitPane().setDividerLocation(getTree().getPreferredSize().width + 20);
            }
        };
        worker.start();
    }

    @Override
    public void dispose() {
        if (inputStream != null) {
            try {
                inputStream.close();
            } catch (IOException e) {
            }
            inputStream = null;
        }
    }

    /** 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.
     */
    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
    private void initComponents() {
    }// </editor-fold>//GEN-END:initComponents
    // Variables declaration - do not modify//GEN-BEGIN:variables
    // End of variables declaration//GEN-END:variables
}
