package org.nio4r;

import java.nio.channels.Channel;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SelectionKey;

import org.jruby.Ruby;
import org.jruby.RubyClass;
import org.jruby.RubyIO;
import org.jruby.RubyObject;
import org.jruby.anno.JRubyMethod;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.builtin.IRubyObject;

public class Monitor extends RubyObject {
    private static final long serialVersionUID = -3733782997115074794L;
    private transient SelectionKey key;
    private RubyIO io;
    private transient IRubyObject interests, selector, value, closed;

    public Monitor(final Ruby ruby, RubyClass rubyClass) {
        super(ruby, rubyClass);
    }

    @JRubyMethod
    public IRubyObject initialize(ThreadContext context, IRubyObject selectable, IRubyObject interests, IRubyObject selector) {
        this.io        = RubyIO.convertToIO(context, selectable);
        this.interests = interests;
        this.selector  = selector;

        this.value  = context.nil;
        this.closed = context.getRuntime().getFalse();

        return context.nil;
    }

    public void setSelectionKey(SelectionKey key) {
        this.key = key;
        key.attach(this);
    }

    @JRubyMethod
    public IRubyObject io(ThreadContext context) {
        return io;
    }

    @JRubyMethod
    public IRubyObject selector(ThreadContext context) {
        return selector;
    }

    @JRubyMethod(name = "interests")
    public IRubyObject getInterests(ThreadContext context) {
        return interests;
    }

    @JRubyMethod(name = "interests=")
    public IRubyObject setInterests(ThreadContext context, IRubyObject interests) {
        if(this.closed == context.getRuntime().getTrue()) {
            throw context.getRuntime().newEOFError("monitor is closed");
        }

        Ruby ruby = context.getRuntime();
        SelectableChannel channel = (SelectableChannel)io.getChannel();

        if(interests != context.nil) {
            key.interestOps(Nio4r.symbolToInterestOps(ruby, channel, interests));
        } else {
            key.interestOps(0);
        }
        
        this.interests = interests;

        return this.interests;
    }

    @JRubyMethod(name = "add_interest")
    public IRubyObject addInterest(ThreadContext context, IRubyObject interest) {
        if(this.closed == context.getRuntime().getTrue()) {
            throw context.getRuntime().newEOFError("monitor is closed");
        }

        Ruby ruby = context.getRuntime();
        SelectableChannel channel = (SelectableChannel)io.getChannel();
        int newInterestOps = key.interestOps() | Nio4r.symbolToInterestOps(ruby, channel, interest);

        key.interestOps(newInterestOps);
        this.interests = Nio4r.interestOpsToSymbol(ruby, newInterestOps);

        return this.interests;
    }

    @JRubyMethod(name = "remove_interest")
    public IRubyObject removeInterest(ThreadContext context, IRubyObject interest) {
        if(this.closed == context.getRuntime().getTrue()) {
            throw context.getRuntime().newEOFError("monitor is closed");
        }

        Ruby ruby = context.getRuntime();
        SelectableChannel channel = (SelectableChannel)io.getChannel();
        int newInterestOps = key.interestOps() & ~Nio4r.symbolToInterestOps(ruby, channel, interest);

        key.interestOps(newInterestOps);
        this.interests = Nio4r.interestOpsToSymbol(ruby, newInterestOps);

        return this.interests;
    }

    @JRubyMethod
    public IRubyObject readiness(ThreadContext context) {
        if(!key.isValid())
          return this.interests; 
        return Nio4r.interestOpsToSymbol(context.getRuntime(), key.readyOps());
    }

    @JRubyMethod(name = "readable?")
    public IRubyObject isReadable(ThreadContext context) {
        Ruby runtime  = context.getRuntime();
        if (!this.key.isValid())
          return runtime.getTrue();
        int  readyOps = this.key.readyOps();

        if((readyOps & SelectionKey.OP_READ) != 0 || (readyOps & SelectionKey.OP_ACCEPT) != 0) {
            return runtime.getTrue();
        } else {
            return runtime.getFalse();
        }
    }

    @JRubyMethod(name = {"writable?", "writeable?"})
    public IRubyObject writable(ThreadContext context) {
        Ruby runtime  = context.getRuntime();
        if (!this.key.isValid())
          return runtime.getTrue();
        int  readyOps = this.key.readyOps();

        if((readyOps & SelectionKey.OP_WRITE) != 0 || (readyOps & SelectionKey.OP_CONNECT) != 0) {
            return runtime.getTrue();
        } else {
            return runtime.getFalse();
        }
    }

    @JRubyMethod(name = "value")
    public IRubyObject getValue(ThreadContext context) {
        return this.value;
    }

    @JRubyMethod(name = "value=")
    public IRubyObject setValue(ThreadContext context, IRubyObject obj) {
        this.value = obj;
        return context.nil;
    }

    @JRubyMethod
    public IRubyObject close(ThreadContext context) {
        return close(context, context.getRuntime().getTrue());
    }

    @JRubyMethod
    public IRubyObject close(ThreadContext context, IRubyObject deregister) {
        Ruby runtime = context.getRuntime();
        this.closed = runtime.getTrue();

        if(deregister == runtime.getTrue()) {
            selector.callMethod(context, "deregister", io);
        }

        return context.nil;
    }

    @JRubyMethod(name = "closed?")
    public IRubyObject isClosed(ThreadContext context) {
        return this.closed;
    }
}
