/*
 * Copyright 2004-2012 the Seasar Foundation and the Others.
 *
 * 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 org.seasar.mayaa.impl;

import java.util.Iterator;

import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.seasar.mayaa.FactoryFactory;
import org.seasar.mayaa.ParameterAware;
import org.seasar.mayaa.cycle.ServiceCycle;
import org.seasar.mayaa.engine.Engine;
import org.seasar.mayaa.engine.Page;
import org.seasar.mayaa.engine.Template;
import org.seasar.mayaa.engine.specification.Specification;
import org.seasar.mayaa.impl.cycle.CycleUtil;
import org.seasar.mayaa.impl.cycle.web.MockHttpServletRequest;
import org.seasar.mayaa.impl.cycle.web.MockHttpServletResponse;
import org.seasar.mayaa.impl.provider.ProviderUtil;
import org.seasar.mayaa.impl.source.ApplicationSourceDescriptor;
import org.seasar.mayaa.impl.source.SourceHolderFactory;
import org.seasar.mayaa.impl.util.ObjectUtil;
import org.seasar.mayaa.impl.util.StringUtil;
import org.seasar.mayaa.source.PageSourceFactory;
import org.seasar.mayaa.source.SourceHolder;

/**
 * ファイルとしてコンテキストルート以下にあるテンプレートを探して順にビルドする。
 * 単独のスレッドで動作する。
 * ServiceProviderファイルのEngine設定でautoBuildオプションをtrueにすると有効。
 * TODO AutoPageBuilderの詳細な設定説明
 * TODO AutoPageBuilderを抽象化し、処理のカスタマイズを可能にする
 *
 * @author Taro Kato (Gluegent, Inc.)
 * @author Koji Suga (Gluegent Inc.)
 */
public class AutoPageBuilder implements Runnable {
    private static final Log LOG = LogFactory.getLog(AutoPageBuilder.class);

    public static final String OPTION_AUTO_BUILD = "autoBuild";
    private static final String OPTION_AUTO_BUILD_REPRAT =
        OPTION_AUTO_BUILD + ".repeat";
    private static final String OPTION_AUTO_BUILD_WAIT =
        OPTION_AUTO_BUILD + ".wait";
    private static final String OPTION_AUTO_BUILD_FILE_FILTERS =
        OPTION_AUTO_BUILD + ".fileNameFilters";
    private static final String OPTION_AUTO_BUILD_RENDER_MATE =
        OPTION_AUTO_BUILD + ".renderMate";
    private static final String OPTION_AUTO_BUILD_CONTEXT_PATH =
        OPTION_AUTO_BUILD + ".contextPath";
    private static final boolean REPEAT_DEFAULT = false;
    private static final int WAIT_DEFAULT = 60;
    private static final boolean RENDER_MATE_DEFAULT = false;
    private static final String CONTEXT_PATH_DEFAULT = "/";

    public static final AutoPageBuilder INSTANCE = new AutoPageBuilder();

    private AutoPageBuilder() {
        // no operation
    }

    private Thread _thread;
    private boolean _repeat;
    private int _wait;
    private boolean _renderMate;
    private String[] _fileFilters;
    private ServletContext _servletContext;
    private String _contextPath;
    private long _buildTimeSum;
    private long _renderTimeSum;

    public void init(ServletConfig servletConfig) {
        init(servletConfig, null);
    }

    /**
     * 初期化処理。
     * Engine設定の各種パラメータを取得した後、別スレッドで自動ビルドを開始する。
     * ただしautoBuildがfalseに設定されている場合は何もしない。
     *
     * contextPathは"/mayaa"のように"/"始まり、"/"無しで終わる。
     *
     * @param servletConfig ServletConfig
     * @param contextPath コンテキストパス
     */
    public void init(ServletConfig servletConfig, String contextPath) {
        Engine engine = ProviderUtil.getEngine();
        boolean autoBuild = ObjectUtil.booleanValue(
                engine.getParameter(OPTION_AUTO_BUILD), false);
        if (autoBuild && (ParameterAware.IS_SECURE_WEB == false)) {
            _servletContext = servletConfig.getServletContext();
            _contextPath = prepareContextPath(contextPath, engine);
            _repeat = ObjectUtil.booleanValue(
                    engine.getParameter(OPTION_AUTO_BUILD_REPRAT),
                    REPEAT_DEFAULT);
            _wait = ObjectUtil.numberValue(
                    engine.getParameter(OPTION_AUTO_BUILD_WAIT),
                    new Integer(WAIT_DEFAULT)).intValue() * 1000;
            _renderMate = ObjectUtil.booleanValue(
                    engine.getParameter(OPTION_AUTO_BUILD_RENDER_MATE),
                    RENDER_MATE_DEFAULT);

            String filters = engine.getParameter(OPTION_AUTO_BUILD_FILE_FILTERS);
            if (filters != null) {
                _fileFilters = filters.split(";");
            } else {
                _fileFilters = new String[]{".html"};
            }
            _thread = new Thread(this) {
                {
                    setDaemon(true);
                    setName("mayaa.AutoPageBuilder");
                    setPriority(Thread.MIN_PRIORITY);
                    start();
                }
            };
            LOG.info("mayaa.AutoPageBuilder start");
        }
    }

    /**
     * コンテキストパスの前処理をする。
     * contextPathがnullならEngine設定のautoBuild.contextPathから値を取得する。
     * どちらもnullまたは空文字列ならデフォルトの"/"を返す。
     * そうでない場合、"/foobar"の形式になるよう"/"を調整して返す。
     *
     * @param contextPath initメソッドのパラメータに渡されたcontextPath
     * @param engine 現在のEngineインスタンス
     * @return コンテキストパス
     */
    protected String prepareContextPath(String contextPath, Engine engine) {
        if (contextPath == null) {
            contextPath = engine.getParameter(OPTION_AUTO_BUILD_CONTEXT_PATH);
        }
        if (StringUtil.isEmpty(contextPath)) {
            return CONTEXT_PATH_DEFAULT;
        }
        if (contextPath.charAt(0) != '/') {
            contextPath = '/' + contextPath;
        }
        int length = contextPath.length();
        if (length > 1 && contextPath.charAt(length - 1) == '/') {
            contextPath = contextPath.substring(0, length - 1);
        }
        return contextPath;
    }

    public void destroy() {
        if (_thread != null) {
            _thread = null;
            LOG.info("mayaa.AutoPageBuilder end");
        }
    }

    protected void reportTime(Specification spec, long time) {
        LOG.info(spec.getSystemID() + " build time: " + time + " msec.");
    }

    public void run() {
        Thread currentThread = Thread.currentThread();
        try {
            Engine engine = ProviderUtil.getEngine();
            long engineBuildTime = diffMillis(0);
            synchronized (engine) {
                engine.build();
            }
            reportTime(engine, diffMillis(engineBuildTime));

            // prepare SourceHolderFactory
            FactoryFactory.getFactory(PageSourceFactory.class);

            while (currentThread == _thread) {
                _buildTimeSum = 0;
                _renderTimeSum = 0;
                for (Iterator it = SourceHolderFactory.iterator(); it.hasNext();) {
                    SourceHolder holder = (SourceHolder) it.next();
                    for (Iterator itSystemID = holder.iterator(_fileFilters);
                            itSystemID.hasNext();) {
                        String systemID = (String) itSystemID.next();
                        if (systemID.startsWith("/") == false) {
                            systemID = "/" + systemID;
                        }
                        try {
                            buildPage(systemID);
                        } catch (Throwable e) {
                            LOG.error("page load failed: " + systemID, e);
                        }
                        Thread.sleep(100);
                    }
                }
                LOG.info("page all build time: " + _buildTimeSum + " msec.");
                if (_renderMate) {
                    LOG.info("page all render time: " + _renderTimeSum + " msec.");
                }
                if (_repeat == false) {
                    break;
                }
                Thread.sleep(_wait);
            }

        } catch (InterruptedException ignore) {
            // no operation
        }
    }

    protected long diffMillis(long millis) {
        if (millis == 0) {
            return System.currentTimeMillis();
        }
        return System.currentTimeMillis() - millis;
    }

    protected void buildPage(String systemID) {
        if (systemID.indexOf(ApplicationSourceDescriptor.WEB_INF) >= 0) {
            return;
        }
        MockHttpServletRequest request =
            new MockHttpServletRequest(_servletContext, _contextPath);
        MockHttpServletResponse response = new MockHttpServletResponse();
        request.setPathInfo(systemID);
        CycleUtil.initialize(request, response);
        try {
            String pageName = CycleUtil.getRequestScope().getPageName();
            ServiceCycle cycle = CycleUtil.getServiceCycle();
            Engine engine = ProviderUtil.getEngine();
            engine.build();
            cycle.setOriginalNode(engine);
            cycle.setInjectedNode(engine);
            if (engine.isPageRequested()) {
                long pageBuildTime = diffMillis(0);
                Page page = engine.getPage(pageName);
                reportTime(page, diffMillis(pageBuildTime));

                long templateBuildTime = diffMillis(0);
                Template template = page.getTemplate(
                        CycleUtil.getRequestScope().getRequestedSuffix(),
                        CycleUtil.getRequestScope().getExtension());

                templateBuildTime = diffMillis(templateBuildTime);
                _buildTimeSum += templateBuildTime;

                if (_renderMate) {
                    long renderTime = diffMillis(0);
                    engine.doService(null, true);
                    renderTime = diffMillis(renderTime);
                    if (response.getStatus() ==
                            HttpServletResponse.SC_INTERNAL_SERVER_ERROR) {
                        LOG.debug(systemID + " (render: ----"
                                + " / build: " + templateBuildTime + " msec)");
                    } else {
                        _renderTimeSum += renderTime;
                        LOG.debug(systemID + " (render: " + renderTime
                                            + " / build: " + templateBuildTime + " msec)");
                    }
                } else {
                    reportTime(template, templateBuildTime);
                }
            }
        } finally {
            CycleUtil.cycleFinalize();
        }
    }

}
