
package org.jruby.ext.ffi;

import java.lang.ref.Reference;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.jruby.Ruby;
import org.jruby.RubyBoolean;
import org.jruby.RubyClass;
import org.jruby.RubyModule;
import org.jruby.anno.JRubyClass;
import org.jruby.anno.JRubyMethod;
import org.jruby.internal.runtime.methods.DynamicMethod;
import org.jruby.runtime.Block;
import org.jruby.runtime.ObjectAllocator;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.builtin.IRubyObject;
import org.jruby.runtime.callsite.CachingCallSite;
import org.jruby.runtime.callsite.FunctionalCachingCallSite;
import org.jruby.util.PhantomReferenceReaper;
import org.jruby.util.cli.Options;

import static org.jruby.runtime.Visibility.*;

@JRubyClass(name = "FFI::" + AutoPointer.AUTOPTR_CLASS_NAME, parent = "FFI::Pointer")
public class AutoPointer extends Pointer {
    static final String AUTOPTR_CLASS_NAME = "AutoPointer";
    
    /** Keep strong references to the Reaper until cleanup */
    private static final ConcurrentMap<ReaperGroup, Boolean> referenceSet = new ConcurrentHashMap<ReaperGroup, Boolean>();
    private static final ThreadLocal<Reference<ReaperGroup>> currentReaper = new ThreadLocal<Reference<ReaperGroup>>();
    
    private Pointer pointer;
    private Object referent;
    private transient volatile Reaper reaper;
    
    public static RubyClass createAutoPointerClass(Ruby runtime, RubyModule module) {
        RubyClass autoptrClass = module.defineClassUnder(AUTOPTR_CLASS_NAME,
                module.getClass("Pointer"),
                Options.REIFY_FFI.load() ? new ReifyingAllocator(AutoPointer.class) : AutoPointerAllocator.INSTANCE);
        autoptrClass.defineAnnotatedMethods(AutoPointer.class);
        autoptrClass.defineAnnotatedConstants(AutoPointer.class);
        autoptrClass.setReifiedClass(AutoPointer.class);
        autoptrClass.kindOf = new RubyModule.KindOf() {
            @Override
            public boolean isKindOf(IRubyObject obj, RubyModule type) {
                return obj instanceof AutoPointer && super.isKindOf(obj, type);
            }
        };

        return autoptrClass;
    }

    private static final class AutoPointerAllocator implements ObjectAllocator {
        static final ObjectAllocator INSTANCE = new AutoPointerAllocator();

        public IRubyObject allocate(Ruby runtime, RubyClass klazz) {
            return new AutoPointer(runtime, klazz);
        }

    }

    public AutoPointer(Ruby runtime, RubyClass klazz) {
        super(runtime, klazz, runtime.getFFI().getNullMemoryIO());
    }
    
    private static final void checkPointer(Ruby runtime, IRubyObject ptr) {
        if (!(ptr instanceof Pointer)) {
            throw runtime.newTypeError(ptr, runtime.getFFI().pointerClass);
        }
        if (ptr instanceof MemoryPointer || ptr instanceof AutoPointer) {
            throw runtime.newTypeError("Cannot use AutoPointer with MemoryPointer or AutoPointer instances");
        }
    }

    @JRubyMethod(name="from_native", meta = true)
    public static IRubyObject from_native(ThreadContext context, IRubyObject recv, IRubyObject value, IRubyObject ctx) {
        return ((RubyClass) recv).newInstance(context, value, Block.NULL_BLOCK);
    }

    @Override
    @JRubyMethod(name = "initialize", visibility = PRIVATE)
    public final IRubyObject initialize(ThreadContext context, IRubyObject pointerArg) {

        Ruby runtime = context.runtime;

        checkPointer(runtime, pointerArg);

        Object ffiHandle = getMetaClass().getFFIHandle();
        if (!(ffiHandle instanceof ClassData)) {
            getMetaClass().setFFIHandle(ffiHandle = new ClassData());
        }
        ClassData classData = (ClassData) ffiHandle;

        // If no release method is defined, then memory leaks will result.
        DynamicMethod releaseMethod = classData.releaseCallSite.retrieveCache((IRubyObject) getMetaClass()).method;
        if (releaseMethod.isUndefined()) {
            throw runtime.newRuntimeError("release method undefined");

        } else if ((releaseMethod.getArity().isFixed() && releaseMethod.getArity().required() != 1) || releaseMethod.getArity().required() > 1) {
            throw runtime.newRuntimeError("wrong number of arguments to release method ("
                    + 1 + " for " + releaseMethod.getArity().required()  + ")");
        }


        setMemoryIO(((Pointer) pointerArg).getMemoryIO());
        this.pointer = (Pointer) pointerArg;
        this.size = pointer.size;
        this.typeSize = pointer.typeSize;
        setReaper(new Reaper(pointer, getMetaClass(), classData.releaseCallSite));

        return this;
    }

    @Override
    @JRubyMethod(name = "initialize", visibility = PRIVATE)
    public final IRubyObject initialize(ThreadContext context, IRubyObject pointerArg, IRubyObject releaser) {

        checkPointer(context.runtime, pointerArg);

        setMemoryIO(((Pointer) pointerArg).getMemoryIO());
        this.pointer = (Pointer) pointerArg;
        this.size = pointer.size;
        this.typeSize = pointer.typeSize;

        Object ffiHandle = releaser.getMetaClass().getFFIHandleAccessorField().getVariableAccessorForRead().get(releaser);
        if (!(ffiHandle instanceof ReleaserData)) {
            getMetaClass().setFFIHandle(ffiHandle = new ReleaserData());
        }

        ReleaserData releaserData = (ReleaserData) ffiHandle;
        DynamicMethod releaseMethod = releaserData.releaseCallSite.retrieveCache(releaser).method;
        // If no release method is defined, then memory leaks will result.
        if (releaseMethod.isUndefined()) {
            throw context.runtime.newRuntimeError("call method undefined");

        } else if ((releaseMethod.getArity().isFixed() && releaseMethod.getArity().required() != 1) || releaseMethod.getArity().required() > 1) {
            throw context.runtime.newRuntimeError("wrong number of arguments to call method ("
                    + 1 + " for " + releaseMethod.getArity().required()  + ")");
        }

        setReaper(new Reaper(pointer, releaser, releaserData.releaseCallSite));

        return this;
    }

    @JRubyMethod(name = "free")
    public final IRubyObject free(ThreadContext context) {
        Reaper r = reaper;

        if (r == null || r.released) {
            throw context.runtime.newRuntimeError("pointer already freed");
        }

        r.release(context);
        reaper = null;
        referent = null;

        return context.nil;
    }

    @JRubyMethod(name = "autorelease=")
    public final IRubyObject autorelease(ThreadContext context, IRubyObject autorelease) {
        Reaper r = reaper;

        if (r == null || r.released) {
            throw context.runtime.newRuntimeError("pointer already freed");
        }

        r.autorelease(autorelease.isTrue());

        return context.nil;
    }

    @JRubyMethod(name = "autorelease?")
    public final IRubyObject autorelease_p(ThreadContext context) {
        return RubyBoolean.newBoolean(context, reaper != null ? !reaper.unmanaged : false);
    }

    private void setReaper(Reaper reaper) {
        Reference<ReaperGroup> reaperGroupReference = currentReaper.get();
        ReaperGroup reaperGroup = reaperGroupReference != null ? reaperGroupReference.get() : null;
        Object referent = reaperGroup != null ? reaperGroup.referent() : null;
        if (referent == null || !reaperGroup.canAccept()) {
            reaperGroup = new ReaperGroup(referent = new Object());
            currentReaper.set(new SoftReference<ReaperGroup>(reaperGroup));
            referenceSet.put(reaperGroup, Boolean.TRUE);
        }
        this.referent = referent;
        this.reaper = reaper;
        reaperGroup.add(reaper);
    }

    private static final class ReaperGroup extends PhantomReferenceReaper<Object> implements Runnable {
        private static int MAX_REAPERS_PER_GROUP = 100;
        private final WeakReference<Object> weakref;
        private int reaperCount;
        private volatile Reaper head;
        
        ReaperGroup(Object referent) {
            super(referent);
            this.weakref = new WeakReference<Object>(referent);
        }
        
        Object referent() {
            return weakref.get();
        }
        
        boolean canAccept() {
            return reaperCount < MAX_REAPERS_PER_GROUP;
        }
        
        void add(Reaper r) {
            ++reaperCount;
            r.next = head;
            head = r;
        }
        
        public void run() {
            referenceSet.remove(this);
            Ruby runtime = null;
            ThreadContext ctx = null;
            Reaper r = head;
            
            while (r != null) {
                if (!r.released && !r.unmanaged) {
                    if (r.getRuntime() != runtime) {
                        runtime = r.getRuntime();
                        ctx = runtime.getCurrentContext();
                    }
                    r.dispose(ctx);
                }
                r = r.next;
            }
        } 
    }

    private static final class Reaper {
        final Pointer pointer;
        final IRubyObject proc;
        final CachingCallSite callSite;
        volatile Reaper next;
        volatile boolean released;
        volatile boolean unmanaged;


        private Reaper(Pointer ptr, IRubyObject proc, CachingCallSite callSite) {
            this.pointer = ptr;
            this.proc = proc;
            this.callSite = callSite;
        }
        
        final Ruby getRuntime() {
            return proc.getRuntime();
        }
        
        void dispose(ThreadContext context) {
            callSite.call(context, proc, proc, pointer);
        }

        public final void release(ThreadContext context) {
            if (!released) {
                released = true;
                dispose(context);
            }
        }

        public final void autorelease(boolean autorelease) {
            this.unmanaged = !autorelease;
        }
    }

    private static final class ClassData {
        private final CachingCallSite releaseCallSite = new FunctionalCachingCallSite("release");
    }

    private static final class ReleaserData {
        private final CachingCallSite releaseCallSite = new FunctionalCachingCallSite("call");
    }
}
