/*
 * IRCNetwork.java
 * IRCCore
 *
 * Created by tarchan on Aug 07, 2006.
 * Copyright (c) 2006 tarchan. All rights reserved.
 */

package com.mac.tarchan.irc;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Map;
import java.util.Vector;
import java.util.concurrent.ConcurrentHashMap;

import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.EventListenerList;

import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.log4j.Logger;

import com.mac.tarchan.misc.SimpleConcurrentHashSet;

/**
 * IRCNetwork
 * 
 * @author tarchan
 * @author nori090
 */
public class IRCNetwork
    implements Runnable {
    /** debug logger */
    private static Logger log = Logger.getLogger( IRCNetwork.class );

    /** IRCネットワークの名前 */
    private String name;

    /** IRCネットワークの地域 */
    private String locale = "jp";

    /** 名前にひもづくIRCネットワークのマップ */
    private static Map<String, IRCNetwork> netMap = new ConcurrentHashMap<String, IRCNetwork>();

    /** IRCネットワークの名前空間 */
    private IRCNamespace namespace = new IRCNamespace();

    /** チャンネルリスト */
    private SimpleConcurrentHashSet<IRCChannel> channels = new SimpleConcurrentHashSet<IRCChannel>();

    /** IRCネットワークの接続 */
    private IRCConnection connection;

    /** 入力ストリーム */
    private BufferedReader in;

    /** 出力ストリーム */
    private PrintWriter out;

    /** 現在のニックネーム */
    private String nick = "";

    /** ログインユーザー */
    private IRCUser loginUser;

    /**
     * IRCNetwork を作成します。
     * 
     * @param name IRCネットワークの名前
     */
    protected IRCNetwork( String name ) {
        this.name = name;
    }

    // 名前空間

    /**
     * IRCネットワークの名前を返します。
     * 
     * @return IRCネットワークの名前
     */
    public String getName() {
        return name;
    }

    /**
     * 指定した名前のIRCネットワークを返します。 未定義の場合は新しく作成します。
     * 
     * @param name IRCネットワークの名前
     * @return IRCネットワーク
     */
    public static IRCNetwork getNetwork( String name ) {
        IRCNetwork net = netMap.get( name );

        // 未定義の場合は新しく作る
        if ( net == null ) {
            net = new IRCNetwork( name );
            netMap.put( name, net );
        }

        return net;
    }

    /** クライアントリスト */
    private Vector<IRCClient> clientList = new Vector<IRCClient>();

    /**
     * IRCクライアントを設定します。
     * 
     * @param client IRCクライアント
     */
    private IRCNetwork addClient( IRCClient client ) {
        clientList.add( client );
        return this;
    }

    public IRCNetwork addClient( Object target ) {
        addClient( new IRCClient( target ) );
        return this;
    }

    public void clearClient() {
        clientList.clear();
    }

    /**
     * IRCネットワークの地域を設定します。 デフォルトは"jp"です。
     * 
     * @param locale IRCネットワークの地域
     */
    public IRCNetwork setLocale( String locale ) {
        this.locale = locale;
        return this;
    }

    public IRCChannel getChannel( String name ) {
        if ( !IRCNamespace.isChannel( name ) )
            return null;

        name = IRCNamespace.normalizeChannel( name, locale );
        IRCChannel ch = namespace.channelForName( name );
        if ( ch == null ) {
            ch = new IRCChannel( name );
            ch.setNetwork( this );
            namespace.put( name, ch );
            channels.add( ch );
        }

        return ch;
    }

    public IRCChannel findChannel( String name ) {
        name = IRCNamespace.normalizeChannel( name, locale );
        IRCChannel ch = namespace.channelForName( name );
        return ch;
    }

    public IRCUser getUser( String name ) {
        if ( !IRCNamespace.isNick( name ) )
            return null;

        String nick = IRCNamespace.normalizeNick( name );
        IRCUser user = namespace.userForName( nick );
        if ( user == null ) {
            user = new IRCUser( name );
            user.setNetwork( this );
            namespace.put( nick, user );
        }

        return user;
    }

    public IRCUser findUser( String name ) {
        String nick = IRCNamespace.normalizeNick( name );
        IRCUser user = namespace.userForName( nick );
        return user;
    }

    public IRCUser removeUser( String name ) {
        String nick = IRCNamespace.normalizeNick( name );
        IRCUser user = (IRCUser) namespace.remove( nick );
        return user;
    }

    public IRCUser renameUser( String oldNick, String newNick ) {
        IRCUser oldUser = findUser( oldNick );
        if ( oldUser == null )
            throw new IllegalArgumentException( "not found: " + newNick );

        if ( !oldNick.equalsIgnoreCase( newNick ) ) {
            IRCUser newUser = findUser( newNick );
            if ( newUser != null )
                throw new IllegalArgumentException( "already exsists: " + oldNick + "->" + newNick );
        }

        namespace.put( newNick, removeUser( oldNick ) );
        return oldUser;
    }

    /**
     * イベントリスナー
     */
    private EventListenerList listenerList = new EventListenerList();

    public void addChangeListener( ChangeListener listener ) {
        listenerList.add( ChangeListener.class, listener );
    }

    public void removeChangeListener( ChangeListener listener ) {
        listenerList.remove( ChangeListener.class, listener );
    }

    protected void fireStateChanged() {
        ChangeListener[] listeners = listenerList.getListeners( ChangeListener.class );
        ChangeEvent evt = new ChangeEvent( this );
        for ( ChangeListener l : listeners ) {
            l.stateChanged( evt );
        }
    }

    public boolean isConnected() {
        return connection != null && connection.isConnected();
    }

    // 接続系

    /**
     * IRCネットワークに接続します。
     * 
     * @param address IRCアドレス
     */
    private void connect( IRCAddress address ) {
        try {
            // 接続
            this.connection = address.openConnection();
            connection.connect();

            // デフォルト入出力
            in = new BufferedReader( connection.reader() );
            out = connection.writer();

            // 受信開始
            new Thread( this ).start();
        }
        catch ( IOException x ) {
            throw new RuntimeException( "" + address, x );
        }
    }

    public void disconnect() {
        connection.disconnect();
    }

    private String readLine()
        throws IOException {
        return isConnected() ? in.readLine() : null;
    }

    /**
     * 指定した文字エンコーディングの出力ストリームを返します。
     * 
     * @param encoding 文字エンコーディング
     * @return 出力ストリーム。接続していない場合は null
     */
    public IRCWriter writer( String encoding ) {
        return connection.writer( encoding );
    }

    private void writeCommand( String command, Object... params ) {
        if ( isConnected() ) {
            out.format( command, params );
        }
    }

    /**
     * IRCネットワークにログインします。
     * 
     * @param nick ニックネーム
     * @param real 名前
     * @param mode ユーザモード
     * @param user ユーザ名
     * @param pass パスワード
     */
    private void login( String nick, String real, int mode, String user, String pass, String ch ) {
        this.nick = nick;
        if ( pass != null && pass.length() > 0 ) {
            writeCommand( "PASS", pass );
        }
        writeCommand( "NICK", nick );
        writeCommand( "USER", user, mode, real );
        if ( ch != null && ch.length() > 0 ) {
            writeCommand( "JOIN", ch, "" );
        }
    }

    /**
     * IRCネットワークにログインします。 IRCネットワークに接続したとき、パスワード、ニックネーム、ユーザ名、名前、モードを送信します。 パスワードは、不要な場合に省略できます。
     * 
     * @param address IRCアドレス
     */
    public IRCNetwork login( IRCAddress address ) {
        // 接続
        connect( address );

        // ログイン
        String nick = address.getNick();
        String real = address.getReal();
        int mode = address.getMode();
        String user = address.getUser();
        String pass = address.getPassword();
        String ch = address.getChannel();
        login( nick, real, mode, user, pass, ch );
        return this;
    }

    /**
     * IRCネットワークにログインします。
     * 
     * @param address IRCアドレス
     */
    public IRCNetwork login( String address, String user, String pass, String nick, String real, int mode ) {
        IRCAddress addr = new IRCAddress( address );
        connect( addr );
        String ch = addr.getChannel();
        login( nick, real, mode, user, pass, ch );
        return this;
    }

    public IRCNetwork login( String address, String user, String pass, String nick, String real ) {
        return login( address, user, pass, nick, real, 0 );
    }

    public IRCNetwork login( String address, String user, String pass ) {
        String nick = user;
        String real = "IRCCore 2.0";
        return login( address, user, pass, nick, real );
    }

    public IRCNetwork login( String address ) {
        String user = System.getProperty( "user.name" );
        return login( address, user, "" );
    }

    /**
     * IRC入力を読みとってイベントを発生します。
     */
    public void run() {
        // run loop
        try {
            while ( isConnected() ) {
                String line = readLine();
                if ( line == null )
                    break;
                fireReply( line );
            }
        }
        catch ( IOException x ) {
            x.printStackTrace();
            fireReply( "ERROR: " + x.toString() );
        }

        // close socket
        System.out.println( "disconnected: " + this );
    }

    /** 自動継続フラグ */
    private boolean pingpong = true;

    /**
     * PINGに対応する応答アクションです。 IRC接続を継続するには、定期的に実行する必要があります。 自動的に接続を継続したくない場合は、このメソッドをオーバーライドして応答しないようにします。
     */
    private void pong( IRCMessage message ) {
        String server = message.getTrailing();
        writeCommand( "PONG", server );
    }

    private void welcome( IRCMessage message ) {
        nick = message.getTarget();
        loginUser = getUser( nick );
        fireStateChanged();
    }

    private void names( IRCMessage message ) {
        String channel = message.getMessageSource();
        String[] names = message.getTrailing().split( " " );
        IRCChannel ch = getChannel( channel );
        for ( String nick : names ) {
            try {
                IRCUser user = getUser( nick );
                ch.addUser( user );
            }
            catch ( RuntimeException x ) {
                System.err.println( "nick=" + nick );
                x.printStackTrace();
            }
        }
        fireStateChanged();
    }

    /**
     * 自分のニックネーム変更の場合は新しいニックネームに更新
     */
    private void nick( IRCMessage message ) {
        String newNick = message.getTrailing();
        String oldNick = message.getNick();
        IRCUser user = getUser( oldNick );
        user.nick( newNick );
        fireStateChanged();
    }

    private void join( IRCMessage message ) {
        String nick = message.getNick();
        String channel = message.getTrailing();
        if ( channel == null )
            channel = message.getTarget();
        IRCUser user = getUser( nick );
        IRCChannel ch = getChannel( channel );
        user.join( ch );
        fireStateChanged();
    }

    private void part( IRCMessage message ) {
        String nick = message.getNick();
        String channel = message.getTarget();
        IRCUser user = getUser( nick );
        IRCChannel ch = getChannel( channel );
        user.part( ch );
        fireStateChanged();
    }

    private void quit( IRCMessage message ) {
        String nick = message.getNick();
        IRCUser user = getUser( nick );
        user.quit();
        fireStateChanged();
    }

    public String currentNick() {
        // return nick;
        return loginUser != null ? loginUser.getNick() : "";
    }

    public IRCUser getLoginUser() {
        return loginUser;
    }

    /**
     * 文字列をIRCメッセージとして解析してイベントを発生します。
     * 
     * @param line 入力文字列
     */
    protected void fireReply( String line ) {
        try {
            IRCMessage message = new IRCMessage( this, line );
            String command = message.getCommand();

            // コマンド前処理
            if ( pingpong && command.equalsIgnoreCase( "PING" ) )
                pong( message );
            else if ( command.equalsIgnoreCase( "NICK" ) )
                nick( message );
            else if ( command.equalsIgnoreCase( "JOIN" ) )
                join( message );
            else if ( command.equalsIgnoreCase( "353" ) )
                names( message );
            else if ( command.equalsIgnoreCase( "001" ) )
                welcome( message );

            // クライアント処理
            for ( IRCClient client : clientList ) {
                try {
                    client.listen( message );
                }
                catch ( RuntimeException x ) {
                    log.error( x );
                    x.printStackTrace();
                }
            }

            // コマンド後処理
            if ( command.equalsIgnoreCase( "PART" ) )
                part( message );
            else if ( command.equalsIgnoreCase( "QUIT" ) )
                quit( message );
        }
        catch ( RuntimeException x ) {
            x.printStackTrace();
        }
    }

    /**
     * IRCネットワークの文字列表現を返します。
     */
    public String toString() {
        return new ToStringBuilder( this ).append( "name", name ).append( "channels", channels ).toString();
    }
}
