/*
 * 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.engine.processor;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Stack;

import org.seasar.mayaa.cycle.Response;
import org.seasar.mayaa.cycle.ServiceCycle;
import org.seasar.mayaa.cycle.scope.RequestScope;
import org.seasar.mayaa.engine.Engine;
import org.seasar.mayaa.engine.Page;
import org.seasar.mayaa.engine.processor.InformalPropertyAcceptable;
import org.seasar.mayaa.engine.processor.ProcessStatus;
import org.seasar.mayaa.engine.processor.ProcessorProperty;
import org.seasar.mayaa.engine.processor.VirtualPropertyAcceptable;
import org.seasar.mayaa.engine.specification.PrefixAwareName;
import org.seasar.mayaa.impl.CONST_IMPL;
import org.seasar.mayaa.impl.cycle.CycleUtil;
import org.seasar.mayaa.impl.cycle.DefaultCycleLocalInstantiator;
import org.seasar.mayaa.impl.engine.EngineUtil;
import org.seasar.mayaa.impl.engine.PageImpl;
import org.seasar.mayaa.impl.engine.RenderNotCompletedException;
import org.seasar.mayaa.impl.engine.RenderUtil;
import org.seasar.mayaa.impl.engine.specification.SpecificationUtil;
import org.seasar.mayaa.impl.provider.ProviderUtil;
import org.seasar.mayaa.impl.util.StringUtil;

/**
 * @author Masataka Kurihara (Gluegent, Inc.)
 */
public class InsertProcessor
        extends TemplateProcessorSupport
        implements CONST_IMPL, InformalPropertyAcceptable, VirtualPropertyAcceptable {

    private static final long serialVersionUID = -945900326182620192L;

    private static final String RENDERING_INSERT_CHAIN =
        InsertProcessor.class.getName() + "#renderingInsertChain";
    private static final String INSERT_PARAMS =
        InsertProcessor.class.getName() + "#insertParams";
    static {
        CycleUtil.registVariableFactory(RENDERING_INSERT_CHAIN, new DefaultCycleLocalInstantiator() {
            public Object create(Object[] params) {
                return new Stack/*<InsertRenderingParams>*/();
            }
        });
        CycleUtil.registVariableFactory(INSERT_PARAMS, new DefaultCycleLocalInstantiator() {
            public Object create(Object owner, Object[] params) {
                return new Stack/*<InsertRenderingParams>*/();
            }
        });
    }

    class PathPart {
    	String _pathValue;
    	String _pageName;
    	String _suffix;
    	String _extension;
    	boolean _needAdjustPath;
    }

    private ProcessorProperty _path;
    private ProcessorProperty _name;
    private transient PathPart _pathPart;

    private List/*<Serializable(ProcessorProperty or PrefixAwareName)>*/
                    _attributes;

    /**
     * 相対パスを解決、分解して拡張子などをあらかじめ取得する。
     */
    public void initialize() {
    	if (_path != null && _path.getValue() != null) {
	        if (_path.getValue().isLiteral()) {
	        	_pathPart = parsePath(_path.getValue().getScriptText());
	        }
    	}
    }

    private PathPart parsePath(String path) {
        Engine engine = ProviderUtil.getEngine();
        String suffixSeparator = engine.getParameter(SUFFIX_SEPARATOR);

        PathPart result = new PathPart();
    	if (path != null) {
    		String[] pagePath = StringUtil.parsePath(path, suffixSeparator);

	        String sourcePath = EngineUtil.getSourcePath(getStaticParentProcessor());

	        result._pathValue = path;
	        result._pageName = StringUtil.adjustRelativePath(sourcePath, pagePath[0]);
	        result._suffix = pagePath[1];
	        result._extension = pagePath[2];
	        if (path.startsWith("/") == false && path.startsWith("./") == false) {
	        	result._needAdjustPath = true;
	        }
    	}
    	return result;
    }

    // MLD property, required
    public void setPath(ProcessorProperty path) {
        if (path == null || path.getValue() == null) {
            throw new IllegalArgumentException();
        }
        String pathScript = path.getValue().getScriptText();
		if (StringUtil.isEmpty(pathScript)) {
			throw new IllegalArgumentException();
		}
        _path = path;
    }

    // MLD property
    public void setName(ProcessorProperty name) {
        if (name != null) {
            _name = name;
        }
    }

    // MLD method
    public void addInformalProperty(PrefixAwareName name, Serializable attr) {
        if (_attributes == null) {
            _attributes = new ArrayList();
        }
        _attributes.add(attr);
    }

    /**
     * @deprecated 1.1.9 代わりに{@link #getInformalPropertyClass()}を使う
     */
    public Class getPropertyClass() {
        return getInformalPropertyClass();
    }

    /**
     * @deprecated 1.1.9 代わりに{@link #getInformalPropertyClass()}を使う
     */
    public Class getExpectedClass() {
        return getInformalExpectedClass();
    }

    public Class getInformalPropertyClass() {
        return ProcessorProperty.class;
    }

    public Class getInformalExpectedClass() {
        return Object.class;
    }

    /**
     * VirtualPropertyもInformalPropertyとして扱います。
     * @param name プロパティ名。
     * @param property プロパティ値。
     */
    public void addVirtualProperty(PrefixAwareName name, Serializable attr) {
        addInformalProperty(name, attr);
    }

    /**
     * VirtualPropertyもInformalPropertyとして扱います。
     */
    public Class getVirtualPropertyClass() {
        return getInformalPropertyClass();
    }

    /**
     * VirtualPropertyもInformalPropertyとして扱います。
     */
    public Class getVirtualExpectedClass() {
        return getInformalExpectedClass();
    }

    public List getInformalProperties() {
        if (_attributes == null) {
            _attributes = new ArrayList();
        }
        return _attributes;
    }

    private PathPart getPathPart() {
        PathPart pathPart = _pathPart;
        if (pathPart == null) {
        	if (_path == null || StringUtil.isEmpty(_path.getValue().getScriptText())) {
        		return new PathPart();
        	}
       		String path = StringUtil.valueOf(_path.getValue().execute(null));
       		if (StringUtil.isEmpty(path)) {
       			return new PathPart();
       		}
       		pathPart = parsePath(path);
        }
        return pathPart;
    }

    private Page pathPartToPage(PathPart pathPart) {
    	if (pathPart._pageName == null) {
    		return null;
    	}
        if (pathPart._needAdjustPath) {
            // "/" 始まりでも "./" 始まりでもない場合は相対パス解決をする
            String sourcePath = EngineUtil.getSourcePath(getStaticParentProcessor());
            return ProviderUtil.getEngine().getPage(
                    StringUtil.adjustRelativePath(sourcePath, "./" + pathPart._pageName));
        }
        return ProviderUtil.getEngine().getPage(pathPart._pageName);
    }

    /**
     * pathを指定されていない場合(レイアウト側)はnullを返す。
     * pathが指定されている場合(コンポーネント利用)はEngineからPageを
     * 取得して返す。その際、必要ならばパスの自動解決をする。
     *
     * @return ページオブジェクト
     */
    protected Page getPage() {
    	return pathPartToPage(getPathPart());
    }

    protected Stack getRenderingParams() {
        return (Stack) CycleUtil.getLocalVariable(INSERT_PARAMS, this, null);
    }

    protected InsertRenderingParams pushRenderingParams() {
        InsertRenderingParams newParams = new InsertRenderingParams();
        return (InsertRenderingParams) getRenderingParams().push(newParams);
    }

    protected InsertRenderingParams peekRenderingParams() {
        return (InsertRenderingParams) getRenderingParams().peek();
    }

    protected InsertRenderingParams popRenderingParams() {
        return (InsertRenderingParams) getRenderingParams().pop();
    }

    public Map getRenderingParameters() {
        InsertRenderingParams params = peekRenderingParams();
        if (params.isRendering()) {
            return params.getParams();
        }
        return null;
    }

    protected void invokeBeforeRenderComponent(Page component) {
        InsertRenderingParams params = peekRenderingParams();
        params.setRendering(true);
        params.setStackComponent(PageImpl.getCurrentComponent());
        params.setCurrentComponent(component);
        PageImpl.setCurrentComponent(component);
        SpecificationUtil.execEvent(ProviderUtil.getEngine(), QM_BEFORE_RENDER_COMPONENT);
    }

    protected void invokeAfterRenderComponent() {
        InsertRenderingParams params = peekRenderingParams();
        PageImpl.setCurrentComponent(params.getCurrentComponent());
        SpecificationUtil.execEvent(ProviderUtil.getEngine(), QM_AFTER_RENDER_COMPONENT);
        PageImpl.setCurrentComponent(params.getStackComponent());
        params.setRendering(false);
        params.clear();
    }

    public ProcessStatus doStartProcess(Page topLevelPage) {
        PathPart pathPart = getPathPart();
        Page renderPage = pathPartToPage(pathPart);

        boolean fireEvent = true;

        // layoutからcontentを呼ぶ場合
        if (renderPage == null) {
            ServiceCycle cycle = CycleUtil.getServiceCycle();
            renderPage = topLevelPage;
            RequestScope request = cycle.getRequestScope();
            pathPart._suffix = request.getRequestedSuffix();
            pathPart._extension = request.getExtension();
            fireEvent = false;
        }

        if (renderPage == null) {
            throw new IllegalStateException();
        }

        Map properties = new LinkedHashMap(getInformalProperties().size());
        for (int i = 0; i < getInformalProperties().size(); i++) {
            Object object = getInformalProperties().get(i);
            if (object instanceof ProcessorProperty) {
                ProcessorProperty prop = (ProcessorProperty) object;
                properties.put(
                        prop.getName().getQName().getLocalName(),
                        prop.getValue().execute(null));
            }
        }
        Map params = pushRenderingParams().getParams();
        params.clear();
        params.putAll(properties);
        properties.clear();

        invokeBeforeRenderComponent(renderPage);
        try {
            ProcessStatus ret;
            getRenderingInsertChain().push(this);
            try {
            	String name;
            	if (_name == null) {
            		name = "";
            	} else {
            		name = StringUtil.valueOf(_name.getValue().execute(null));
            	}
                ComponentRenderer renderer = new ComponentRenderer(this, name);
                ret = RenderUtil.renderPage(fireEvent, renderer,
                        getVariables(), renderPage, pathPart._suffix, pathPart._extension);
            } finally {
                getRenderingInsertChain().pop();
            }
            if (ret == null) {
                Response response = CycleUtil.getResponse();
                if (response.getWriter().isDirty() == false) {
                    throw new RenderNotCompletedException(
                            renderPage.getPageName(), pathPart._extension);
                }
            }
            if (ret == ProcessStatus.EVAL_PAGE) {
                ret = ProcessStatus.SKIP_BODY;
            }
            return ret;
        } catch(RuntimeException e) {
            invokeAfterRenderComponent();
            throw e;
        }
    }

    protected static Stack getRenderingInsertChain() {
        return (Stack) CycleUtil.getGlobalVariable(RENDERING_INSERT_CHAIN, null);
    }

    public static InsertProcessor getRenderingCurrent() {
        if (getRenderingInsertChain().size() == 0) {
            return null;
        }
        return (InsertProcessor) getRenderingInsertChain().peek();
    }

    public ProcessStatus doEndProcess() {
        invokeAfterRenderComponent();
        popRenderingParams();
        return super.doEndProcess();
    }

    // for serialize

    private void writeObject(java.io.ObjectOutputStream out)
            throws java.io.IOException {
        out.defaultWriteObject();
    }

    private void readObject(java.io.ObjectInputStream in)
            throws java.io.IOException, ClassNotFoundException {
        in.defaultReadObject();
    }

}
