/*************************************************************************
 * PCRaw
 * Author: nori090
 * Original author: ◆e5bW6vDOJ.
 * Copyright: Copyright (C) 2008 nori090
 * Copyright: Copyright (C) 2007 ◆e5bW6vDOJ.
 * Date: February 11, 2007
 * License: 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 * 
 * http://mosax.sakura.ne.jp/fswiki.cgi?page=PCRaw
 */
package org.coderepos.nori090.pcraw;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.channels.SocketChannel;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.zip.Inflater;
import java.util.zip.InflaterInputStream;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * @author nori090
 * @version $Rev: 123 $ $Date: 2008-07-26 13:36:18 +0900 (Sat, 26 Jul 2008) $
 */
public class PCRP
    extends StreamIO {
    private static final Log log = LogFactory.getLog( PCRP.class );

    static final byte STREAM_VERSION = 4;

    static final char[] PCRP_MARKER = new char[] { 0xff, 0xaa, 0x00, 0x55 };

    static final char[] PCRP_START = new char[] { 's', 's', 's', 's' };

    static final char[] PCRP_END = new char[] { 'e', 'e', 'e', 'e' };

    static final char[] PCRP_START_DATA = new char[] { 's', 'd', 'd', 's' };

    static final char[] PCRP_END_DATA = new char[] { 'e', 'd', 'd', 'e' };

    static final char[] PCRP_START_ATOM = new char[] { 's', 'a', 'a', 's' };

    static final char[] PCRP_END_ATOM = new char[] { 'e', 'a', 'a', 'e' };

    static final char[] PCRP_LIFETIME = new char[] { 'l', 'i', 'f', 't' };

    static final char[] PCRP_SENDTIME = new char[] { 's', 'n', 'd', 't' };

    static final char[] PCRP_DATA = new char[] { 'd', 'a', 't', 'a' };

    static final char[] PCRP_FLAG = new char[] { 'f', 'l', 'a', 'g' };

    static final char[] PCRP_KEYN = new char[] { 'k', 'e', 'y', 'n' };

    static final char[] PCRP_MD5 = new char[] { 'm', 'd', '5', '\0' };

    static final int MAX_DATA_SIZE = 1000000;

    static final int MAX_DATA_NUM = 16;

    static final int TIMEOUT = 5000;

    static final int FLAG_MD5 = 0x00000001;

    static final int FLAG_RC4 = 0x00000002;

    static final int FLAG_GZ = 0x00000004;

    static final int FLAG_ALL = FLAG_MD5 | FLAG_RC4 | FLAG_GZ;

    static class PCRPInfo {
        KeyManager keyMgr;

        int localPort;

        String localHost;

        int trackerPort;

        String trackerHost;

    }

    PCRPInfo info;

    SocketChannel sock;

    public PCRP( PCRPInfo info, SocketChannel sock ) {
        this.info = info;
        super.sock = sock;
    }

    char[] readId()
        throws IOException {
        return toChar( readExact( 4 ) );
    }

    byte[] readAtom()
        throws IOException {
        char[] id = readId();
        if ( !Arrays.equals( id, PCRP_START_ATOM ) ) {
            throw new IOException( "PCRP.readAtom() Invalid data start." );
        }
        int size = readInt();
        if ( size < 0 || size > MAX_DATA_SIZE ) {
            throw new IOException( "PCRP.readAtom() Invalid data size." );
        }
        byte[] bs = null;
        if ( size > 0 ) {
            bs = readExact( size );
        }
        id = readId();
        if ( !Arrays.equals( id, PCRP_END_ATOM ) ) {
            throw new IOException( "PCRP.readAtom() Invalid data end." );
        }
        return bs;
    }

    byte[] readData()
        throws IOException {
        char[] id = readId();
        if ( !Arrays.equals( id, PCRP_START_DATA ) ) {
            throw new IOException( "PCRP.readData() Invalid data start." );
        }
        byte[] data = null;
        int flg = 0;
        byte[] hash = null;
        for ( int i = 0; i < MAX_DATA_NUM; i++ ) {
            id = readId();
            if ( Arrays.equals( id, PCRP_END_DATA ) ) {
                break;
            }
            if ( Arrays.equals( id, PCRP_DATA ) ) {
                data = readAtom();
            }
            else if ( Arrays.equals( id, PCRP_FLAG ) ) {
                byte[] tmp = readAtom();
                if ( tmp.length == 4 ) {
                    flg = ( ( tmp[3] & 0xFF ) << 24 );
                    flg = flg | ( ( tmp[2] & 0xFF ) << 16 );
                    flg = flg | ( ( tmp[1] & 0xFF ) << 8 );
                    flg = flg | ( tmp[0] & 0xFF );
                }
            }
            else if ( Arrays.equals( id, PCRP_MD5 ) ) {
                byte[] tmp = readAtom();
                if ( tmp.length == 16 ) {
                    hash = tmp;
                }
            }
            else {
                log.debug( new StringBuffer( "AtomスキップＩＤ：" ).append( id ).toString() );
                readAtom();
            }
        }

        if ( !Arrays.equals( id, PCRP_END_DATA ) ) {
            throw new IOException( "PCRP.readData() Invalid data end." );
        }
        if ( ( flg & ~FLAG_ALL ) != 0 || ( ( flg & FLAG_RC4 ) != 0 && ( flg & FLAG_MD5 ) == 0 ) ) {
            throw new IOException( "PCRP.readData() Invalid data flag." );
        }
        if ( flg == 0 || data == null || data.length == 0 ) {
            return data == null ? new byte[0] : data;
        }
        else {
            boolean succeed = false;
            if ( ( flg & FLAG_RC4 ) != 0 ) {
                log.debug( "データは暗号化されています." );
                try {
                    if ( info.keyMgr.count() == 0 && PcpTools.pcpPortCheck( info, TIMEOUT ) == 0 ) {
                        throw new IOException( "Port 0" );
                    }
                }
                catch ( IOException e ) {
                    e.printStackTrace();
                    throw new IOException( "Portcheck error.", e );
                }
                int i = 0;
                MessageDigest md5 = getMessageDigest();
                Arcfour rc4 = new Arcfour();
                for ( KeyManager.Key k : info.keyMgr.keys ) {
                    log.debug( "データの復号化を試行中... Key No." + i );
                    rc4.init( k.key );
                    byte[] tmp = rc4.codec( data );
                    byte[] hash1 = md5.digest( tmp );
                    rc4.init( k.key );
                    byte[] hash2 = rc4.codec( hash );
                    if ( Arrays.equals( hash1, hash2 ) ) {
                        log.debug( "データ復号化成功." );
                        data = tmp;
                        succeed = true;
                        break;
                    }
                    i++;
                }
                if ( succeed ) {
                    if ( i > 0 ) {
                        info.keyMgr.swap( i, 0 );
                    }
                    if ( info.keyMgr.beforeKey >= 0 && i > 0 ) {
                        info.keyMgr.deleteOld();
                    }
                    info.keyMgr.beforeKey = i;
                }
                else {
                    if ( info.keyMgr.deleteOld() <= 0 ) {
                        throw new IOException( "PCRP.readData() Lost keys." );
                    }
                    throw new IOException( "PCRP.readData() decryption failed." );
                }
            }
            else if ( ( flg & FLAG_MD5 ) != 0 ) {
                byte[] hash1 = getMessageDigest().digest( data );
                if ( Arrays.equals( hash, hash1 ) ) {
                    succeed = true;
                }
            }
            else {
                succeed = true;
            }
            if ( !succeed ) {
                throw new IOException( "PCRP.readData() Invalid data." );
            }
            if ( ( flg & FLAG_GZ ) != 0 ) {
                log.debug( "データは圧縮されています." );
                data = uncompress( data );
                log.debug( "データ展開成功." );
            }
            log.debug( "データ確定." );
            return data;
        }
    }

    MessageDigest getMessageDigest() {
        try {
            return MessageDigest.getInstance( "MD5" );
        }
        catch ( NoSuchAlgorithmException e ) {
            throw new RuntimeException( e );
        }
    }

    void skipData()
        throws IOException {
        char[] id = readId();
        if ( !Arrays.equals( id, PCRP_START_DATA ) ) {
            throw new IOException( "PCRP.skipData() Invalid data start." );
        }
        for ( int i = 0; i < MAX_DATA_NUM; i++ ) {
            id = readId();
            if ( Arrays.equals( id, PCRP_END_DATA ) ) {
                break;
            }
            readAtom();
        }
        if ( !Arrays.equals( id, PCRP_END_DATA ) ) {
            throw new IOException( "PCRP.skipData() Invalid data end." );
        }
    }

    /**
     * zip byte arrays uncompress
     */
    byte[] uncompress( byte[] bs )
        throws IOException {
        Inflater inflater = new Inflater();
        InflaterInputStream in = null;
        ByteArrayOutputStream bos = null;
        try {
            in = new InflaterInputStream( new ByteArrayInputStream( bs ), inflater, bs.length );
            bos = new ByteArrayOutputStream( bs.length * 3 );
            byte[] buf = new byte[1024];
            int read = -1;
            while ( ( read = in.read( buf, 0, buf.length ) ) != -1 ) {
                bos.write( buf, 0, read );
            }
            inflater.reset();
            inflater.end();
            return bos.toByteArray();
        }
        finally {
            closeQuietly( in );
            closeQuietly( bos );
        }
    }

    static void closeQuietly( InputStream input ) {
        try {
            if ( input != null ) {
                input.close();
            }
        }
        catch ( IOException ioe ) {
            // ignore
        }
    }

    static void closeQuietly( OutputStream output ) {
        try {
            if ( output != null ) {
                output.close();
            }
        }
        catch ( IOException ioe ) {
            // ignore
        }
    }

}
