package dareka;

import java.io.IOException;
import java.net.ConnectException;
import java.net.Socket;
import java.net.SocketException;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

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

import dareka.common.CloseUtil;
import dareka.common.Config;
import dareka.common.HttpIOException;
import dareka.processor.HttpRequestHeader;
import dareka.processor.Processor;
import dareka.processor.Resource;

public class ConnectionManager implements Runnable {
	public static Log logger = LogFactory.getLog(ConnectionManager.class);
    private Socket browser;
    private Config config;
    private String processingURI;
    private List<ProcessorEntry> processorEntries =
            new ArrayList<ProcessorEntry>();

    public ConnectionManager(Config config, Socket browser) {
        if (config == null) {
            throw new IllegalArgumentException("config must not be null");
        }
        if (browser == null) {
            throw new IllegalArgumentException("browser must not be null");
        }

        this.config = config;
        this.browser = browser;
    }

    public void run() {
        try {
            while (processAPairOfMessages()) {
                // loop until the method returns false.
                processingURI = null;
            }
            logger.debug("loop end");
        } catch (ConnectException e) {
            // アウトバウンド側に接続失敗
        	logger.debug(e);
            printWarning(e);
        } catch (SocketException e) {
        	logger.debug(e);

            // Connection reset はよくあるので通常はログに出さない
            if (!isConnectionReset(e)) {
                printWarning(e);
            }
        } catch (HttpIOException e) {
        	logger.debug(e);
        } catch (IOException e) {
            // NIOを使っているとConnection resetではなく以下のメッセージを持った
            // IOExceptionになる。
            // 「既存の接続はリモート ホストに強制的に切断されました。」
            // 「確立された接続がホスト コンピュータのソウトウェアによって中止されました。」
            // UnknownHostExceptionも通常ログには出さない
        	logger.debug(e);
        } catch (Exception e) {
        	logger.debug(e);
            printWarning(e);
        } finally {
            if (!browser.isClosed()) {
                consumeBrowserInput();
                CloseUtil.close(browser);
            }
        }
    }

    private void printWarning(Exception e) {
        logger.warn("failed to process: " + processingURI + "\n\t"
                + e.toString());
    }

    private boolean isConnectionReset(SocketException e) {
        // この判定方法でいいかは…
        return e.getMessage().startsWith("Connection reset")
                || e.getMessage().startsWith("Software caused connection abort");
    }

    /**
     * 余分なデータを送ってくるブラウザ対策。
     */
    private void consumeBrowserInput() {
        try {
            // java.net APIで処理する。
            // そのために非ブロックモードを解除。
            SocketChannel bc = browser.getChannel();
            // bcはnullにはならない。
            bc.configureBlocking(true);

            // 次の読み込みでデッドロックを避けるため出力は停止。
            // (FINを送信)
            browser.shutdownOutput();

            // IEはPOSTリクエストの時に余分にCRLFを送って来ているので
            // IEが送信を正常終了できるように読み飛ばしてやる。
            // (FINの受信まで待つ)
            // これを読み飛ばしてやらないでSocket#close()すると
            // 「メッセージサーバーに接続できませんでした。」などになる
            // 参考: IEが余分なCRLFを送信することについて触れられ
            // ている公式の文書
            // http://support.microsoft.com/kb/823099/
            while (browser.getInputStream().read() != -1) {
                // no nothing
            }
        } catch (IOException e) {
            // Connection reset系のエラーが来る。
            // 処理中でresetされていた場合はここでもまた例外になるが、
            // 実際にread()してみないと区別が付かないので仕方ない。
            logger.debug(e.toString() + "(consuming)");
        }
    }

    private boolean processAPairOfMessages() throws IOException {
        HttpRequestHeader requestHeader =
                new HttpRequestHeader(browser.getInputStream());
        processingURI = requestHeader.getURI();

        logger.debug(requestHeader.getMethod() + " "
                + requestHeader.getURI());

        for (ProcessorEntry entry : processorEntries) {
            if (isMatchToEntry(entry, requestHeader)) {
                boolean canContinue =
                        useProcessor(requestHeader, entry.getProcessor());
                logger.debug("end");
                return canContinue;
            }
        }

        throw new HttpIOException("request cannot be processed:\r\n"
                + requestHeader);
    }

    private boolean useProcessor(HttpRequestHeader requestHeader,
            Processor processor) throws IOException {
        Resource resource = processor.onRequest(requestHeader);

        if (resource == null) {
            throw new HttpIOException(
                    "request processor failed to handle request:\r\n"
                            + requestHeader);
        }

        requestHeader.removeHopByHopHeaders();

        return resource.transferTo(browser, requestHeader, config);
    }

    private boolean isMatchToEntry(ProcessorEntry entry,
            HttpRequestHeader requestHeader) {
        if (entry.getMethod() != null) {
            if (!entry.getMethod().equals(requestHeader.getMethod())) {
                return false;
            }
        }

        if (entry.getUri() != null) {
            Matcher m = entry.getUri().matcher(requestHeader.getURI());
            if (!m.lookingAt()) {
                return false;
            }
            // TODO マッチ結果をProcessorで再利用できるようにして高速化
        }

        return true;
    }

    public void addProcessor(String method, Pattern url, Processor processor) {
        if (processor == null) {
            throw new IllegalArgumentException("processor must not be null");
        }

        ProcessorEntry entry = new ProcessorEntry(method, url, processor);

        processorEntries.add(entry);
    }
}
