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

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;

import org.seasar.mayaa.cycle.ServiceCycle;
import org.seasar.mayaa.cycle.script.CompiledScript;
import org.seasar.mayaa.engine.specification.Namespace;
import org.seasar.mayaa.engine.specification.NodeAttribute;
import org.seasar.mayaa.engine.specification.NodeTreeWalker;
import org.seasar.mayaa.engine.specification.PrefixAwareName;
import org.seasar.mayaa.engine.specification.PrefixMapping;
import org.seasar.mayaa.engine.specification.QName;
import org.seasar.mayaa.engine.specification.Specification;
import org.seasar.mayaa.engine.specification.SpecificationNode;
import org.seasar.mayaa.engine.specification.URI;
import org.seasar.mayaa.impl.CONST_IMPL;
import org.seasar.mayaa.impl.cycle.CycleUtil;
import org.seasar.mayaa.impl.cycle.script.LiteralScript;
import org.seasar.mayaa.impl.cycle.script.ScriptUtil;
import org.seasar.mayaa.impl.provider.ProviderUtil;
import org.seasar.mayaa.impl.util.StringUtil;

/**
 * Specificationに関わるユーティリティメソッドをまとめたクラスです。
 *
 * @author Masataka Kurihara (Gluegent, Inc.)
 */
public class SpecificationUtil implements CONST_IMPL {

    private static EventScriptEnvironment _eventScripts =
        new EventScriptEnvironment();

    public static final PrefixMapping XML_DEFAULT_PREFIX_MAPPING =
        PrefixMappingImpl.getInstance("xml", CONST_IMPL.URI_XML);

    public static final PrefixMapping HTML_DEFAULT_PREFIX_MAPPING =
        PrefixMappingImpl.getInstance("", CONST_IMPL.URI_HTML);

    public static final PrefixMapping XHTML_DEFAULT_PREFIX_MAPPING =
        PrefixMappingImpl.getInstance("", CONST_IMPL.URI_XHTML);

    private SpecificationUtil() {
        // no instantiation.
    }

    /**
     * nodeの属性のうちqNameで表されるものの値を返します。
     * 見つからない場合はnullを返します。
     *
     * @param node 対象とするノード
     * @param qName 属性名
     * @return 属性値
     */
    public static String getAttributeValue(
            SpecificationNode node, QName qName) {
        NodeAttribute nameAttr = node.getAttribute(qName);
        if (nameAttr != null) {
            return nameAttr.getValue();
        }
        return null;
    }

    /**
     * currentのparentを辿り、Specificationが見つかった場合それを返します。
     * 見つからない場合はnullを返します。
     *
     * @param current 対象とするノード
     * @return Specification
     */
    public static Specification findSpecification(NodeTreeWalker current) {
        while (current instanceof Specification == false) {
            current = current.getParentNode();
            if (current == null) {
                return null;
            }
        }
        return (Specification) current;
    }

    /**
     * ServiceCycleで現在処理中のノードからparentを辿り、Specificationが
     * 見つかった場合それを返します。
     * 見つからない場合はnullを返します。
     *
     * @return Specification
     */
    public static Specification findSpecification() {
        ServiceCycle cycle = CycleUtil.getServiceCycle();
        NodeTreeWalker current = cycle.getOriginalNode();
        // リソースアクセス中にredirectしても対応できるようにする。
        if (current != null) {
            return findSpecification(current);
        }
        return null;
    }

    /**
     * currentが持つm:mayaaノードを探し、もしあればm:mayaaノードの属性のうち
     * qNameで表されるものの値を返します。
     * 見つからない場合はnullを返します。
     *
     * @param current 対象とするノード
     * @param qName 属性名
     * @return 属性値
     */
    public static SpecificationNode getMayaaNode(NodeTreeWalker current) {
        Specification specification = findSpecification(current);
        for (Iterator it = specification.iterateChildNode(); it.hasNext();) {
            SpecificationNode node = (SpecificationNode) it.next();
            if (node.getQName().equals(QM_MAYAA)) {
                return node;
            }
        }
        return null;
    }

    /**
     * currentが持つm:mayaaノードを探し、もしあればm:mayaaノードの属性のうち
     * qNameで表されるものの値を返します。
     * 見つからない場合はnullを返します。
     *
     * @param current 対象とするノード
     * @param qName 属性名
     * @return 属性値
     */
    public static String getMayaaAttributeValue(
            NodeTreeWalker current, QName qName) {
        SpecificationNode mayaa = getMayaaNode(current);
        if (mayaa != null) {
            String value = getAttributeValue(mayaa, qName);
            if (value != null) {
                return value;
            }
        }
        return null;
    }

    /**
     * nodeの子要素である文字ノードをまとめた文字列を取得します。
     * 文字ノード以外を含む場合、{@link IllegalChildNodeException} が発生します。
     * CDATAはその子を文字ノードと見なします。
     *
     * @param node 文字ノードを子として持つノード
     * @return 文字ノードをまとめた文字列
     * @throws IllegalChildNodeException 文字ノード以外を含む場合
     */
    public static String getNodeBodyText(SpecificationNode node) {
        StringBuffer buffer = new StringBuffer(256);
        for (Iterator it = node.iterateChildNode(); it.hasNext();) {
            SpecificationNode child = (SpecificationNode) it.next();
            QName qName = child.getQName();
            if (QM_CDATA.equals(qName)) {
                buffer.append(getNodeBodyText(child));
            } else if (QM_CHARACTERS.equals(qName)) {
                buffer.append(SpecificationUtil.getAttributeValue(
                        child, QM_TEXT));
            } else {
                String name = child.getPrefix() + ":" + qName.getLocalName();
                throw new IllegalChildNodeException(name);
            }
        }
        return buffer.toString();
    }

    /**
     * スクリプト実行環境のスコープを初期化します。
     */
    public static void initScope() {
        ProviderUtil.getScriptEnvironment().initScope();
    }

    /**
     * スクリプト実行環境の新規スコープを開始します。
     * @param variables 初期変数
     */
    public static void startScope(Map variables) {
        ProviderUtil.getScriptEnvironment().startScope(variables);
    }

    /**
     * スクリプト実行環境の現在スコープを終了します。
     */
    public static void endScope() {
        ProviderUtil.getScriptEnvironment().endScope();
    }

    public static void execEvent(Specification spec, QName eventName) {
        if (eventName == null) {
            throw new IllegalArgumentException();
        }
        SpecificationNode mayaa = getMayaaNode(spec);
        if (mayaa != null) {
        	if (_eventScripts.isCached(spec, eventName)) {
        		_eventScripts.execEventScripts(spec, eventName);
        	} else {
        		List eventChilds = new ArrayList();
	            for (Iterator it = mayaa.iterateChildNode(); it.hasNext();) {
	                SpecificationNode child = (SpecificationNode) it.next();
	                if (eventName.equals(child.getQName())) {
	                	eventChilds.add(child);
	                }
	            }
        		_eventScripts.execEventScriptsAndCache(spec, eventName, eventChilds);
        	}
        }
    }

    // factory methods ----------------------------------------------

    public static Namespace createNamespace() {
        return new NamespaceImpl();
    }

    public static Namespace getFixedNamespace(Namespace original) {
        return original;
        //return NamespaceImpl.getInstance(original);
    }

    public static QName createQName(String localName) {
        return createQName(URI_MAYAA, localName);
    }

    public static QName createQName(
            URI namespaceURI, String localName) {
        return QNameImpl.getInstance(namespaceURI, localName);
    }

    public static PrefixMapping createPrefixMapping(
            String prefix, URI namespaceURI) {
        return PrefixMappingImpl.getInstance(prefix, namespaceURI);
    }

    public static PrefixMapping createPrefixMapping(
            String prefixAndNamespaceURI) {
        return PrefixMappingImpl.revertStringToMapping(prefixAndNamespaceURI);
    }

    public static QName parseQName(String qName) {
        if (StringUtil.hasValue(qName) && qName.charAt(0) == '{') {
            int end = qName.indexOf('}');
            if (end != -1 && end < qName.length() - 1) {
                String namespaceURI = qName.substring(1, end).trim();
                String localName = qName.substring(end + 1).trim();
                if (StringUtil.hasValue(namespaceURI)
                        && StringUtil.hasValue(localName)) {
                    return QNameImpl.getInstance(createURI(namespaceURI), localName);
                }
            }
        }
        throw new IllegalArgumentException(qName);
    }

    public static PrefixAwareName createPrefixAwareName(QName qName) {
        return createPrefixAwareName(qName, "");
    }

    public static PrefixAwareName createPrefixAwareName(QName qName, String prefix) {
        return PrefixAwareNameImpl.getInstance(qName, prefix);
    }

    public static SpecificationNode createSpecificationNode(
            QName qName, String systemID, int lineNumber,
            boolean onTemplate, int sequenceID) {
        SpecificationNodeImpl node = new SpecificationNodeImpl(qName);
        node.setSequenceID(sequenceID);
        node.setSystemID(systemID);
        node.setLineNumber(lineNumber);
        node.setOnTemplate(onTemplate);
        return node;
    }

    public static URI createURI(String uri) {
        return URIImpl.getInstance(uri);
    }

    // script cache ----------------------------------------------

    protected static class EventScriptEnvironment {

		// WeakHashMap<Specification, Map<QName, List<CompiledScript>>>
        private WeakHashMap _mayaaScriptCache = new WeakHashMap();

        protected CompiledScript compile(String text) {
            if (StringUtil.hasValue(text)) {
                ScriptUtil.assertSingleScript(text);
                return ScriptUtil.compile(text, Void.class);
            }
            return LiteralScript.NULL_LITERAL_SCRIPT;
        }

        protected List getEventScripts(Specification spec, QName eventName) {
        	Map events = (Map)_mayaaScriptCache.get(spec);
        	if (events == null) {
        		return null;
        	}
        	return (List)events.get(eventName);
        }

        public boolean isCached(Specification spec, QName eventName) {
        	return getEventScripts(spec, eventName) != null;
        }

        public boolean execEventScriptsAndCache(
        		Specification spec, QName eventName, List eventChilds) {
        	if (eventChilds == null) {
        		throw new NullPointerException();
        	}
        	Map events = (Map)_mayaaScriptCache.get(spec);
        	if (events == null) {
        		events = new HashMap();
        		_mayaaScriptCache.put(spec, events);
        	}
        	List scripts = new ArrayList();

            ServiceCycle cycle = CycleUtil.getServiceCycle();
            for (Iterator it = eventChilds.iterator(); it.hasNext();) {
                SpecificationNode child = (SpecificationNode) it.next();
                NodeTreeWalker save = cycle.getInjectedNode();
                try {
                    cycle.setInjectedNode(child);
                    String bodyText = getNodeBodyText(child);
                    bodyText = ScriptUtil.getBlockSignedText(bodyText);
                    CompiledScript script = compile(bodyText);
                    scripts.add(script);
                    script.execute(null);
                } finally {
                    cycle.setInjectedNode(save);
                }
            }
            if (scripts.size() == 0) {
            	events.put(eventName, Collections.EMPTY_LIST);
            } else {
            	events.put(eventName, scripts);
            }
        	return true;
        }

        public boolean execEventScripts(
        		Specification spec, QName eventName) {
        	List scripts = getEventScripts(spec, eventName);
        	if (scripts == null) {
        		return false;
        	}
            ServiceCycle cycle = CycleUtil.getServiceCycle();
            for (Iterator it = scripts.iterator(); it.hasNext();) {
                NodeTreeWalker save = cycle.getInjectedNode();
                try {
                    ((CompiledScript) it.next()).execute(null);
                } finally {
                    cycle.setInjectedNode(save);
                }
            }
            return true;
        }

    }

    public static Namespace copyNamespace(Namespace original) {
        return NamespaceImpl.copyOf(original);
    }

    public static URI getDefaultTemplateNamespace(String systemID) {
        return null;
    }

}
