/*
 * $Revision: 1.17 $ $Date: 2005/11/24 23:43:00 $
 * Copyright (C) 2004-2005 SUGIMOTO Ken-ichi
 */

package feat.v1;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.InvocationTargetException;
import java.util.Enumeration;
import java.util.Locale;

import javax.servlet.http.HttpServletRequest;

import org.apache.commons.beanutils.ConversionException;
import org.apache.commons.beanutils.ConvertUtils;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import feat.v1.config.CommandDeclaration;
import feat.v1.config.DataDeclaration;
import feat.v1.config.ExceptionDeclaration;
import feat.v1.config.FeatConfig;
import feat.v1.config.FeatureConfig;
import feat.v1.config.FeatureDeclaration;
import feat.v1.config.FileLocator;
import feat.v1.config.ForwardDeclaration;
import feat.v1.config.Function;
import feat.v1.config.PageDeclaration;
import feat.v1.config.PreProcessDeclaration;
import feat.v1.config.RedirectDeclaration;
import feat.v1.config.ResponseDeclaration;
import feat.v1.config.ViewDeclaration;
import feat.v1.template.HTMLDocument;
import feat.v1.template.HTMLTemplate;

public class Processor {

    protected static Log log = LogFactory.getLog(Processor.class);

    public Processor() {
    }

    /**
     * リクエストを処理する。
     * @param acc
     * @return
     * @throws NotHandledException アプリケーションの例外設定で処理されなかった例外
     * @throws ResponseException レスポンスを特定できない
     * @throws FileNotFoundException テンプレートファイルが見つからかった
     * @throws TemplateParsingException テンプレートの解析ができなかった
     * @throws IOException HTMLデータ読み込み中のIO例外
     * @throws CommandInstantiationException コマンドがインスタンス化できなかった
     */
    public Response process(ContextAccessor acc) throws
            NotHandledException, ResponseException, FileNotFoundException,
            TemplateParsingException, IOException, CommandInstantiationException {

        ResponsePointer p;
        try {
            p = null;
            FeatureDeclaration featureDec = acc.getCurrentFeatureDeclaration();
            CommandDeclaration commandDec = acc.getCurrentCommandDeclaration();
            // コマンドインスタンスを作る
            Command command;
            try {
                command = (Command) commandDec.getCommandClass().newInstance();
            }
            catch (InstantiationException ex1) {
                throw new CommandInstantiationException(ex1);
            }
            catch (IllegalAccessException ex1) {
                throw new CommandInstantiationException(ex1);
            }

            // Formの処理
            boolean c = processParameters(command, acc);

            // エラーなし、またはエラーがあっても続行の指示があればコマンドを実行する
            if ( acc.getFeatureErrors().count() == 0 || c) {
                p = processCommand(command, acc);
            }
            else {
                // エラー時の遷移先ページを決定。
                // error-pageが指定されていなければhomeページに移動。
                p = new ResponsePointer(featureDec.getName(), commandDec.getErrorPage().getName());
            }

            return processResponse(p, acc);
        }
        catch (ApplicationException ex) {
            acc.setException((Exception)ex.getCause());
            p = processException(acc, ex.getFeatureDeclaration());
        }
        /*catch (Exception ex) {
            acc.setException(ex);
            p = processException(acc, acc.getCurrentFeatureDeclaration());
        }*/

        // 例外が起きたときのレスポンス
        try {
            return processResponse(p, acc);
        }
        catch (ApplicationException ex) {
            throw new NotHandledException(ex.getCause());
        }
    }

    /**
     * HTTPリクエストのパラメータをコマンドにコピーする。
     * HTTPパラメータに対応するコマンドのプロパティが存在しない場合、その値は無視される。
     * @param command
     * @param acc
     * @return コマンドの実行を許可するならtrue。
     *          プリプロセスでコマンド実行の事前条件を満たさず、コマンドの実行を許可しないならfalse。
     * @throws ApplicationException PreProcessで例外が起きた
     */
    private boolean processParameters(Command command, ContextAccessor acc) throws ApplicationException {
        // プリプロセスの実行
        PreProcessDeclaration[] preProcDecs = acc.getCurrentCommandDeclaration().getPreProcessList();
        Function[] ppfunc = acc.getCurrentCommandDeclaration().getPreProcessFunctions();
        try {
            for(int i=0; i<preProcDecs.length; i++) {
                PreProcess pp = preProcDecs[i].getPreProcess();
                boolean pass = pp.examine(command, ppfunc[i].getParams(), acc);
                if ( !pass && !preProcDecs[i].isContinue() )
                    return false;
            }
        }
        catch (Exception ex) {
            throw new ApplicationException(ex, command.getCommandDeclaration().getFeatureDeclaration());
        }

        // リクエストパラメータをコマンドにコピー
        HttpServletRequest req = acc.getRequest();
        boolean getMethod = req.getMethod().equalsIgnoreCase("get");
        for(Enumeration e=req.getParameterNames(); e.hasMoreElements(); ) {
        //for(Iterator i=reqinfo.getParameterNames(); i.hasNext(); ) {
            String key = (String)e.nextElement();
            try {
                // TODO プロパティ値が複数の場合に対応
                Class type = PropertyUtils.getPropertyType(command, key);
                Object value = ConvertUtils.convert(req.getParameter(key), type);
                if ( getMethod && value instanceof String ) {
                    try {
                        // UTF-8で送られてくるのが正しい
                        // TODO GETメソッドのクエリ文字コードの扱いを設定可能にする
                        value = new String(((String)value).getBytes("ISO-8859-1"), acc.getEncoding());
                    }
                    catch (UnsupportedEncodingException ex1) {
                    }
                }
                PropertyUtils.setProperty(command, key, value);
            }
            catch (NoSuchMethodException ex) {
                log.warn("NoSuchMethodException feature:"+acc.getCurrentFeatureDeclaration().getName()+" property:"+key);
            }
            catch (InvocationTargetException ex) {
                log.warn("InvocationTargetException feature:"+acc.getCurrentFeatureDeclaration().getName()+" property:"+key);
            }
            catch (IllegalAccessException ex) {
                log.warn("IllegalAccessException feature:"+acc.getCurrentFeatureDeclaration().getName()+" property:"+key);
            }
            catch (ConversionException ex) {
                acc.getFeatureErrors().add(new ValidationError(key, key+"の値が不正", ValidationError.CONVERSION_ERROR));
            }
        }

        return true;
    }

    /**
     * コマンドを実行する。
     * @return feat.v1.ResponsePointer
     * @throws ApplicationException アプリケーションの例外
     */
    private ResponsePointer processCommand(Command command, ContextAccessor acc) throws ApplicationException {
        String responseName;
        try {
            long start = System.currentTimeMillis();
            responseName = command.execute(acc);
            long elapseTime = System.currentTimeMillis() - start;
            log.info(acc.getCurrentFeatureDeclaration().getName()+"/"+acc.getCurrentCommandDeclaration().getName()+"(command) "+elapseTime+"[ms]");
        }
        catch (Exception ex) {
            throw new ApplicationException(ex, acc.getCurrentFeatureDeclaration());
        }

        ResponsePointer ret = acc.getCurrentCommandDeclaration().getResponsePointer(responseName);

        return ret;
    }


    /**
     * レスポンスの処理。
     * @param pointer
     * @param acc
     * @return
     * @throws FileNotFoundException テンプレートファイルが見つからかった
     * @throws TemplateParsingException テンプレートの解析ができなかった
     * @throws IOException HTMLデータ読み込み中のIO例外
     * @throws ResponseException レスポンスを特定できない
     */
    private Response processResponse(ResponsePointer pointer, ContextAccessor acc)
            throws ResponseException, FileNotFoundException , TemplateParsingException, IOException, ApplicationException {

        if ( pointer == null )
            throw new ResponseException("コマンド"+acc.getCurrentFeatureDeclaration().getName()+"."+
                    acc.getCurrentCommandDeclaration().getName()+"のレスポンスがnull");

        FeatConfig config = acc.getFeatConfig();

        // ポインタの情報でページを取り出す
        ResponseDeclaration responseDecl = null;
        FeatureDeclaration featureDec = config.getFeatureDeclaration(pointer.getFeatureName());
        if ( pointer.getName().equals(ResponsePointer.HOME_PAGE) ) {
            responseDecl = featureDec.getHomeResponse();
        }
        else
            responseDecl = featureDec.getResponseDeclaration(pointer.getName());

        // ページレスポンス
        if ( responseDecl instanceof PageDeclaration ) {
            PageDeclaration pageDec = (PageDeclaration)responseDecl;
            ViewDeclaration[] viewDecs = pageDec.getViewList();
            Function[] viewParams = pageDec.getViewParams();

            // テンプレートのロード
            FeatureConfig featureConfig = responseDecl.getFeatureDeclaration().getFeatureConfig();
            FileLocator templateFile = featureConfig.getTemplateLocation().newLocation(pageDec.getTemplateName());
            HTMLTemplate template = TemplateLoader.load(templateFile, acc.getLocale(), null);
            HTMLDocument doc = template.getDocument();//pageDec.getTemplate().getDocument(acc.getLocale());

            // ビューの実行
            for(int i=0; i<viewDecs.length; i++) {
                try {
                    View view = viewDecs[i].getView();
                    long start = System.currentTimeMillis();
                    doc = view.format(doc, viewParams[i].getParams(), acc);
                    long elapseTime = System.currentTimeMillis() - start;
                    log.info(acc.getCurrentFeatureDeclaration().getName()+"/"+viewDecs[i].getName()+"(view) "+elapseTime+"[ms]");
                }
                catch(Exception ex) {
                    throw new ApplicationException(ex, viewDecs[i].getFeatureDeclaration());
                }
            }

            // レスポンスの出力
            String enc = template.getEncoding();
            return new PageResponse(doc, enc);
        }

        // データレスポンス
        else if ( responseDecl instanceof DataDeclaration ) {
            DataDeclaration dataDecl = (DataDeclaration)responseDecl;
            return dataDecl.getResponse();
        }

        // フォワード
        else if ( responseDecl instanceof ForwardDeclaration ) {
            ForwardDeclaration forwardDecl = (ForwardDeclaration)responseDecl;
            return forwardDecl.getResponse();
        }

        // リダイレクト
        else if ( responseDecl instanceof RedirectDeclaration ) {
            RedirectDeclaration redirectDecl = (RedirectDeclaration)responseDecl;
            return redirectDecl.getResponse();
        }

        else {
            throw new ResponseException("未知のレスポンス型 "+responseDecl.getClass().toString());
        }
    }


    /**
     * 例外の処理。
     * @param acc
     * @return 例外の遷移先レスポンス
     * @throws NotHandledException 例外が処理されなかった
     */
    private ResponsePointer processException(ContextAccessor acc, FeatureDeclaration featureDecl) throws NotHandledException {
        ExceptionDeclaration exDecl = featureDecl.getExceptionDeclaration(acc.getException());
        if ( exDecl != null ) {
            // ログの出力
            String logLevel = exDecl.getLog();
            String logMessage = exDecl.getLogMessage();
            if ( logMessage == null )
                logMessage = "";
            else
                logMessage = acc.getStringResource(acc.getCurrentFeatureDeclaration(), logMessage, Locale.getDefault());
            if ( logLevel != null ) {
                if ( logLevel.equals(ExceptionDeclaration.LOG_FATAL) ) {
                    log.fatal(logMessage, acc.getException());
                }
                else if (logLevel.equals(ExceptionDeclaration.LOG_ERROR) ) {
                    log.error(logMessage, acc.getException());
                }
                else if (logLevel.equals(ExceptionDeclaration.LOG_WARN) ) {
                    log.warn(logMessage, acc.getException());
                }
                else if (logLevel.equals(ExceptionDeclaration.LOG_INFO) ) {
                    log.info(logMessage, acc.getException());
                }
                else if (logLevel.equals(ExceptionDeclaration.LOG_DEBUG) ) {
                    log.debug(logMessage, acc.getException());
                }
                else if (logLevel.equals(ExceptionDeclaration.LOG_TRACE) ) {
                    log.trace(logMessage, acc.getException());
                }
            }
            return exDecl.getResponsePointer();
        }
        else
            throw new NotHandledException(acc.getException());
    }
}
