/*
 * Copyright (c) 2009-2012 jMonkeyEngine
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * * Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 *
 * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
 *   may be used to endorse or promote products derived from this software
 *   without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.jme3.export.binary;

import com.jme3.asset.AssetInfo;
import com.jme3.asset.AssetManager;
import com.jme3.export.*;
import com.jme3.math.FastMath;
import java.io.*;
import java.net.URL;
import java.nio.ByteOrder;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * @author Joshua Slack
 * @author Kirill Vainer - Version number, Fast buffer reading
 */
public final class BinaryImporter implements JmeImporter {
    private static final Logger logger = Logger.getLogger(BinaryImporter.class
            .getName());

    private AssetManager assetManager;

    //Key - alias, object - bco
    private HashMap<String, BinaryClassObject> classes
             = new HashMap<String, BinaryClassObject>();
    //Key - id, object - the savable
    private HashMap<Integer, Savable> contentTable
            = new HashMap<Integer, Savable>();
    //Key - savable, object - capsule
    private IdentityHashMap<Savable, BinaryInputCapsule> capsuleTable
             = new IdentityHashMap<Savable, BinaryInputCapsule>();
    //Key - id, opject - location in the file
    private HashMap<Integer, Integer> locationTable
             = new HashMap<Integer, Integer>();

    public static boolean debug = false;

    private byte[] dataArray;
    private int aliasWidth;
    private int formatVersion;

    private static final boolean fastRead = ByteOrder.nativeOrder() == ByteOrder.LITTLE_ENDIAN;
    
    public BinaryImporter() {
    }
    
    public int getFormatVersion(){
        return formatVersion;
    }
    
    public static boolean canUseFastBuffers(){
        return fastRead;
    }

    public static BinaryImporter getInstance() {
        return new BinaryImporter();
    }

    public void setAssetManager(AssetManager manager){
        this.assetManager = manager;
    }

    public AssetManager getAssetManager(){
        return assetManager;
    }

    public Object load(AssetInfo info){
//        if (!(info.getKey() instanceof ModelKey))
//            throw new IllegalArgumentException("Model assets must be loaded using a ModelKey");

        assetManager = info.getManager();

        InputStream is = null;
        try {
            is = info.openStream();
            Savable s = load(is);
            
            return s;
        } catch (IOException ex) {
            logger.log(Level.SEVERE, "An error occured while loading jME binary object", ex);
        } finally {
            if (is != null){
                try {
                    is.close();
                } catch (IOException ex) {}
            }
        }
        return null;
    }

    public Savable load(InputStream is) throws IOException {
        return load(is, null, null);
    }

    public Savable load(InputStream is, ReadListener listener) throws IOException {
        return load(is, listener, null);
    }

    public Savable load(InputStream is, ReadListener listener, ByteArrayOutputStream baos) throws IOException {
        contentTable.clear();
        BufferedInputStream bis = new BufferedInputStream(is);
        
        int numClasses;
        
        // Try to read signature
        int maybeSignature = ByteUtils.readInt(bis);
        if (maybeSignature == FormatVersion.SIGNATURE){
            // this is a new version J3O file
            formatVersion = ByteUtils.readInt(bis);
            numClasses = ByteUtils.readInt(bis);
            
            // check if this binary is from the future
            if (formatVersion > FormatVersion.VERSION){
                throw new IOException("The binary file is of newer version than expected! " + 
                                      formatVersion + " > " + FormatVersion.VERSION);
            }
        }else{
            // this is an old version J3O file
            // the signature was actually the class count
            numClasses = maybeSignature;
            
            // 0 indicates version before we started adding
            // version numbers
            formatVersion = 0; 
        }
        
        int bytes = 4;
        aliasWidth = ((int)FastMath.log(numClasses, 256) + 1);

        classes.clear();
        for(int i = 0; i < numClasses; i++) {
            String alias = readString(bis, aliasWidth);
            
            // jME3 NEW: Read class version number
            int[] classHierarchyVersions;
            if (formatVersion >= 1){
                int classHierarchySize = bis.read();
                classHierarchyVersions = new int[classHierarchySize];
                for (int j = 0; j < classHierarchySize; j++){
                    classHierarchyVersions[j] = ByteUtils.readInt(bis);
                }
            }else{
                classHierarchyVersions = new int[]{ 0 };
            }
            
            // read classname and classname size
            int classLength = ByteUtils.readInt(bis);
            String className = readString(bis, classLength);
            
            BinaryClassObject bco = new BinaryClassObject();
            bco.alias = alias.getBytes();
            bco.className = className;
            bco.classHierarchyVersions = classHierarchyVersions;
            
            int fields = ByteUtils.readInt(bis);
            bytes += (8 + aliasWidth + classLength);

            bco.nameFields = new HashMap<String, BinaryClassField>(fields);
            bco.aliasFields = new HashMap<Byte, BinaryClassField>(fields);
            for (int x = 0; x < fields; x++) {
                byte fieldAlias = (byte)bis.read();
                byte fieldType = (byte)bis.read();

                int fieldNameLength = ByteUtils.readInt(bis);
                String fieldName = readString(bis, fieldNameLength);
                BinaryClassField bcf = new BinaryClassField(fieldName, fieldAlias, fieldType);
                bco.nameFields.put(fieldName, bcf);
                bco.aliasFields.put(fieldAlias, bcf);
                bytes += (6 + fieldNameLength);
            }
            classes.put(alias, bco);
        }
        if (listener != null) listener.readBytes(bytes);

        int numLocs = ByteUtils.readInt(bis);
        bytes = 4;

        capsuleTable.clear();
        locationTable.clear();
        for(int i = 0; i < numLocs; i++) {
            int id = ByteUtils.readInt(bis);
            int loc = ByteUtils.readInt(bis);
            locationTable.put(id, loc);
            bytes += 8;
        }

        @SuppressWarnings("unused")
        int numbIDs = ByteUtils.readInt(bis); // XXX: NOT CURRENTLY USED
        int id = ByteUtils.readInt(bis);
        bytes += 8;
        if (listener != null) listener.readBytes(bytes);

        if (baos == null) {
                baos = new ByteArrayOutputStream(bytes);
        } else {
                baos.reset();
        }
        int size = -1;
        byte[] cache = new byte[4096];
        while((size = bis.read(cache)) != -1) {
            baos.write(cache, 0, size);
            if (listener != null) listener.readBytes(size);
        }
        bis = null;

        dataArray = baos.toByteArray();
        baos = null;

        Savable rVal = readObject(id);
        if (debug) {
            logger.fine("Importer Stats: ");
            logger.log(Level.FINE, "Tags: {0}", numClasses);
            logger.log(Level.FINE, "Objects: {0}", numLocs);
            logger.log(Level.FINE, "Data Size: {0}", dataArray.length);
        }
        dataArray = null;
        return rVal;
    }

    public Savable load(URL f) throws IOException {
        return load(f, null);
    }

    public Savable load(URL f, ReadListener listener) throws IOException {
        InputStream is = f.openStream();
        Savable rVal = load(is, listener);
        is.close();
        return rVal;
    }

    public Savable load(File f) throws IOException {
        return load(f, null);
    }

    public Savable load(File f, ReadListener listener) throws IOException {
        FileInputStream fis = new FileInputStream(f);
        try {
            return load(fis, listener);
        } finally {
            fis.close();
        }
    }

    public Savable load(byte[] data) throws IOException {
        ByteArrayInputStream bais = new ByteArrayInputStream(data);
        Savable rVal = load(bais);
        bais.close();
        return rVal;
    }

    @Override
    public InputCapsule getCapsule(Savable id) {
        return capsuleTable.get(id);
    }

    protected String readString(InputStream f, int length) throws IOException {
        byte[] data = new byte[length];
        for(int j = 0; j < length; j++) {
            data[j] = (byte)f.read();
        }

        return new String(data);
    }

    protected String readString(int length, int offset) throws IOException {
        byte[] data = new byte[length];
        for(int j = 0; j < length; j++) {
            data[j] = dataArray[j+offset];
        }

        return new String(data);
    }

    public Savable readObject(int id) {

        if(contentTable.get(id) != null) {
            return contentTable.get(id);
        }

        try {
            int loc = locationTable.get(id);

            String alias = readString(aliasWidth, loc);
            loc+=aliasWidth;

            BinaryClassObject bco = classes.get(alias);

            if(bco == null) {
                logger.logp(Level.SEVERE, this.getClass().toString(), "readObject(int id)", "NULL class object: " + alias);
                return null;
            }

            int dataLength = ByteUtils.convertIntFromBytes(dataArray, loc);
            loc+=4;

            Savable out = null;
            if (assetManager != null) {
                out = SavableClassUtil.fromName(bco.className, assetManager.getClassLoaders());
            } else {
                out = SavableClassUtil.fromName(bco.className);
            }

            BinaryInputCapsule cap = new BinaryInputCapsule(this, out, bco);
            cap.setContent(dataArray, loc, loc+dataLength);

            capsuleTable.put(out, cap);
            contentTable.put(id, out);

            out.read(this);

            capsuleTable.remove(out);

            return out;

        } catch (IOException e) {
            logger.logp(Level.SEVERE, this.getClass().toString(), "readObject(int id)", "Exception", e);
            return null;
        } catch (ClassNotFoundException e) {
            logger.logp(Level.SEVERE, this.getClass().toString(), "readObject(int id)", "Exception", e);
            return null;
        } catch (InstantiationException e) {
            logger.logp(Level.SEVERE, this.getClass().toString(), "readObject(int id)", "Exception", e);
            return null;
        } catch (IllegalAccessException e) {
            logger.logp(Level.SEVERE, this.getClass().toString(), "readObject(int id)", "Exception", e);
            return null;
        }
    }
}