
package org.jruby.ext.ffi;

import java.nio.ByteOrder;

import org.jruby.*;
import org.jruby.anno.JRubyClass;
import org.jruby.anno.JRubyMethod;
import org.jruby.internal.runtime.methods.DynamicMethod;
import org.jruby.runtime.*;
import org.jruby.runtime.builtin.IRubyObject;

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

/**
 * C memory pointer operations.
 * <p>
 * This is an abstract class that defines Pointer operations
 * </p>
 */
@JRubyClass(name="FFI::Pointer", parent=AbstractMemory.ABSTRACT_MEMORY_RUBY_CLASS)
public class Pointer extends AbstractMemory {
    public static RubyClass createPointerClass(Ruby runtime, RubyModule module) {
        RubyClass pointerClass = module.defineClassUnder("Pointer",
                module.getClass(AbstractMemory.ABSTRACT_MEMORY_RUBY_CLASS),
                RubyInstanceConfig.REIFY_RUBY_CLASSES ? new ReifyingAllocator(Pointer.class) : PointerAllocator.INSTANCE);

        pointerClass.defineAnnotatedMethods(Pointer.class);
        pointerClass.defineAnnotatedConstants(Pointer.class);
        pointerClass.setReifiedClass(Pointer.class);
        pointerClass.kindOf = new RubyModule.KindOf() {
            @Override
            public boolean isKindOf(IRubyObject obj, RubyModule type) {
                return obj instanceof Pointer && super.isKindOf(obj, type); 
            }
        };

        module.defineClassUnder("NullPointerError", runtime.getRuntimeError(),
                runtime.getRuntimeError().getAllocator());

        // Add Pointer::NULL as a constant
        Pointer nullPointer = new Pointer(runtime, pointerClass, new NullMemoryIO(runtime));
        pointerClass.setConstant("NULL", nullPointer);
        
        runtime.getNilClass().addMethod("to_ptr", new NilToPointerMethod(runtime.getNilClass(), nullPointer));

        return pointerClass;
    }

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

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

    public static final Pointer getNull(Ruby runtime) {
        return runtime.getFFI().nullPointer;
    }

    public Pointer(Ruby runtime, RubyClass klazz) {
        super(runtime, klazz, runtime.getFFI().getNullMemoryIO(), 0);
    }

    public Pointer(Ruby runtime, MemoryIO io) {
        this(runtime, getPointerClass(runtime), io);
    }
    public Pointer(Ruby runtime, MemoryIO io, long size, int typeSize) {
        this(runtime, getPointerClass(runtime), io, size, typeSize);
    }
    protected Pointer(Ruby runtime, RubyClass klass, MemoryIO io) {
        super(runtime, klass, io, Long.MAX_VALUE);
    }
    protected Pointer(Ruby runtime, RubyClass klass, MemoryIO io, long size) {
        super(runtime, klass, io, size);
    }
    protected Pointer(Ruby runtime, RubyClass klass, MemoryIO io, long size, int typeSize) {
        super(runtime, klass, io, size, typeSize);
    }

    public static final RubyClass getPointerClass(Ruby runtime) {
        return runtime.getFFI().pointerClass;
    }

    public final AbstractMemory order(Ruby runtime, ByteOrder order) {
        return new Pointer(runtime,
                order.equals(getMemoryIO().order()) ? getMemoryIO() : new SwappedMemoryIO(runtime, getMemoryIO()),
                size, typeSize);
    }
    
    @JRubyMethod(name = "size", meta = true, visibility = PUBLIC)
    public static IRubyObject size(ThreadContext context, IRubyObject recv) {
        return RubyFixnum.newFixnum(context.getRuntime(), Factory.getInstance().sizeOf(NativeType.POINTER));
    }

    @JRubyMethod(name = { "initialize" }, visibility = PRIVATE)
    public IRubyObject initialize(ThreadContext context, IRubyObject address) {
        setMemoryIO(address instanceof Pointer
                ? ((Pointer) address).getMemoryIO()
                : Factory.getInstance().wrapDirectMemory(context.runtime, RubyFixnum.num2long(address)));
        size = Long.MAX_VALUE;
        typeSize = 1;

        return this;
    }

    @JRubyMethod(name = { "initialize" }, visibility = PRIVATE)
    public IRubyObject initialize(ThreadContext context, IRubyObject type, IRubyObject address) {
        setMemoryIO(address instanceof Pointer
                ? ((Pointer) address).getMemoryIO()
                : Factory.getInstance().wrapDirectMemory(context.runtime, RubyFixnum.num2long(address)));
        size = Long.MAX_VALUE;
        typeSize = calculateTypeSize(context, type);

        return this;
    }
    
    /**
     * 
     */
    @JRubyMethod(required = 1, visibility=PRIVATE)
    public IRubyObject initialize_copy(ThreadContext context, IRubyObject other) {
        if (this == other) {
            return this;
        }
        Pointer orig = (Pointer) other;
        this.typeSize = orig.typeSize;
        this.size = orig.size;

        setMemoryIO(orig.getMemoryIO().dup());

        return this;
    }


    /**
     * Tests if this <tt>Pointer</tt> represents the C <tt>NULL</tt> value.
     *
     * @return true if the address is NULL.
     */
    @JRubyMethod(name = "null?")
    public IRubyObject null_p(ThreadContext context) {
        return context.runtime.newBoolean(getMemoryIO().isNull());
    }


    @Override
    @JRubyMethod(name = { "to_s", "inspect" }, optional = 1)
    public IRubyObject to_s(ThreadContext context, IRubyObject[] args) {
        String s = size != Long.MAX_VALUE
                ? String.format("#<%s address=0x%x size=%s>", getMetaClass().getName(), getAddress(), size)
                : String.format("#<%s address=0x%x>", getMetaClass().getName(), getAddress());

        return RubyString.newString(context.runtime, s);
    }

    @JRubyMethod(name = { "address", "to_i" })
    public IRubyObject address(ThreadContext context) {
        return context.runtime.newFixnum(getAddress());
    }

    /**
     * Gets the native memory address of this pointer.
     *
     * @return A long containing the native memory address.
     */
    public final long getAddress() {
        return getMemoryIO().address();
    }

    @JRubyMethod(name = "==", required = 1)
    public IRubyObject op_equal(ThreadContext context, IRubyObject obj) {
        return context.runtime.newBoolean(this == obj
                || getAddress() == 0L && obj.isNil()
                || (obj instanceof Pointer && ((Pointer) obj).getAddress() == getAddress()));
    }
    
    @Override
    protected AbstractMemory slice(Ruby runtime, long offset) {
        return new Pointer(runtime, getPointerClass(runtime),
                getMemoryIO().slice(offset),
                size == Long.MAX_VALUE ? Long.MAX_VALUE : size - offset, typeSize);
    }

    @Override
    protected AbstractMemory slice(Ruby runtime, long offset, long size) {
        return new Pointer(runtime, getPointerClass(runtime),
                getMemoryIO().slice(offset, size), size, typeSize);
    }

    protected Pointer getPointer(Ruby runtime, long offset) {
        return new Pointer(runtime, getPointerClass(runtime), getMemoryIO().getMemoryIO(offset), Long.MAX_VALUE);
    }

    private static final class NilToPointerMethod extends DynamicMethod {
        private static final Arity ARITY = Arity.NO_ARGUMENTS;
        private final Pointer nullPointer;

        private NilToPointerMethod(RubyModule implementationClass, Pointer nullPointer) {
            super(implementationClass, Visibility.PUBLIC);
            this.nullPointer = nullPointer;
        }

        @Override
        public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule clazz, String name, IRubyObject[] args, Block block) {
            ARITY.checkArity(context.runtime, args);
            return nullPointer;
        }

        @Override
        public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule klazz, String name) {
            return nullPointer;
        }

        @Override
        public DynamicMethod dup() {
            return this;
        }
    }
}
