/*
 * Copyright (c) 2009-2021 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.util;

import com.jme3.renderer.Renderer;
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * GLObjectManager tracks all GLObjects used by the Renderer. Using a
 * <code>ReferenceQueue</code> the <code>GLObjectManager</code> can delete
 * unused objects from GPU when their counterparts on the CPU are no longer used.
 *
 * On restart, the renderer may request the objects to be reset, thus allowing
 * the GLObjects to re-initialize with the new display context.
 */
public class NativeObjectManager {

    private static final Logger logger = Logger.getLogger(NativeObjectManager.class.getName());
    
    /**
     * Set to <code>true</code> to enable deletion of native buffers together with GL objects
     * when requested. Note that usage of object after deletion could cause undefined results
     * or native crashes, therefore by default this is set to <code>false</code>.
     */
    public static boolean UNSAFE = false;
    
    /**
     * The maximum number of objects that should be removed per frame.
     * If the limit is reached, no more objects will be removed for that frame.
     */
    private static final int MAX_REMOVES_PER_FRAME = 100;
    
    /**
     * Reference queue for {@link NativeObjectRef native object references}.
     */
    private ReferenceQueue<Object> refQueue = new ReferenceQueue<>();

    /**
     * List of currently active GLObjects.
     */
    final private HashMap<Long, NativeObjectRef> refMap = new HashMap<>();
    
    /**
     * List of real objects requested by user for deletion.
     */
    final private ArrayDeque<NativeObject> userDeletionQueue = new ArrayDeque<>();

    private static class NativeObjectRef extends PhantomReference<Object> {
        
        final private NativeObject objClone;
        final private WeakReference<NativeObject> realObj;

        public NativeObjectRef(ReferenceQueue<Object> refQueue, NativeObject obj){
            super(obj.handleRef, refQueue);
            assert obj.handleRef != null;

            this.realObj = new WeakReference<NativeObject>(obj);
            this.objClone = obj.createDestructableClone();
            assert objClone.getId() == obj.getId();
        }
    }

    /**
     * (Internal use only) Register a <code>NativeObject</code> with the manager.
     *
     * @param obj the object to register (not null)
     */
    public void registerObject(NativeObject obj) {
        if (obj.getId() <= 0) {
            throw new IllegalArgumentException("object id must be greater than zero");
        }

        NativeObjectRef ref = new NativeObjectRef(refQueue, obj);
        refMap.put(obj.getUniqueId(), ref);
        
        obj.setNativeObjectManager(this);

        if (logger.isLoggable(Level.FINEST)) {
            logger.log(Level.FINEST, "Registered: {0}", new String[]{obj.toString()});
        }
    }
    
    private void deleteNativeObject(Object rendererObject, NativeObject obj, NativeObjectRef ref, 
                                    boolean deleteGL, boolean deleteBufs) {
        assert rendererObject != null;
        
        // "obj" is considered the real object (with buffers and everything else)
        // if "ref" is null.
        NativeObject realObj = ref != null ? 
                                    ref.realObj.get() : 
                                    obj;
        
        assert realObj == null || obj.getId() == realObj.getId();
        
        if (deleteGL) {
            if (obj.getId() <= 0) {
                logger.log(Level.WARNING, "Object already deleted: {0}", obj.getClass().getSimpleName() + "/" + obj.getId());
            } else {
                // Unregister it from cleanup list.
                NativeObjectRef ref2 = refMap.remove(obj.getUniqueId());
                if (ref2 == null) {
                    throw new IllegalArgumentException("The " + obj + " NativeObject is not "
                            + "registered in this NativeObjectManager");
                }

                assert ref == null || ref == ref2;
                ref2.clear();

                int id = obj.getId();

                // Delete object from the GL driver
                obj.deleteObject(rendererObject);
                assert obj.getId() == NativeObject.INVALID_ID;

                if (logger.isLoggable(Level.FINEST)) {
                    logger.log(Level.FINEST, "Deleted: {0}", obj.getClass().getSimpleName() + "/" + id);
                }

                if (realObj != null){
                    // Note: make sure to reset them as well
                    // They may get used in a new renderer in the future
                    realObj.resetObject();
                }
            }
        }
        if (deleteBufs && UNSAFE && realObj != null) {
            // Only the real object has native buffers. 
            // The destructible clone has nothing and cannot be used in this case.
            realObj.deleteNativeBuffersInternal();
        }
    }
    
    /**
     * (Internal use only) Deletes unused NativeObjects.
     * Will delete at most {@link #MAX_REMOVES_PER_FRAME} objects.
     * 
     * @param rendererObject The renderer object. 
     * For graphics objects, {@link Renderer} is used, for audio, {#link AudioRenderer} is used.
     */
    public void deleteUnused(Object rendererObject){
        int removed = 0;
        while (removed < MAX_REMOVES_PER_FRAME && !userDeletionQueue.isEmpty()) {
            // Remove user requested objects.
            NativeObject obj = userDeletionQueue.pop();
            deleteNativeObject(rendererObject, obj, null, true, true);
            removed++;
        }
        while (removed < MAX_REMOVES_PER_FRAME) {
            // Remove objects reclaimed by GC.
            NativeObjectRef ref = (NativeObjectRef) refQueue.poll();
            if (ref == null) {
                break;
            }

            deleteNativeObject(rendererObject, ref.objClone, ref, true, false);
            removed++;
        }
        if (removed >= 1) {
            logger.log(Level.FINE, "NativeObjectManager: {0} native objects were removed from native", removed);
        }
    }

    /**
     * (Internal use only) Deletes all objects. 
     * Must only be called when display is destroyed.
     *
     * @param rendererObject the renderer object
     */
    public void deleteAllObjects(Object rendererObject){
        deleteUnused(rendererObject);
        ArrayList<NativeObjectRef> refMapCopy = new ArrayList<>(refMap.values());
        for (NativeObjectRef ref : refMapCopy) {
            deleteNativeObject(rendererObject, ref.objClone, ref, true, false);
        }
        assert refMap.size() == 0;
    }

    /**
     * Marks the given <code>NativeObject</code> as unused, 
     * to be deleted on the next frame. 
     * Usage of this object after deletion will cause an exception. 
     * Note that native buffers are only reclaimed if 
     * {@link #UNSAFE} is set to <code>true</code>.
     * 
     * @param obj The object to mark as unused.
     */
    void enqueueUnusedObject(NativeObject obj) {
        userDeletionQueue.push(obj);
    }
    
    /**
     * (Internal use only) Resets all {@link NativeObject}s.
     * This is typically called when the context is restarted.
     */
    public void resetObjects(){
        for (NativeObjectRef ref : refMap.values()) {
            // Must use the real object here, for this to be effective.
            NativeObject realObj = ref.realObj.get();
            if (realObj == null) {
                continue;
            }
            
            realObj.resetObject();
            logger.log(Level.FINEST, "Reset: {0}", realObj);
        }
        refMap.clear();
        refQueue = new ReferenceQueue<Object>();
    }

//    public void printObjects(){
//        System.out.println(" ------------------- ");
//        System.out.println(" GL Object count: "+ objectList.size());
//        for (GLObject obj : objectList){
//            System.out.println(obj);
//        }
//    }
}
