/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package textkeymatcher.entity;

import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
import textkeymatcher.entity.KeyMatchedRowMap.RowKey;
import textkeymatcher.entity.KeyMatchedRowMap.RowValues;
import textkeymatcher.service.KeyMatcher;

/**
 * 各カラムデータと、キーによるマッチングを行い、キーと値リストの組にする.<br>
 * @author seraphy
 */
public class KeyMatchedRowMap extends AbstractMap<RowKey, RowValues> {

    /**
     * データソースのコレクション.<br>
     */
    private ArrayList<LineDataList> lineDataLists = new ArrayList<LineDataList>();
    
    
    /**
     * 行のキー表現
     */
    public static class RowKey implements Comparable<RowKey> {
        
        /**
         * キー判定方法
         */
        private final KeyMatcher keyMatcher;
        
        /**
         * キーの生データ
         */
        private final String key;

        
        public RowKey(KeyMatcher keyMatcher, String key) {
            if (keyMatcher == null) {
                throw new IllegalArgumentException();
            }

            this.keyMatcher = keyMatcher;
            this.key = key;
        }
        
        public String getKey() {
            return key;
        }

        @Override
        public int compareTo(RowKey t) {
            String okey = t.key;
            return keyMatcher.compare(key, okey);
        }
        
        @Override
        public int hashCode() {
            return keyMatcher.hashCode(key);
        }
        
        @Override
        public boolean equals(Object obj) {
            if (obj == this) {
                return true;
            }
            if (obj != null && obj instanceof RowKey) {
                RowKey rk = (RowKey) obj;
                if (rk.keyMatcher != keyMatcher) {
                    return false;
                }
                return keyMatcher.compare(key, rk.key) == 0;
            }
            return false;
        }
        
        /**
         * キーのマッチング法に従った文字列表現
         * @return 文字列表現
         */
        @Override
        public String toString() {
            return keyMatcher.getNormalize(key);
        }
    }
    
    
    /**
     * データソースごとの値のリスト.<br>
     */
    public static class RowValues {
        
        /**
         * データソースごとのデータを入れる配列.<br>
         * データソースごとに添字を指定する.<br>
         * 単一データであれば文字列が入り、複数データであれば文字列のリストオブジェクトが入る.<br>
         * 該当がなければ、nullとなる.<br>
         */
        private Object[] datas;

        /**
         * データソース数を指定して構築する.
         * @param dataWidth データソース数
         */
        public RowValues(int dataWidth) {
            this.datas = new Object[dataWidth];
        }
        
        /**
         * データソース数を取得する
         * @return データソース数
         */
        public int getDataWidth() {
            return datas.length;
        }
        
        /**
         * データソースと、値を指定してデータをセットする.<br>
         * 同じデータソースですでにデータがある場合はリストとして追加される.<br>
         * @param column データソース添字
         * @param value データ、nullは空文字として登録される
         */
        public void add(int column, String value) {
            if (value == null) {
                value = "";
            }
            if (datas[column] == null) {
                // 新規の場合
                datas[column] = value;
            
            } else if (datas[column] instanceof List) {
                // すでに2要素以上ある場合
                @SuppressWarnings("unchecked")
                List<String> lst = (List<String>) datas[column];
                lst.add(value);
            
            } else {
                // すでに存在する場合はリストにつめ直す.
                assert datas[column] instanceof String;
                List<String> tmp = new ArrayList<String>();
                tmp.add((String) datas[column]);
                tmp.add(value);
                datas[column] = tmp;
            }
        }
        
        /**
         * 各データソースの中の最大のデータ数.<br>
         * すべてのデータソースにひとつもデータがなければ0となる.<br>
         * @return 最大のデータ数
         */
        public int getMaxCount() {
            int mx = 0;
            int dataWidth = datas.length;
            for (int dataIndex = 0; dataIndex < dataWidth; dataIndex++) {
                int count = getCount(dataIndex);
                if (count > mx) {
                    mx = count;
                }
            }
            return mx;
        }
        
        /**
         * 指定したデータソースのデータの個数を返す.
         * @param column 添字
         * @return データの個数、未登録ならば0
         */
        public int getCount(int column) {
            if (column < 0 || column >= datas.length) {
                // 範囲外、データがないので0を返す.
                return 0;
            }
            Object data = datas[column];
            if (data != null && data instanceof List) {
                // リスト格納であれば、要素数を返す
                @SuppressWarnings("unchecked")
                List<String> lst = (List<String>) datas[column];
                return lst.size();
            }
            // 非nullで、リストでなければ単一要素なので1を返す
            if (data != null) {
                return 1;
            }
            // nullであればデータは未設定なので0
            return 0;
        }
        
        /**
         * 指定したデータソースの指定した番号のデータを取り出す.<br>
         * 範囲外、もしくはデータ未登録である場合は空文字が返される.<br>
         * @param column データソース番号
         * @param rowIndex データのインデックス、getCountで得られる個数分
         * @return データ、無し、もしくは範囲外の場合は空文字
         */
        public String get(int column, int rowIndex) {
            if (column < 0 || column >= datas.length) {
                return "";
            }
            Object data = datas[column];
            if (data == null) {
                return "";
            }
            if (data instanceof List) {
                @SuppressWarnings("unchecked")
                List<String> lst = ((List<String>) data);
                if (rowIndex >= 0 && rowIndex < lst.size()) {
                    // 複数データならば存在する行分だけ
                    return lst.get(rowIndex);
                }
                // 範囲外であればデータなし
                return "";
            }
            if (rowIndex == 0) {
                // 単一データは先頭行のみ
                return (String) data;
            }
            // 単一データで次行以降はデータなし。
            return "";
        }
        
    }
    
    
    /**
     * キーの判定方法を示すキーマッチャ
     */
    private KeyMatcher keyMatcher = KeyMatcher.TEXT;
    
    /**
     * キーの判定方法を設定する.<br>
     * 設定後は{@link #remap() }を呼び出して再構築しないかぎり反映されない.<br>
     * @param keyMatcher 
     */
    public void setKeyMatcher(KeyMatcher keyMatcher) {
        if (keyMatcher == null) {
            throw new IllegalArgumentException();
        }
        KeyMatcher oldValue = this.keyMatcher;
        this.keyMatcher = keyMatcher;
    }
    
    /**
     * キーの判定方法を取得する.
     * @return 
     */
    public KeyMatcher getKeyMatcher() {
        return keyMatcher;
    }

    
    
    /**
     * データソース数を返す.<br>
     * @return データソース数
     */
    public int getNumOfLineDataLists() {
        return lineDataLists.size();
    }

    /**
     * データソースを追加する.
     * @param lineDataList データソース 
     */
    public void addLineDataList(LineDataList lineDataList) {
        if (lineDataList == null) {
            throw new IllegalArgumentException();
        }
        lineDataLists.add(lineDataList);
    }
    
    /**
     * データソースを削除する.
     * @param idx データソース
     */
    public void removeLineDataList(int idx) {
        lineDataLists.remove(idx);
    }

    /**
     * 指定されたインデックスのデータソースを取得する
     * @param idx インデックス
     * @return データソース
     */
    public LineDataList getLineDataList(int idx) {
        return lineDataLists.get(idx);
    }

    /**
     * データソースのタイトルを取得する.<br>
     * タイトル自身はデータソースが保持しているため、これはアクセスヘルパである.<br>
     * @param columnIndex データソースのインデックス
     * @return タイトル
     */
    public String getTitle(int columnIndex) {
        return getLineDataList(columnIndex).getTitle();
    }
    
    

    /**
     * キーと値のリストに選別されたデータ表現
     */
    private Map<RowKey, RowValues> dataMap = Collections.emptyMap();
    
    
    /**
     * データソースをキーに従って分類しマッピングする.<br>
     */
    public void remap() {
        TreeMap<RowKey, RowValues> map = new TreeMap<RowKey, RowValues>();
        
        // データ幅(データ数)
        int dataWidth = lineDataLists.size();
        
        // 全データからキー順のマップを構築する.
        for (int dataIndex = 0; dataIndex < dataWidth; dataIndex++) {
            LineDataList dataList = lineDataLists.get(dataIndex);
            
            for (LineData lineData : dataList) {
                // キーの構築
                String key = lineData.getKey();
                RowKey rowKey = new RowKey(keyMatcher, key);

                // nullは空文字に置換されているので非null
                String value = lineData.getValue();
                
                // キーのエントリを取得
                // なければエントリを作成する.
                RowValues rowValue = map.get(rowKey);
                if (rowValue == null) {
                    rowValue = new RowValues(dataWidth);
                    map.put(rowKey, rowValue);
                }
                rowValue.add(dataIndex, value);
            }
        }
        
        // 確定
        this.dataMap = map;
    }

    
    @Override
    public Set<Entry<RowKey, RowValues>> entrySet() {
        return dataMap.entrySet();
    }

    @Override
    public int size() {
        return dataMap.size();
    }
}
