/*
 * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package java.nio;

import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.util.Objects;
import jdk.internal.misc.InnocuousThread;
import sun.nio.Cleaner;

/**
 * BufferCleaner supports PhantomReference-based management of native memory
 * referred to by Direct-XXX-Buffers. Unreferenced DBBs may be garbage
 * collected, deactivating the associated PRefs and making them available for
 * cleanup here.
 *
 * There is a configured limit to the amount of memory that may be allocated
 * by DBBs. When that limit is reached, the allocator may invoke the garbage
 * collector directly to attempt to trigger cleaning here, hopefully
 * permitting the allocation to complete. Only if that doesn't free sufficient
 * memory does the allocation fail.  See java.nio.Bits::reserveMemory() for
 * details.
 *
 * One of the requirements for that approach is having a way to determine that
 * deactivated cleaners have been cleaned. java.lang.ref.Cleaner doesn't
 * provide such a mechanism, and adding such a mechanism to that class to
 * satisfy this unique requirement was deemed undesirable. Instead, this class
 * uses the underlying primitives (PhantomReferences, ReferenceQueues) to
 * provide the functionality needed for DBB management.
 */
class BufferCleaner {
    private static final class PhantomCleaner
        extends PhantomReference<Object>
        implements Cleaner
    {
        private final Runnable action;
        // Position in the CleanerList.
        CleanerList.Node node;
        int index;

        public PhantomCleaner(Object obj, Runnable action) {
            super(obj, queue);
            this.action = action;
        }

        @Override
        public void clean() {
            if (cleanerList.remove(this)) {
                // If being cleaned explicitly by application, rather than via
                // reference processing by BufferCleaner, clear the referent so
                // reference processing is disabled for this object.
                clear();
                try {
                    action.run();
                } catch (Throwable x) {
                    // Long-standing behavior: when cleaning fails, VM exits.
                    if (System.err != null) {
                        new Error("nio Cleaner terminated abnormally", x).printStackTrace();
                    }
                    System.exit(1);
                }
            }
        }
    }

    // Cribbed from jdk.internal.ref.CleanerImpl.
    static final class CleanerList {
        /**
         * Capacity for a single node in the list.
         * This balances memory overheads vs locality vs GC walking costs.
         */
        static final int NODE_CAPACITY = 4096;

        /**
         * Head node. This is the only node where PhantomCleanabls are
         * added to or removed from. This is the only node with variable size,
         * all other nodes linked from the head are always at full capacity.
         */
        private Node head;

        /**
         * Cached node instance to provide better behavior near NODE_CAPACITY
         * threshold: if list size flips around NODE_CAPACITY, it would reuse
         * the cached node instead of wasting and re-allocating a new node all
         * the time.
         */
        private Node cache;

        public CleanerList() {
            this.head = new Node();
        }

        /**
         * Insert this PhantomCleaner in the list.
         */
        public synchronized void insert(PhantomCleaner phc) {
            if (head.size == NODE_CAPACITY) {
                // Head node is full, insert new one.
                // If possible, pick a pre-allocated node from cache.
                Node newHead;
                if (cache != null) {
                    newHead = cache;
                    cache = null;
                } else {
                    newHead = new Node();
                }
                newHead.next = head;
                head = newHead;
            }
            assert head.size < NODE_CAPACITY;

            // Put the incoming object in head node and record indexes.
            final int lastIndex = head.size;
            phc.node = head;
            phc.index = lastIndex;
            head.arr[lastIndex] = phc;
            head.size++;
        }

        /**
         * Remove this PhantomCleaner from the list.
         *
         * @return true if Cleaner was removed or false if not because
         * it had already been removed before
         */
        public synchronized boolean remove(PhantomCleaner phc) {
            if (phc.node == null) {
                // Not in the list.
                return false;
            }
            assert phc.node.arr[phc.index] == phc;

            // Replace with another element from the head node, as long
            // as it is not the same element. This keeps all non-head
            // nodes at full capacity.
            final int lastIndex = head.size - 1;
            assert lastIndex >= 0;
            if (head != phc.node || (phc.index != lastIndex)) {
                PhantomCleaner mover = head.arr[lastIndex];
                mover.node = phc.node;
                mover.index = phc.index;
                phc.node.arr[phc.index] = mover;
            }

            // Now we can unlink the removed element.
            phc.node = null;

            // Remove the last element from the head node.
            head.arr[lastIndex] = null;
            head.size--;

            // If head node becomes empty after this, and there are
            // nodes that follow it, replace the head node with another
            // full one. If needed, stash the now free node in cache.
            if (head.size == 0 && head.next != null) {
                Node newHead = head.next;
                if (cache == null) {
                    cache = head;
                    cache.next = null;
                }
                head = newHead;
            }

            return true;
        }

        /**
         * Segment node.
         */
        static class Node {
            // Array of tracked cleaners, and the amount of elements in it.
            final PhantomCleaner[] arr = new PhantomCleaner[NODE_CAPACITY];
            int size;

            // Linked list structure.
            Node next;
        }
    }


    private static final class CleaningRunnable implements Runnable {
        public CleaningRunnable() {}

        @Override
        public void run() {
            while (true) {
                try {
                    Cleaner c = (Cleaner) queue.remove();
                    c.clean();
                } catch (InterruptedException e) {
                    // Ignore InterruptedException in cleaner thread.
                }
            }
        }
    }

    /**
     * Try to do some cleaning. Takes a cleaner from the queue and executes it.
     *
     * @return true if a cleaner was found and executed, false if there
     * weren't any cleaners in the queue.
     */
    public static boolean tryCleaning() {
        Cleaner c = (Cleaner) queue.poll();
        if (c == null) {
            return false;
        } else {
            c.clean();
            return true;
        }
    }

    private static final CleanerList cleanerList = new CleanerList();
    private static final ReferenceQueue<Object> queue = new ReferenceQueue<Object>();
    private static Thread cleaningThread;

    private static void startCleaningThreadIfNeeded() {
        synchronized (cleanerList) {
            if (cleaningThread != null) {
                return;
            }
            cleaningThread = InnocuousThread.newThread(new CleaningRunnable());
        }
        cleaningThread.setDaemon(true);
        cleaningThread.start();
    }

    private BufferCleaner() {}

    /**
     * Construct a new Cleaner for obj, with the associated action.
     *
     * @param obj object to track.
     * @param action cleanup action for obj.
     * @return associated cleaner.
     *
     */
    public static Cleaner register(Object obj, Runnable action) {
        Objects.requireNonNull(obj, "obj");
        Objects.requireNonNull(action, "action");
        startCleaningThreadIfNeeded();
        PhantomCleaner cleaner = new PhantomCleaner(obj, action);
        cleanerList.insert(cleaner);
        Reference.reachabilityFence(obj);
        return cleaner;
    }
}
