package jp.sourceforge.shovel.device.impl;

import static jp.sourceforge.shovel.AvailabilityType.*;

import java.text.MessageFormat;
import java.util.HashSet;
import java.util.Properties;
import java.util.Set;

import org.apache.commons.lang.ArrayUtils;
import org.seasar.framework.container.S2Container;
import org.seasar.framework.log.Logger;

import jp.sourceforge.shovel.device.IDelayExecutor;
import jp.sourceforge.shovel.device.IDelayExecutorContext;
import jp.sourceforge.shovel.entity.IDevice;
import jp.sourceforge.shovel.entity.IFavorite;
import jp.sourceforge.shovel.entity.IFriendship;
import jp.sourceforge.shovel.entity.IStatus;
import jp.sourceforge.shovel.entity.IStatusWrapper;
import jp.sourceforge.shovel.entity.IUser;
import jp.sourceforge.shovel.exception.ApplicationException;
import jp.sourceforge.shovel.logic.IDirectoryLogic;
import jp.sourceforge.shovel.logic.IShovelLogic;

abstract public class AbstractDelayExecutorImpl implements IDelayExecutor {
    static Logger logger = Logger.getLogger(AbstractDelayExecutorImpl.class);
    
    Properties properties_;
    
    /**
     * 遅延処理実行器の表示名を取得
     * @return 遅延処理実行器の表示名（文字列）
     */
    public String getDisplayName() {
        return properties_.getProperty("displayName");
    }
    public void setProperty(String key, String value) {
        if (properties_ == null) {
            properties_ = new Properties();
        }
        properties_.put(key, value);
    }
    
    /////
    
    //TODO 外部リソース化
    //書式は{アカウントID}: {本文}
    static final String bodyFormat_ = "{0}: {1}";
    
    /**
     * メッセージ送信者に鸚鵡返しするか否か
     * @return 鸚鵡返しするか否か
     */
    boolean isRepeat() {
        return true;
    }
    /**
     * メッセージの送信
     * @param receiver  受信者
     * @param body  メッセージ本文
     * @throws ApplicationException
     */
    abstract void sendMessage(IUser receiver, String body) throws ApplicationException;
    /**
     * メッセージの返信
     * @param sender  送信者
     * @param receiver  受信者
     * @param body  メッセージ本文
     * @throws ApplicationException
     */
    void replyMessage(IUser sender, IUser receiver, String body) throws ApplicationException {
        sendMessage(sender, body);
        IFriendship follower = getShovelLogic().getFriendship(receiver.getUserId(), sender.getUserId());
        if (follower == null || (sender.isProtect() && !follower.isAccept())) {
            return;
        }
        sendMessage(receiver, body);
    }
    /**
     * メッセージの受信
     * @param address  （デバイスにおける）メッセージを送るアドレス
     * @param body  メッセージ本文
     * @throws ApplicationException
     */
    void receiveMessage(String address, String body) throws ApplicationException {
        IDirectoryLogic directoryLogic = getDirectoryLogic();
        IUser loginUser = directoryLogic.getUserByDevice(getExecutorId(), address);
        if (loginUser == null) {
            //BAN
            return;
        }
        
        //アクティベーション
        IDevice device = loginUser.getDevice();
        StringBuilder message = new StringBuilder();
        if (device.getAvailabilityType().isInactivation()) {
            String releaseKey = loginUser.getReleaseKey();
            if (releaseKey.compareTo(body) == 0) {
                //解除！
                device.setAvailabilityType(ON);
                getShovelLogic().updateDevice(device);
                //TODO 外部リソース化
                message.append("アカウントを確認しました。\n");
                message.append("デバイスに通知を開始します。\n");
                message.append("通知を止めたいときは\"off\"を送ってください。\n");
                message.append("詳細については\"help\"を送ってください。");
                sendMessage(loginUser, message.toString());
            } else {
                //TODO 解除キーを催促
            }
            return;
        }
        
        IShovelLogic shovelLogic = getShovelLogic();
        IStatusWrapper statusWrapper = shovelLogic.parseStatus(body);
        if (device.getAvailabilityType().isOff() &&
            !statusWrapper.getStatusType().isWake()) {
            return;
        }
        
        String foreignKey = statusWrapper.getForeignKey();
        switch (statusWrapper.getStatusType()) {
        case STATUS:
            IUser sender = loginUser;
            long referenceSenderId = 0;
            if (foreignKey != null) {
                IUser referenceSender = directoryLogic.getUserByForeignKey(foreignKey);
                referenceSenderId = referenceSender.getUserId();
            }
            directoryLogic.incrementStatuses(sender.getUserId());
            shovelLogic.receiveStatus(statusWrapper.getText(), sender.getLocation(), "im",
                    referenceSenderId, statusWrapper.isOpen(), sender.getUserId());
            message = null;
            break;
        case DIRECT_MESSAGE:
            IUser receiver = directoryLogic.getUserByForeignKey(foreignKey);
            directoryLogic.incrementDirectMessages(receiver.getUserId());
            shovelLogic.createDirectMessage(statusWrapper.getText(), "im",
                    loginUser.getUserId(), receiver.getUserId(), false);
            //TODO 外部リソース化
            message.append(foreignKey);
            message.append("にダイレクトメッセージを送りました。");
            break;
        case FAVORITE:
            sender = directoryLogic.getUserByForeignKey(foreignKey);
            IStatus status = shovelLogic.getRecent(sender.getUserId());
            //TODO 外部リソース化
            if (status == null) {
                message.append("まだ");
                message.append(foreignKey);
                message.append("の投稿がありません。");
            } else {
                long statusId = status.getStatusId();
                IFavorite favorite = shovelLogic.getFavorite(statusId, loginUser.getUserId());
                if (favorite == null) {
                    directoryLogic.incrementFavorites(loginUser.getUserId());
                    if (status.getSenderId() != loginUser.getUserId()) {
                        directoryLogic.incrementGivenFavorites(status.getSenderId());
                    }
                    shovelLogic.createFavorite(statusId, loginUser.getUserId(), false);
                    
                    //TODO 外部リソース化
                    message.append("「");
                    message.append(foreignKey);
                    message.append(":");
                    message.append(status.getBody());
                    message.append("」\n");
                    message.append("をお気に入りに登録しました。");
                } else {
                    message.append("お気に入りに登録ずみです。");
                }
            }
            break;
        case FOLLOW:
            IUser active = loginUser;
            IUser passive = directoryLogic.getUserByForeignKey(foreignKey);
            IFriendship friend = shovelLogic.createFriendship(active.getUserId(), passive.getUserId(), active.isProtect(), false);
            message.append(foreignKey);
            //TODO 外部リソース化
            if (passive.isProtect() && !friend.isAccept()) {
                message.append("にフォローのお願いをしました。");
            } else {
                message.append("をフォローしました。");
            }
            break;
        case LEAVE:
            active = loginUser;
            passive = directoryLogic.getUserByForeignKey(foreignKey);
            getShovelLogic().destroyFriendship(active.getUserId(), passive.getUserId(), active.isProtect(), false);
            message.append(foreignKey);
            //TODO 外部リソース化
            message.append("のフォローを停止しました。");
            break;
        case NOTIFY_ON:
            active = loginUser;
            passive = directoryLogic.getUserByForeignKey(foreignKey);
            getShovelLogic().updateNotification(active.getUserId(), passive.getUserId(), true);
            message.append(foreignKey);
            //TODO 外部リソース化
            message.append("の更新の通知を再開します。");
            break;
        case NOTIFY_OFF:
            active = loginUser;
            passive = directoryLogic.getUserByForeignKey(foreignKey);
            getShovelLogic().updateNotification(active.getUserId(), passive.getUserId(), false);
            message.append(foreignKey);
            //TODO リソース化
            message.append("の更新の通知を停止します。");
            break;
        case SLEEP:
            device.setAvailabilityType(OFF);
            getShovelLogic().updateDevice(device);
            //TODO 外部リソース化
            message.append("デバイスへの通知を停止します。\n");
            message.append("通知を再開したいときは\"on\"を送ってください。");
            break;
        case WAKE:
            device.setAvailabilityType(ON);
            getShovelLogic().updateDevice(device);
            //TODO リソース化
            message.append("デバイスへの通知を再開します。\n");
            message.append("通知を停止したいときは\"off\"を送ってください。");
            break;
        default:
            message = null;
            break;
        }
        if (message != null) {
            sendMessage(loginUser, message.toString());
        }
    }
    
    /////
    
    /**
     * 遅延実行
     * @param context  遅延実行コンテキスト
     * @throws ApplicationException
     */
    public void execute(IDelayExecutorContext context) throws ApplicationException {
        if (!isConnected() || context.isCommit() || !context.getDelayExecutionType().isUpdateStatus()) {
            return;
        }
        
        IStatus status = context.getStatus();
        long senderId = status.getSenderId();
        long receiverId = status.getReferenceSenderId();
        IUser sender = getDirectoryLogic().getUser(senderId);
        Object[] args = {sender.getForeignKey(), status.getBody()};
        MessageFormat formatter = new MessageFormat(bodyFormat_);
        String body = formatter.format(args);
        
        //Reply（@付きメッセージ）は元ネタのステータスを変更した人に送る
        IUser receiver = getDirectoryLogic().getUser(receiverId);
        if (receiver != null) {
            replyMessage(sender, receiver, body);
            return;
        }
        
        Set<Long> userIdSet = new HashSet<Long>();
        //送信者に鸚鵡返しするか？
        if (isRepeat()) {
            //デバイスなら個人設定で判断すべき
            IDevice device = sender.getDevice();
            if (getExecutorId().compareToIgnoreCase(device.getType()) == 0 &&
                device.getAvailabilityType() == ON) {
                //初回は送信者にも返す
                userIdSet.add(senderId);
            }
        }
        
        //TODO
        int offset = 0, limit = 50;
        while (true) {
            boolean nextPage = false;
            IFriendship[] followers = getFollowers(senderId, offset, limit + 1);
            if (followers != null && followers.length > 0) {
                nextPage = followers.length > limit;
                if (nextPage) {
                    offset += limit;
                    followers = (IFriendship[])ArrayUtils.subarray(followers, 0, limit);
                }
                for (IFriendship follower : followers) {
                    long userId = follower.getActiveId();
                    userIdSet.add(userId);
                }
            }
            userIdSet.remove(0);
            long[] userIds = ArrayUtils.toPrimitive(userIdSet.toArray(new Long[userIdSet.size()]));
            IUser[] users = this.getDirectoryLogic().getUsers(userIds);
            if (users == null || users.length <= 0) {
                return;
            }
            //TODO 一括でメッセージを投げる方法はないものか。。
            for (IUser user : users) {
                sendMessage(user, body);
            }
            if (!nextPage) {
                break;
            }
        }
    }
    
    IFriendship[] getFollowers(long senderId, int offset, int limit) {
        return getShovelLogic().getFollowers(senderId, getExecutorId(), offset, limit);
    }
    
    /**
     * ログイン
     * @throws ApplicationException
     */
    abstract void login() throws ApplicationException;
    /**
     * ログアウト
     * @throws ApplicationException
     */
    abstract void logout();
    /**
     * 配信サーバに接続
     */
    public void connect() throws ApplicationException {
        login();
    }
    /**
     * 配信サーバから切断
     */
    public void disconnect() {
        logout();
    }
    
    /////
    
    S2Container container_;
    
    public void setContainer(S2Container container) {
        container_ = container;
    }
    S2Container getContainer() {
        return container_;
    }
    IDirectoryLogic getDirectoryLogic() {
        return (IDirectoryLogic)getContainer().getComponent(IDirectoryLogic.class);
    }
    IShovelLogic getShovelLogic() {
        return (IShovelLogic)getContainer().getComponent(IShovelLogic.class);
    }
}
