/*
 * This software is distributed under following license based on modified BSD
 * style license.
 * ----------------------------------------------------------------------
 * 
 * Copyright 2009 The Nimbus2 Project. All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer. 
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE NIMBUS PROJECT ``AS IS'' AND ANY EXPRESS
 * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
 * NO EVENT SHALL THE NIMBUS PROJECT OR CONTRIBUTORS BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * 
 * The views and conclusions contained in the software and documentation are
 * those of the authors and should not be interpreted as representing official
 * policies, either expressed or implied, of the Nimbus2 Project.
 */
package jp.ossc.nimbus.service.writer;

import java.lang.reflect.InvocationTargetException;
import java.util.*;
import java.io.Serializable;

import jp.ossc.nimbus.beans.*;
import jp.ossc.nimbus.core.*;
import jp.ossc.nimbus.lang.*;
import jp.ossc.nimbus.service.log.Logger;

/**
 * 任意のBeanをWritableRecordに変換する{@link WritableRecordFactory}サービス。<p>
 *
 * @author Y.Tokuda
 */
public class DefaultWritableRecordFactoryService extends ServiceBase
 implements WritableRecordFactory,
            DefaultWritableRecordFactoryServiceMBean,
            Serializable{
    
    private static final long serialVersionUID = 5249532509800152052L;
    
    private static final String ITERATE_SUFFIX = "*";
    
    /** フォーマット定義文字列 */
    protected String format;
    
    /** フォーマット定義文字列から生成されるParsedElement型のArrayList */
    protected List<ParsedElement> parsedElements;
    
    /** WritableElementの実装クラス名ハッシュ */
    protected Map<String, Class<WritableElement>> writableElementClassMap;
    
    /** WritableElementの実装サービス名ハッシュ */
    protected Map<String, ServiceName> writableElementServiceNameMap;
    
    protected Properties formatKeyMappings;
    protected Map<String, Property> formatKeyPropertyMap;
    
    protected Map<String, Properties> iterateFormatKeyMappings;
    protected Map<String, Map<String, Property>> iterateformatKeyPropertyMap;
    
    protected Map<String, String> iterateFormats;
    protected Map<String, List<ParsedElement>> iterateFormatMap;
    
    // WritableRecordFactoryServiceMBeanのJavaDoc
    public void setWritableElementClass(String key, Class<WritableElement> clazz){
        writableElementClassMap.put(key, clazz);
    }
    
    // WritableRecordFactoryServiceMBeanのJavaDoc
    public Class<WritableElement> getWritableElementClass(String key){
        return writableElementClassMap == null ? null : writableElementClassMap.get(key);
    }
    
    // WritableRecordFactoryServiceMBeanのJavaDoc
    public void setWritableElementServiceName(String key, ServiceName name){
        writableElementServiceNameMap.put(key, name);
    }
    
    // WritableRecordFactoryServiceMBeanのJavaDoc
    public ServiceName getWritableElementServiceName(String key){
        return writableElementServiceNameMap == null ? null : writableElementServiceNameMap.get(key);
    }
    
    // WritableRecordFactoryServiceMBeanのJavaDoc
    public void setFormat(String fmt){
        format = fmt;
    }
    
    // WritableRecordFactoryServiceMBeanのJavaDoc
    public String getFormat(){
        return format;
    }
    
    // WritableRecordFactoryServiceMBeanのJavaDoc
    public void setFormatKeyMapping(Properties mapping){
        formatKeyMappings = mapping;
    }
    // WritableRecordFactoryServiceMBeanのJavaDoc
    public Properties getFormatKeyMapping(){
        return formatKeyMappings;
    }
    
    // WritableRecordFactoryServiceMBeanのJavaDoc
    public void setFormatKeyProperty(String key, Property prop){
        prop.setIgnoreNullProperty(true);
        formatKeyPropertyMap.put(key, prop);
    }
    // WritableRecordFactoryServiceMBeanのJavaDoc
    public Property getFormatKeyProperty(String key){
        return formatKeyPropertyMap == null ? null : formatKeyPropertyMap.get(key);
    }
    
    // WritableRecordFactoryServiceMBeanのJavaDoc
    public void setIterateFormatKeyMapping(String key, Properties mapping){
        iterateFormatKeyMappings.put(key, mapping);
    }
    // WritableRecordFactoryServiceMBeanのJavaDoc
    public Properties getIterateFormatKeyMapping(String key){
        return iterateFormatKeyMappings.get(key);
    }
    
    // WritableRecordFactoryServiceMBeanのJavaDoc
    public void setIterateFormat(String key, String format){
        iterateFormats.put(key, format);
    }
    // WritableRecordFactoryServiceMBeanのJavaDoc
    public String getIterateFormat(String key){
        return (String)iterateFormats.get(key);
    }
    
    /**
     * 生成処理を行う。<p>
     *
     * @exception Exception サービスの生成処理に失敗した場合
     */
    public void createService() throws Exception {
        writableElementClassMap = new HashMap<String, Class<WritableElement>>();
        writableElementServiceNameMap = new HashMap<String, ServiceName>();
        formatKeyPropertyMap = new HashMap<String, Property>();
        iterateFormatKeyMappings = new HashMap<String, Properties>();
        iterateformatKeyPropertyMap = new HashMap<String, Map<String, Property>>();
        iterateFormats = new HashMap<String, String>();
        iterateFormatMap = new HashMap<String, List<ParsedElement>>();
    }
    
    /**
     * 開始処理を行う。<p>
     *
     * @exception Exception サービスの開始処理に失敗した場合
     */    
    public void startService()throws Exception{
        
        //与えられた実装クラス名でインスタンスを生成できるかどうか確認
        Collection<Class<WritableElement>> implClasses = writableElementClassMap.values();
        for(Class<WritableElement> implClass : implClasses){
            implClass.newInstance();
        }
        
        parsedElements = parseFormat(format);
        
        if(formatKeyMappings != null && formatKeyMappings.size() != 0){
            formatKeyPropertyMap.clear();
            for(Map.Entry<Object, Object> entry : formatKeyMappings.entrySet()){
                final String key = (String)entry.getKey();
                final String prop = (String)entry.getValue();
                if(prop != null && prop.length() != 0){
                    Property property = PropertyFactory.createProperty(prop);
                    property.setIgnoreNullProperty(true);
                    formatKeyPropertyMap.put(key, property);
                }
            }
        }
        
        if(iterateFormats.size() != 0){
            for(Map.Entry<String, String> entry : iterateFormats.entrySet()){
                final String itrKey = entry.getKey();
                final String format = entry.getValue();
                iterateFormatMap.put(itrKey, parseFormat(format));
                if(!iterateFormatKeyMappings.containsKey(itrKey)){
                    throw new IllegalArgumentException(
                        "IterateFormatKeyMapping that corresponds to \"" + itrKey + "\" is not specified."
                    );
                }
            }
        }
        if(iterateFormatKeyMappings.size() != 0){
            for(Map.Entry<String, Properties> entry : iterateFormatKeyMappings.entrySet()){
                final String itrKey = entry.getKey();
                final Properties props = entry.getValue();
                final Map<String, Property> mapping = new HashMap<String, Property>();
                for(Map.Entry<Object, Object> entry2 : props.entrySet()){
                    final String key = (String)entry2.getKey();
                    final String prop = (String)entry2.getValue();
                    if(prop != null && prop.length() != 0){
                        Property property = PropertyFactory.createProperty(prop);
                        property.setIgnoreNullProperty(true);
                        mapping.put(key, property);
                    }
                }
                iterateformatKeyPropertyMap.put(itrKey, mapping);
            }
        }
    }
    
    /**
     * 停止処理を行う。<p>
     *
     * @exception Exception サービスの停止処理に失敗した場合
     */    
    public void stopService() throws Exception{
        parsedElements = null;
        iterateformatKeyPropertyMap.clear();
        iterateFormatMap.clear();
    }
    /**
     * 破棄処理を行う。<p>
     *
     * @exception Exception サービスの破棄処理に失敗した場合
     */            
    public void destroyService() throws Exception{
        writableElementClassMap = null;
        writableElementServiceNameMap = null;
        formatKeyPropertyMap = null;
        iterateformatKeyPropertyMap = null;
        iterateFormats = null;
        iterateFormatMap = null;
    }
    
    /**
     * 指定された出力要素のMapから{@link WritableRecord}を生成する。<p>
     * {@link #setFormat(String)}で指定されたフォーマットに従って、{@link WritableElement}を生成し、それを順次格納したWritableRecordを返す。<br>
     * setFormat(String)で指定されたフォーマット内に、キーが指定されている場合は、出力要素のMapから値を取り出して、{@link #setWritableElementClass(String, Class)}または{@link #setWritableElementServiceName(String, ServiceName)}でマッピングされたWritableElementインスタンスに格納する。<br>
     * 
     * @param elements 出力要素のMapオブジェクト
     * @return MessageWriterサービスの入力オブジェクトとなるWritableRecord
     */
    public WritableRecord createRecord(Object elements){
        if(getState() != State.STARTED){
            throw new IllegalServiceStateException(this);
        }
        
        final WritableRecord record = new WritableRecord();
        if(parsedElements == null || parsedElements.size() == 0){
            for(String key : getElementKeys(elements)){
                final WritableElement element = createElement(
                    key,
                    getElementValue(key, elements, formatKeyPropertyMap)
                );
                record.addElement(element);
            }
        }else{
            for(int i = 0, imax = parsedElements.size(); i < imax; i++){
                final ParsedElement parsedElement = parsedElements.get(i);
                if(parsedElement.isKeyElement()){
                    final String key = parsedElement.getValue();
                    final WritableElement element = createElement(
                        key,
                        getElementValue(key, elements, formatKeyPropertyMap)
                    );
                    if(element != null){
                        record.addElement(element);
                    }
                }else{
                    final SimpleElement element = new SimpleElement();
                    element.setValue(parsedElement.getValue());
                    record.addElement(element);
                }
            }
        }
        return record;
    }
    
    /**
     * 指定された出力要素のMapが持つキー名の集合を取得する。<p>
     *
     * @param elements 出力要素のマップ
     * @return キー名の集合
     */
    @SuppressWarnings("unchecked")
    protected Set<String> getElementKeys(Object elements){
        if(elements instanceof Map){
            return ((Map<String, Object>)elements).keySet();
        }else{
            return SimpleProperty.getPropertyNames(elements);
        }
    }
    
    /**
     * 指定された出力要素のマップから、指定されたキーの値を取得する。<p>
     *
     * @param key 出力要素マップ内のキー
     * @param elements 出力要素マップ
     * @param propMapping キーとプロパティのマッピング
     * @return 出力要素内のキーに該当する値
     */
    @SuppressWarnings("unchecked")
    protected Object getElementValue(
        String key,
        Object elements,
        Map<String, Property> propMapping
    ){
        if(propMapping != null && propMapping.containsKey(key)){
            final Logger logger = getLogger();
            final Property prop = propMapping.get(key);
            try{
                return prop.getProperty(elements);
            }catch(NoSuchPropertyException e){
                return null;
            }catch(InvocationTargetException e){
                logger.write("DWRF_00001", e, key, prop);
                return null;
            }
        }else if(elements instanceof Map){
            return ((Map<Object,Object>)elements).get(key);
        }else{
            return elements;
        }
    }
    
    /**
     * 出力要素から指定されたキーの値を取り出して、対応する{@link WritableElement}を生成する。<p>
     * <br>
     * 
     * @param key キー
     * @param val 出力要素
     * @return WritableElementインスタンス
     */
    @SuppressWarnings("unchecked")
    protected WritableElement createElement(
        String key,
        Object val
    ){
        if(key.endsWith(ITERATE_SUFFIX)){
            Object[] array = null;
            if(val == null){
                return createSingleElement(key, val);
            }else if(val instanceof Collection){
                Collection<Object> col = (Collection<Object>)val;
                array = col.toArray();
            }else if(val.getClass().isArray()){
                array = (Object[])val;
            }else{
                return createSingleElement(key, val);
            }
            if(array == null || array.length == 0){
                return null;
            }
            IterateWritableElement itrElement = new IterateWritableElement(key);
            if(iterateFormatMap.containsKey(key)){
                final List<ParsedElement> parsedElements
                     = iterateFormatMap.get(key);
                final Map<String, Property> itrPropMapping
                     = iterateformatKeyPropertyMap.get(key);
                for(int i = 0; i < array.length; i++){
                    for(int j = 0, jmax = parsedElements.size(); j < jmax; j++){
                        final ParsedElement parsedElem = parsedElements.get(j);
                        WritableElement element = null;
                        if(parsedElem.isKeyElement()){
                            final String itrElementKey = parsedElem.getValue();
                            element = createSingleElement(
                                itrElementKey,
                                getElementValue(
                                    itrElementKey,
                                    array[i],
                                    itrPropMapping
                                )
                            );
                        }else{
                            element = new SimpleElement(parsedElem.getValue());
                        }
                        if(element != null){
                            itrElement.addElement(element);
                        }
                    }
                }
            }else{
                for(int i = 0; i < array.length; i++){
                    final WritableElement element
                         = createSingleElement(key, array[i]);
                    if(element != null){
                        itrElement.addElement(element);
                    }
                }
            }
            return itrElement;
        }else{
            return createSingleElement(key, val);
        }
    }
    
    /**
     * 出力要素から指定されたキーの値を取り出して、対応する{@link WritableElement}を生成する。<p>
     * {@link #getElementValue(String, Object, Map)}で、出力要素から指定されたキーの値を取り出す。{@link #getWritableElementClass(String)}または{@link #getWritableElementServiceName(String)}で、キーに該当する{@link WritableElement}を特定し、そのインスタンスを取得して、キーと値を設定して返す。<br>
     * キーに該当する{@link WritableElement}が特定できない場合は、{@link SimpleElement}クラスを使用する。<br>
     * 
     * @param key キー
     * @param val 出力要素
     * @return WritableElementインスタンス
     */
    protected WritableElement createSingleElement(String key, Object val){
        WritableElement element = null;
        
        final Class<WritableElement> elementClass = getWritableElementClass(key);
        if(elementClass != null){
            try{
                element = elementClass.newInstance();
            }catch(IllegalAccessException e){
                // 起きないはず
                element = new SimpleElement();
            }catch(InstantiationException e){
                // 起きないはず
                element = new SimpleElement();
            }
        }else{
            final ServiceName elementServiceName = getWritableElementServiceName(key);
            if(elementServiceName != null){
                try{
                    element = (WritableElement)ServiceManagerFactory
                        .getServiceObject(elementServiceName);
                }catch(ServiceNotFoundException e){
                    element = new SimpleElement();
                }
            }else{
                element = new SimpleElement();
            }
        }
        
        element.setKey(key);
        element.setValue(val);
        
        return element;
    }
    
    /**
     * フォーマット文字列パースメソッド。<p>
     * setFormat(String)で、セットされたフォーマット文字列をパースし、ParsedElementのListを返す
     * <ol>
     * <li>'%'で囲んだ文字列をキーワードと認識する。</li>
     * <li>'\%'記述すると、文字列の'%'として認識される。</li>
     * <li>'\\'と記述すると、文字列の'\'として認識される。</li>
     * <li>フォーマット文字列がnullまたは空文字のとき、nullを返す。</li>
     * <li>フォーマット文字列において、"%"で始めたキーワードの記述が%で閉じられていない場合、IllegalArgumentExceptionをthrowする。</li>
     * <li>(例) フォーマット文字列が"今日は %D% です。"の場合、3つのParsedElementを含むListが返却される。</li> 
     *         <ul>
     *            <li>最初のParsedElementのmValueは"今日は "、mIsKeyWordはfalse</li>
     *            <li>2番目のParsedElementのmValueは"D"、mIsKeyWordはtrue</li>
     *            <li>3番目のParsedElementのmValueは" です。"、mIsKeyWordはfalse</li>
     *         </ul>
     * <li>フォーマット文字列がnullもしくは空文字の場合、nullを返す。</li>
     * </ol>
     * 
     * @return ParsedElementのList
     */
    protected List<ParsedElement> parseFormat(String format){
        //初期化
        if(format == null || format.length() == 0){
            //nullを返す
            return null;
        }
        final List<ParsedElement> result = new ArrayList<ParsedElement>();
        final StringBuilder word = new StringBuilder();
        boolean isStartKey = false;
        boolean isEscape = false;
        for(int i = 0, max = format.length(); i < max; i++){
            final char c = format.charAt(i);
            switch(c){
            case '%':
                if(isEscape){
                    word.append(c);
                    isEscape = false;
                }else if(isStartKey){
                    if(word.length() != 0){
                        //キーワードとして追加
                        final ParsedElement elem = new ParsedElement(
                            word.toString(),
                            true
                        );
                        result.add(elem);
                        word.setLength(0);
                        isStartKey = false;
                    }else{
                        throw new IllegalArgumentException(
                            "Keyword must not be null. : " + format
                        );
                    }
                }else{
                    if(word.length() > 0){
                        //固定メッセージとして追加
                        final ParsedElement elem = new ParsedElement(
                            word.toString(),
                            false
                        );
                        result.add(elem);
                        word.setLength(0);
                    }
                    isStartKey = true;
                }
                break;
            case '\\':
                if(isEscape){
                    word.append(c);
                    isEscape = false;
                }else{
                    isEscape = true;
                }
                break;
            default:
                if(isEscape){
                    throw new IllegalArgumentException(
                        "'\' is escape character. : " + format
                    );
                }else{
                    word.append(c);
                }
                break;
            }
        }
        if(isEscape){
            throw new IllegalArgumentException(
                "'\' is escape character. : " + format
            );
        }
        if(isStartKey){
            throw new IllegalArgumentException(
                "'%' is key separator character. : " + format
            );
        }
        if(word.length() > 0){
            //固定メッセージとして追加
            final ParsedElement elem = new ParsedElement(
                word.toString(),
                false
            );
            result.add(elem);
            word.setLength(0);
        }
        return result;
    }
    
    /**
     * パースされたフォーマットの要素を表すクラス。<p>
     * Stringの値と、keyか否かの識別(boolean)値を持つ。
     * 
     * @author Y.Tokuda
     */
    protected static class ParsedElement implements Serializable{
        
        private static final long serialVersionUID = 6554326776504636150L;
        
        //メンバ変数
        protected String value;
        protected boolean isKeyElement;
        
        /**
         * コンストラクタ。<p>
         *
         * @param val フォーマットの要素文字列
         * @param isKey キーかどうかを示すフラグ
         */
        public ParsedElement(String val, boolean isKey){
            value = val;
            isKeyElement = isKey;
        }
        
        /**
         * フォーマットの要素文字列を取得する。<p>
         *
         * @return フォーマットの要素文字列
         */
        public String getValue(){
            return value;
        }
        
        /**
         * この要素がキーかどうかを判定する。<p>
         *
         * @return この要素がキーの場合true
         */
        public boolean isKeyElement(){
            return isKeyElement;
        }
    }
}
