/*
 * Copyright 2007 nori090
 *
 * 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 jimmy;

import static jimmy.ConvertUtil.littleEndianBytes2int;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.Random;
import java.util.concurrent.ConcurrentLinkedQueue;

import jimmy.command.Command;
import jimmy.command.Command00;
import jimmy.command.Command97;

import org.apache.commons.lang.ArrayUtils;

/**
 * winny connection
 * 
 * @author nori090
 * @version $Rev: 17 $ $Date: 2008-03-25 22:45:24 +0900 (Tue, 25 Mar 2008) $
 */
public class Connection {

    static final long BUFFER_SIZE = 0x100000;

    static final long BLOCK_MAX = 0x90000;

    static final int SPEED_SEC = 30000;

    static final int RETRY_MAX = 3;

    static byte randam_byte() {
        return randam_bytes( 1 )[0];
    }

    static byte[] randam_bytes( int size ) {
        byte[] bs = new byte[size];
        Random random = new Random( System.currentTimeMillis() );
        random.nextBytes( bs );
        return bs;
    }

    InetAddress address;

    SocketChannel socket;

    ARC4Cipher recv_rc4key;

    ARC4Cipher send_rc4key;

    /* 通信管理関連 */
    long recv_size_sec; // 受信速度(B/S)

    long last_recv_time; // 最終受信時間

    long send_size_sec; // 送信速度(B/S)

    long last_send_time; // 最終送信時間

    long start_time;

    ConcurrentLinkedQueue<Command> queue = new ConcurrentLinkedQueue<Command>();

    public void send( byte[] packet )
        throws WinnyProtocolException {
        long cur;
        long wsize;
        long packet_size = packet.length;
        long block_count = packet_size / 0x2000;
        long remainder = packet_size % 0x2000;
        packet = send_rc4key.encrypt( packet );
        try {
            int pos = 0;
            for ( pos = 0; pos < block_count; ++pos ) {
                wsize = socket.write( ByteBuffer.wrap( packet, ( pos * 0x2000 ), 0x2000 ) );
                if ( wsize != 0x2000 ) {
                    throw new IOException( "socket send size error! expected:<" + 0x2000 + "> but was:<" + wsize + ">" );
                }
                cur = System.currentTimeMillis();
                if ( last_send_time + SPEED_SEC > cur ) {
                    send_size_sec += 0x2000;
                }
                else {
                    send_size_sec = 0x2000;
                    last_send_time = cur;
                }
            }
            if ( remainder != 0 ) {
                wsize = socket.write( ByteBuffer.wrap( packet, ( pos * 0x2000 ), (int) remainder ) );
                if ( wsize != remainder ) {
                    throw new IOException( "socket write size error! expected:<" + remainder + "> but was:<" + wsize +
                        ">" );
                }
                cur = System.currentTimeMillis();
                if ( last_send_time + SPEED_SEC > cur ) {
                    send_size_sec += remainder;
                }
                else {
                    send_size_sec = remainder;
                    last_send_time = cur;
                }
            }
        }
        catch ( IOException e ) {
            throw new WinnyProtocolException( e );
        }
    }

    int receive( byte[] bs, boolean decrypt )
        throws WinnyProtocolException {
        int rsize = 0;
        long cur;
        ByteBuffer buf = ByteBuffer.wrap( bs );
        try {
            int read = 0;
            while ( rsize != bs.length ) {
                read = socket.read( buf );
                rsize += read;
                if ( read < 0 ) {
                    throw new IOException( "socket read error!" );
                }
                cur = System.currentTimeMillis();
                if ( last_recv_time + SPEED_SEC > cur ) {
                    recv_size_sec += read;
                }
                else {
                    recv_size_sec = read;
                    last_recv_time = cur;
                }
            }
            if ( decrypt ) {
                byte[] unpack = recv_rc4key.decrypt( buf.array() );
                System.arraycopy( unpack, 0, bs, 0, bs.length );
            }
            return rsize;
        }
        catch ( IOException e ) {
            throw new WinnyProtocolException( e );
        }
    }

    public Command00 authorize()
        throws WinnyProtocolException {
        Command00 header = new Command00();
        Command97 low_ver = new Command97();
        byte[] authorize_block = randam_bytes( 6 );
        int rsize;
        try {
            byte[] key = ArrayUtils.subarray( authorize_block, 2, 6 );
            send_rc4key = new ARC4Cipher( key );
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            out.write( authorize_block );
            byte[] pack = send_rc4key.encrypt( ArrayUtils.addAll( header.pack(), low_ver.pack() ) );
            out.write( pack );
            socket.write( ByteBuffer.wrap( out.toByteArray() ) );
            send_rc4key = new ARC4Cipher( Command.toStep2Key( key ) );

            authorize_block = new byte[6];
            rsize = receive( authorize_block, false );
            if ( rsize != authorize_block.length ) {
                throw new IOException( "authorize error" );
            }
            key = ArrayUtils.subarray( authorize_block, 2, 6 );
            recv_rc4key = new ARC4Cipher( key );
            Command c = null;
            for ( int i = 0; i < 2; i++ ) {
                c = receiveOneCommand();
                if ( c instanceof Command00 ) {
                    break;
                }
                else {
                    c = null;
                }
            }
            if ( c == null ) {
                throw new IOException( "authorize protocol error" );
            }
            recv_rc4key = new ARC4Cipher( Command.toStep2Key( key ) );
            return (Command00) c;
        }
        catch ( IOException e ) {
            throw new WinnyProtocolException( e );
        }
    }

    public Command receiveOneCommand()
        throws WinnyProtocolException {
        if ( !queue.isEmpty() ) {
            return queue.poll();
        }
        byte[] head = new byte[5];
        int rsize = receive( head, true );
        if ( rsize != head.length ) {
            throw new WinnyProtocolException( "read packet error" );
        }
        int block_length = littleEndianBytes2int( ArrayUtils.subarray( head, 0, 4 ) );
        if ( block_length > BUFFER_SIZE ) {
            throw new WinnyProtocolException( "read packet error" );
        }
        byte[] data = new byte[block_length - 1];
        if ( block_length - 1 != 0 ) {
            rsize = receive( data, true );
            if ( rsize == 0 ) {
                throw new WinnyProtocolException( "read packet error" );
            }
        }
        return Command.getCommandInstance( ArrayUtils.addAll( head, data ) );
    }
    
    // TODO バッファリング受信は良く分からないので見送り
    void replenish() {
        // キューに入れる作業
    }

    long rate() {
        long cur = System.currentTimeMillis();
        long rate;

        long recv = cur - last_recv_time;
        long send = cur - last_send_time;

        if ( recv == 0 && send == 0 ) {
            return 0;
        }
        else if ( recv == 0 ) {
            rate = send_size_sec / send;
        }
        else if ( send == 0 ) {
            rate = recv_size_sec / recv;
        }
        else {
            rate = ( send_size_sec / send ) + ( recv_size_sec / recv );
        }
        // maybe int size;
        return rate;
    }

    Command00 connect( InetAddress address, int port )
        throws WinnyProtocolException {

        this.address = address;
        try {
            socket = SocketChannel.open( new InetSocketAddress( address, port ) );
            return authorize();
        }
        catch ( IOException e ) {
            throw new WinnyProtocolException( e );
        }
    }

    void close() {
        try {
            Socket s = socket.socket();
            s.shutdownOutput();
            s.shutdownInput();
            socket.finishConnect();
            socket.close();
        }
        catch ( IOException e ) {
            e.printStackTrace();
        }
    }
}
