/*
 * Copyright 2009 the Stormcat Project.
 *
 * 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.stormcat.jvbeans.jvlink.analyze;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

import org.apache.commons.lang.ArrayUtils;
import org.stormcat.commons.constants.Charset;
import org.stormcat.commons.lang.EnumUtil;
import org.stormcat.commons.lang.StringUtil;
import org.stormcat.commons.reflect.ClassUtil;
import org.stormcat.commons.reflect.MethodUtil;
import org.stormcat.jvbeans.config.RecordTypeId;
import org.stormcat.jvbeans.exception.JvBeansRuntimeException;
import org.stormcat.jvbeans.jvlink.JvBindingDto;
import org.stormcat.jvbeans.util.JvStringUtil;


/**
 * JV-Dataの文字列からJavaBeansへのバインディングを行うファクトリです。
 * @author a.yamada
 *
 */
public class JvBindingDtoFactory {
    
    private JvBeansContainer jvBeansContainer;
    
    /**
     * 
     * コンストラクタ
     */
    public JvBindingDtoFactory() { }
    
    /**
     * JV-Dataの1レコードをDTOにバインディングしたものを返します。
     * <p>取得した{@link JvBindingDto}は利用側でキャストする必要があります。
     * @param str JV-Data文字列
     * @return バインディングDTO
     * @throws IllegalArgumentException レコードが{@code null}の場合
     * @throws JvBeansRuntimeException レコードからレコード種別IDが取得できなかった場合
     * @throws IllegalStateException JvBeansContainerがプロパティとして設定されていない場合
     */
    public JvBindingDto create(String str) {
        if (StringUtil.isBlank(str)) {
            throw new IllegalArgumentException("文字列が空です");
        }

        RecordTypeId recordTypeId = EnumUtil.getEnum(RecordTypeId.class, str.substring(0, 2));
        if (recordTypeId == null) {
            throw new JvBeansRuntimeException("レコード種別IDを取得できません。");
        }
        
        if (jvBeansContainer == null) {
            throw new IllegalStateException("JVBindingDtoFactoryのプロパティ(jvBeansContainer)がnullになっています。");
        }
        
        JvBindingDto dto = JvBindingDto.class.cast(ClassUtil.newInstance(jvBeansContainer.getMappedType(recordTypeId)));
        return createDto(str, recordTypeId, dto);
    }

    /**
     * JV-Dataの1レコードを指定したDTOにバインディングしたものを返します。
     * @param str JV-Data文字列
     * @param clazz バインディング型
     * @return バインディングDTO
     * @throws IllegalArgumentException レコードが{@code null}の場合
     * @throws JvBeansRuntimeException レコードからレコード種別IDが取得できなかった場合
     * @throws IllegalStateException JvBeansContainerがプロパティとして設定されていない場合
     */
    public <T extends JvBindingDto> T create(String str, Class<T> clazz) {
        if (StringUtil.isBlank(str)) {
            throw new IllegalArgumentException("文字列が空です");
        }

        RecordTypeId recordTypeId = EnumUtil.getEnum(RecordTypeId.class, str.substring(0, 2));
        if (recordTypeId == null) {
            throw new JvBeansRuntimeException("レコード種別IDを取得できません。");
        }
        
        if (jvBeansContainer == null) {
            throw new IllegalStateException("JVBindingDtoFactoryのプロパティ(jvBeansContainer)がnullになっています。");
        }
        
        T dto = ClassUtil.newInstance(clazz);
        return createDto(str, recordTypeId, dto);
    }
    
    private <T extends JvBindingDto> T createDto(String str, RecordTypeId recordTypeId, T dto) {
        byte[] bt = StringUtil.getBytes(str, Charset.MS932);
        for (JvRecordMeta meta : jvBeansContainer.getRecordMetaItems(recordTypeId)) {
            byte[] parts = null;
            List<JvRecordMeta> joinItems = meta.getJoinMetaItems();
            if (meta.getRepeatCount() == 1) {
                parts = subarray(bt, meta.getPosition(), meta.getByteLength());
                setProperty(parts, meta, dto);
            } else {
                List<Object> list = new ArrayList<Object>();
                int offset = meta.getPosition() - 1;
                for (int i = 0; i < meta.getRepeatCount(); i++) {
                    Object child = null;
                    if (joinItems == null) {
                        parts = subarray(bt, i * meta.getByteLength() + meta.getPosition(), meta.getByteLength());
                        child = convert(parts, meta);
                    } else {
                        child = ClassUtil.newInstance(meta.getType());
                        for (JvRecordMeta childMeta : joinItems) {
                            parts = subarray(bt, offset + childMeta.getPosition(), childMeta.getByteLength());
                            setProperty(parts, childMeta, child);
                        }                           
                    }
                    offset += meta.getByteLength();
                    list.add(child);
                }
                MethodUtil.invoke(meta.getMutator(), dto, null, list);
            }
        }
        dto.setLine(str);
        return dto;
    }
    
    private void setProperty(byte[] data, JvRecordMeta meta, Object target) {
        Object value = convert(data, meta);
        Method mutator = meta.getMutator();
        if (mutator == null) {
            throw new JvBeansRuntimeException("セッターメソッドがありません。");
        }
        MethodUtil.invoke(mutator, target, null, value);        
    }
    
    private Object convert(byte[] data, JvRecordMeta meta) {
        String rawData = JvStringUtil.trim(StringUtil.getString(data, Charset.MS932));
        return JvBindingObjectConverter.convert(rawData, meta);
    }

    private byte[] subarray(byte[] data, int position, int byteLength) {
        return ArrayUtils.subarray(data, position - 1, position + byteLength - 1);
    }

    /**
     * {@link JvBeansContainer}をセットします。
     * @param jvBeansContainer {@link JvBeansContainer}
     */
    public void setJvBeansContainer(JvBeansContainer jvBeansContainer) {
        this.jvBeansContainer = jvBeansContainer;
    }

}
