package project.svc.generic;

import java.io.IOException;
import java.io.OutputStream;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;

import org.apache.logging.log4j.LogManager;

import common.db.JdbcSource;
import common.sql.QueryUtil;
import common.sql.SelectQuery;
import common.sql.Selector;
import core.config.Factory;
import core.util.MojiUtil;
import online.model.UniModel;
import online.model.UniModelImpl;
import project.base.QueryAbstract;
import project.common.CsvUtil;

/**
 * CSVサービス
 * @author Tadashi Nakayama
 * @version 1.0.0
 */
public final class CsvService extends QueryAbstract implements SelectQuery {

	/** アイテム位置 */
	private final Map<String, Integer> location = new HashMap<>();

	/** クエリ */
	private String query = null;
	/** クエリファイル */
	private String queryFile = null;
	/** 汎用モデル */
	private UniModel param = null;
	/** ヘッダ文字列 */
	private byte[] title = null;
	/** 出力ストリーム */
	private OutputStream os = null;
	/** ヘッダ出力 */
	private boolean header = false;

	/**
	 * 出力ストリーム設定
	 *
	 * @param val 出力ストリーム
	 */
	public void setOutputStream(final OutputStream val) {
		this.os = val;
	}

	/**
	 * ヘッダ出力設定
	 * @param val ヘッダ出力フラグ
	 */
	public void showTitle(final boolean val) {
		this.header = val;
	}

	/**
	 * ヘッダ出力確認
	 * @return ヘッダ出力の場合 true を返す。
	 */
	public boolean isTitled() {
		return this.header;
	}

	/**
	 * クエリ設定
	 * @param val クエリ
	 */
	public void setQuery(final String val) {
		this.query = val;
	}

	/**
	 * クエリファイル設定
	 * @param val クエリファイル
	 */
	public void setQueryFile(final String val) {
		this.queryFile = val;
	}

	/**
	 * 汎用モデル設定
	 * @param val 汎用モデル
	 */
	public void setUniModel(final UniModel val) {
		this.param = new UniModelImpl(val);
	}

	/**
	 * ヘッダ文字列設定
	 * @param val ヘッダ文字列
	 */
	public void setTitle(final String... val) {
		this.title = CsvUtil.toByteTitle(val, getCharset());
	}

	/**
	 * アイテム設定
	 * @param val アイテム文字列
	 */
	public void setItem(final String val) {
		this.location.clear();
		if (!Objects.toString(val, "").isEmpty()) {
			int loc = 0;
			for (final var item : val.split(",")) {
				final var key = item.toUpperCase(Locale.ENGLISH);
				if (!this.location.containsKey(key)) {
					this.location.put(key, loc++);
				}
			}
		}
	}

	/**
	 * アイテム数取得
	 * @return アイテム数
	 */
	public int getItemSize() {
		return this.location.size();
	}

	/**
	 * @see common.sql.SelectQuery#callback(java.sql.ResultSet)
	 */
	@Override
	public boolean callback(final ResultSet rs) throws SQLException {
		final var rsmd = rs.getMetaData();

		var ret = false;
		try {
			writeTitle(rsmd);

			final var crlf = CsvUtil.crlf(getCharset());
			final var comm = CsvUtil.comma(getCharset());
			final var vals = new String[getSize(rsmd)];
			while (rs.next()) {
				ret = true;

				for (var i = 0; i < rsmd.getColumnCount(); i++) {
					final var col = rsmd.getColumnLabel(i + 1);
					final var pos = getPosition(i, col);
					if (0 <= pos) {
						vals[pos] = rs.getString(col);
					}
				}

				this.os.write(toBytes(vals[0]));
				for (var i = 1; i < vals.length; i++) {
					this.os.write(comm);
					this.os.write(toBytes(vals[i]));
				}
				this.os.write(crlf);
			}

		} catch (final IOException ex) {
			LogManager.getLogger().info(ex.getMessage());
		}

		return ret;
	}

	/**
	 * バイト化
	 * @param val 文字列
	 * @return バイト化文字列
	 */
	private byte[] toBytes(final String val) {
		return MojiUtil.correctGarbled(
				CsvUtil.toCsvValue(val), getCharset()).getBytes(getCharset());
	}

	/**
	 * タイトル出力
	 * @param rsmd ResultSetMetaData
	 * @throws IOException IO例外
	 * @throws SQLException SQL例外
	 */
	private void writeTitle(final ResultSetMetaData rsmd) throws IOException, SQLException {
		if (this.header) {
			if (this.title == null || this.title.length == 0) {
				this.os.write(CsvUtil.toByteTitle(toLabelArray(rsmd), getCharset()));
			} else {
				this.os.write(this.title);
			}
		}
	}

	/**
	 * ラベル配列化
	 * @param rsmd ResultSetMetaData
	 * @return ラベル配列
	 * @throws SQLException SQL例外
	 */
	private String[] toLabelArray(final ResultSetMetaData rsmd) throws SQLException {
		final var ret = new String[rsmd.getColumnCount()];
		for (var i = 0; i < ret.length; i++) {
			final var label = rsmd.getColumnLabel(i + 1);
			final var pos = getPosition(i, label);
			if (0 <= pos) {
				ret[pos] = label;
			}
		}
		return ret;
	}

	/**
	 * @see common.sql.SelectQuery#makeParam()
	 */
	@Override
	public Map<String, Object> makeParam() {
		return toParamMap(this.param.toMap());
	}

	/**
	 * @see common.sql.SelectQuery#makeQuery()
	 */
	@Override
	public String makeQuery() {
		if (this.query != null) {
			return this.query;
		}
		return QueryUtil.getSqlFromFile(this.queryFile);
	}

	/**
	 * サイズ取得
	 * @param rsmd メタデータ
	 * @return サイズ
	 * @throws SQLException SQL例外
	 */
	private int getSize(final ResultSetMetaData rsmd) throws SQLException {
		if (0 < this.location.size()) {
			return this.location.size();
		}
		return rsmd.getColumnCount();
	}

	/**
	 * 設定位置取得
	 * @param loc カラム位置
	 * @param col カラム名
	 * @return 設定位置
	 */
	public int getPosition(final int loc, final String col) {
		if (0 < this.location.size()) {
			final var it = this.location.get(col.toUpperCase(Locale.ENGLISH));
			return (it != null) ? it : -1;
		}
		return loc;
	}

	/**
	 * 検索処理
	 * @return レコードが存在した場合 true を返す。
	 */
	public boolean search() {
		try (var conn = JdbcSource.getConnection(getSchema())) {
			final var sc = Factory.create(Selector.class);
			sc.setConnection(conn);
			sc.setFetchSize(getFetchSize());
			return sc.search(this);
		}
	}
}
