/*
 * Copyright (C) 2016 Fritz Elfert
 *
 * This file is part of the JNR project.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package jnr.unixsocket;

import java.io.IOException;

import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.DatagramChannel;
import java.nio.channels.MembershipKey;
import java.nio.channels.UnsupportedAddressTypeException;

import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketAddress;
import java.net.SocketException;
import java.net.SocketOption;

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

import jnr.constants.platform.ProtocolFamily;
import jnr.constants.platform.Sock;
import jnr.enxio.channels.AbstractNativeDatagramChannel;

public class UnixDatagramChannel extends AbstractNativeDatagramChannel {
    static enum State {
        UNINITIALIZED,
        CONNECTED,
        IDLE,
    }
    private State state;
    private UnixSocketAddress remoteAddress = null;
    private UnixSocketAddress localAddress = null;
    private final ReadWriteLock stateLock = new ReentrantReadWriteLock();
    private final BindHandler bindHandler;

    public static final UnixDatagramChannel open() throws IOException {
        return new UnixDatagramChannel();
    }

    public static final UnixDatagramChannel[] pair() throws IOException {
        int[] sockets = { -1, -1 };
        Native.socketpair(ProtocolFamily.PF_UNIX, Sock.SOCK_DGRAM, 0, sockets);
        return new UnixDatagramChannel[] {
            new UnixDatagramChannel(sockets[0], State.CONNECTED, true),
                new UnixDatagramChannel(sockets[1], State.CONNECTED, true)
        };
    }

    private UnixDatagramChannel() throws IOException {
        this(Native.socket(ProtocolFamily.PF_UNIX, Sock.SOCK_DGRAM, 0));
    }

    UnixDatagramChannel(int fd) {
        this(fd, State.IDLE, false);
    }

    UnixDatagramChannel(int fd, State initialState, boolean initialBoundState) {
        super(fd);
        stateLock.writeLock().lock();
        state = initialState;
        bindHandler = new BindHandler(initialBoundState);
        stateLock.writeLock().unlock();
    }

    UnixDatagramChannel(int fd, UnixSocketAddress remote) throws IOException {
        this(fd);
        connect(remote);
    }

    @Override
    public UnixDatagramChannel bind(SocketAddress local) throws IOException {
        localAddress = bindHandler.bind(getFD(), local);
        return this;
    }

    public UnixDatagramChannel connect(UnixSocketAddress remote) {
        stateLock.writeLock().lock();
        remoteAddress = remote;
        state = State.CONNECTED;
        stateLock.writeLock().unlock();
        return this;
    }

    public UnixDatagramChannel disconnect() throws IOException {
        stateLock.writeLock().lock();
        remoteAddress = null;
        state = State.IDLE;
        stateLock.writeLock().unlock();
        return this;
    }

    boolean isBound() {
        return bindHandler.isBound();
    }

    public boolean isConnected() {
        stateLock.readLock().lock();
        boolean isConnected = state == State.CONNECTED;
        stateLock.readLock().unlock();
        return isConnected;
    }

    public final UnixSocketAddress getRemoteSocketAddress() {
        if (!isConnected()) {
            return null;
        }
        return remoteAddress != null ? remoteAddress : (remoteAddress = Common.getpeername(getFD()));
    }

    public final UnixSocketAddress getLocalSocketAddress() {
        return localAddress != null ? localAddress : (localAddress = Common.getsockname(getFD()));
    }

    @Override
    public UnixSocketAddress receive(ByteBuffer src) throws IOException {
        UnixSocketAddress remote = new UnixSocketAddress();
        int n = Native.recvfrom(getFD(), src, remote.getStruct());
        if (n < 0) {
            throw new IOException(Native.getLastErrorString());
        }
        return remote;
    }

    @Override
    public int send(ByteBuffer src, SocketAddress target) throws IOException {
        UnixSocketAddress remote = null;
        if (null == target) {
            if (isConnected()) {
                remote = remoteAddress;
            } else {
                throw new IllegalArgumentException("Destination address cannot be null on unconnected datagram sockets");
            }
        } else {
            if (!(target instanceof UnixSocketAddress)) {
                throw new UnsupportedAddressTypeException();
            }
            remote = (UnixSocketAddress)target;
        }
        SockAddrUnix sa = (null == remote) ? null : remote.getStruct();
        int addrlen = (null == sa) ? 0 : sa.length();
        int n = Native.sendto(getFD(), src, sa, addrlen);
        if (n < 0) {
            throw new IOException(Native.getLastErrorString());
        }

        return n;
    }

    @Override
    public DatagramChannel connect(SocketAddress remote) throws IOException {
        if (remote instanceof UnixSocketAddress) {
            return connect(((UnixSocketAddress) remote));
        } else {
            throw new UnsupportedAddressTypeException();
        }
    }

    @Override
    public UnixDatagramSocket socket() {
        try {
            return new UnixDatagramSocket(this);
        } catch (SocketException e) {
            throw new NullPointerException("Could not create UnixDatagramSocket");
        }
    }

    @Override
    public long write(ByteBuffer[] srcs, int offset, int length)
        throws IOException {

        if (state == State.CONNECTED) {
            return super.write(srcs, offset, length);
        } else if (state == State.IDLE) {
            return 0;
        } else {
            throw new ClosedChannelException();
        }
    }

    @Override
    public int read(ByteBuffer dst) throws IOException {
        if (state == State.CONNECTED) {
            return super.read(dst);
        } else if (state == State.IDLE) {
            return 0;
        } else {
            throw new ClosedChannelException();
        }
    }

    @Override
    public int write(ByteBuffer src) throws IOException {
        if (state == State.CONNECTED) {
            return super.write(src);
        } else if (state == State.IDLE) {
            return 0;
        } else {
            throw new ClosedChannelException();
        }
    }


    @Override
    public SocketAddress getRemoteAddress() throws IOException {
        return remoteAddress;
    }

    @Override
    public SocketAddress getLocalAddress() throws IOException {
        return localAddress;
    }

    private static class DefaultOptionsHolder {
        static final Set<SocketOption<?>> defaultOptions = defaultOptions();

        private static Set<SocketOption<?>> defaultOptions() {
            HashSet<SocketOption<?>> set = new HashSet<SocketOption<?>>(5);
            set.add(UnixSocketOptions.SO_SNDBUF);
            set.add(UnixSocketOptions.SO_SNDTIMEO);
            set.add(UnixSocketOptions.SO_RCVBUF);
            set.add(UnixSocketOptions.SO_RCVTIMEO);
            set.add(UnixSocketOptions.SO_PEERCRED);
            return Collections.unmodifiableSet(set);
        }
    }

    @Override
    public final Set<SocketOption<?>> supportedOptions() {
        return DefaultOptionsHolder.defaultOptions;
    }

    @Override
    public <T> T getOption(SocketOption<T> name) throws IOException {
        if (!supportedOptions().contains(name)) {
            throw new UnsupportedOperationException("'" + name + "' not supported");
        }
        return Common.getSocketOption(getFD(), name);
    }

    @Override
    public <T> DatagramChannel setOption(SocketOption<T> name, T value)
        throws IOException {
        if (name == null) {
            throw new IllegalArgumentException("name may not be null");
        }
        if (!supportedOptions().contains(name)) {
            throw new UnsupportedOperationException("'" + name + "' not supported");
        }
        Common.setSocketOption(getFD(), name, value);
        return this;
    }

    @Override
    public MembershipKey join(InetAddress group, NetworkInterface interf) {
        throw new UnsupportedOperationException("join is not supported");
    }

    @Override
    public MembershipKey join(InetAddress group, NetworkInterface interf, InetAddress source) {
        throw new UnsupportedOperationException("join is not supported");
    }

}
