/*
 * Copyright 2006 Takahiro Nakamura.
 *
 * 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 woolpack.sql.convert;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import woolpack.fn.Fn;
import woolpack.sql.fn.PreparedStatementInfo;

/**
 * クエスチョンでパラメータバインドが定義されたSQLを解析するユーティリティです。
 * @author nakamura
 *
 */
public final class SqlColumnUtils {
	private static final Pattern WHERE_PATTERN =
		Pattern.compile("\\s+where\\s+", Pattern.CASE_INSENSITIVE);
	private static final Pattern INSERT_COLS_PATTERN =
		Pattern.compile("\\(([^\\)]+)\\)", Pattern.CASE_INSENSITIVE);
	private static final Pattern INSERT_HEADER_PATTERN =
		Pattern.compile("^insert\\s+into\\s+", Pattern.CASE_INSENSITIVE);
	
	private static int count(final String s, final char c) {
		int count = 0;
		final int length = s.length();
		for(int i = 0; i < length; i++) {
			if (s.charAt(i) == c) {
				count++;
			}
		}
		return count;
	}

	/**
	 * パラメータ名をリストに追加する関数です。
	 * ひとつのパラメータ名で出現位置が複数ある場合はゼロ開始の位置を下線文字で接続します。
	 */
	public static final Fn<List<String>, Fn<ParamBindInfo, Void, RuntimeException>, RuntimeException> APPEND_NAME =
	new Fn<List<String>, Fn<ParamBindInfo, Void, RuntimeException>, RuntimeException>() {
		public Fn<ParamBindInfo, Void, RuntimeException> exec(final List<String> list) {
			return new Fn<ParamBindInfo, Void, RuntimeException>() {
				public Void exec(final ParamBindInfo info) {
					final int count = count(info.getBindString(), '?');
					if (count == 1) {
						list.add(info.getName());
					} else {
						for (int i = 0; i < count; i++) {
							list.add(info.getName() + '_' + i);
						}
					}
					return null;
				}
			};
		}
	};
	
	/**
	 * パラメータ名とオペレータのラベルを下線文字で接続した文字列を返す関数です。
	 * ひとつのパラメータ名で出現位置が複数ある場合はゼロ開始の位置を追加で接続します。
	 */
	public static final Fn<List<String>, Fn<ParamBindInfo, Void, RuntimeException>, RuntimeException> APPEND_NAME_WITH_OPERATOR =
	new Fn<List<String>, Fn<ParamBindInfo, Void, RuntimeException>, RuntimeException>() {
		public Fn<ParamBindInfo, Void, RuntimeException> exec(final List<String> list) {
			return new Fn<ParamBindInfo, Void, RuntimeException>() {
				public Void exec(final ParamBindInfo info) {
					if (info.getOperator() == null || !info.isWhereFlag()) {
						list.add(info.getName());
					} else {
						final int count = count(info.getBindString(), '?');
						if (count == 1) {
							list.add(info.getName() + '_' + info.getOperator().getLabel());
						} else {
							for (int i = 0; i < count; i++) {
								list.add(info.getName() + '_' + info.getOperator().getLabel() + i);
							}
						}
					}
					return null;
				}
			};
		}
	};
	
	private SqlColumnUtils() {}
	
	/**
	 * Insert以外のクエリについて、
	 * SQLを解析してパラメータバインド情報の一覧を生成します。
	 * @param s 解析対象のSQL。
	 * @return パラメータバインド情報の一覧。
	 */
	public static List<ParamBindInfo> getNotInsertList(final String s) {
		final int whereIndex;
		{
			final Matcher matcher = WHERE_PATTERN.matcher(s);
			whereIndex = matcher.find() ? matcher.start() : -1;
		}
		final List<ParamBindInfo> list = new ArrayList<ParamBindInfo>();
		for (final SqlOperator operator : SqlOperator.values()) {
			final Matcher matcher = operator.getPattern().matcher(s);
			while(matcher.find()) {
				final int index = matcher.start();
				list.add(new ParamBindInfo(
						matcher.group(SqlOperator.COL_GROUP),
						index,
						matcher.end(),
						matcher.group(SqlOperator.BIND_GROUP),
						operator,
						(whereIndex < index)));
			}
		}
		Collections.sort(list, ParamBindInfo.COMPARATOR);
		return list;
	}
	
	/**
	 * Insertのクエリについて、
	 * SQLを解析してパラメータバインド情報の一覧を生成します。
	 * @param s 解析対象のSQL。
	 * @return パラメータバインド情報の一覧。
	 */
	public static List<ParamBindInfo> getInsertList(final String s) {
		final Matcher matcher = INSERT_COLS_PATTERN.matcher(s);
		if (!matcher.find()) {
			return null;
		}
		final String[] colArray = matcher.group(1).split("[\\s,]+");
		final List<ParamBindInfo> list = new ArrayList<ParamBindInfo>();
		for (final String col : colArray) {
			list.add(new ParamBindInfo(col));
		}
		return list;
	}
	
	/**
	 * SQLを解析してパラメータバインド情報の一覧を生成します。
	 * @param s 解析対象のSQL。
	 * @return パラメータバインド情報の一覧。
	 */
	public static List<ParamBindInfo> getList(final String s) {
		return INSERT_HEADER_PATTERN.matcher(s).find() ?
				getInsertList(s) :
					getNotInsertList(s);
	}

	/**
	 * SQLを解析してコンパイル済ステートメント情報を生成します。
	 * @param s 解析対象のSQL。
	 * @param fn パラメータバインド情報から変数名を取得する関数。
	 * @return コンパイル済ステートメント情報。
	 */
	public static PreparedStatementInfo toPreparedStatementInfo(
			final String s,
			final Fn<? super List<String>, ? extends Fn<? super ParamBindInfo, Void, ? extends RuntimeException>, ? extends RuntimeException> fn) {
		final PreparedStatementInfo info = new PreparedStatementInfo();
		final List<ParamBindInfo> bindInfoList = getList(s);
		final List<String> list = new ArrayList<String>(bindInfoList.size());
		info.setList(list);
		final Fn<? super ParamBindInfo, Void, ? extends RuntimeException> appender = fn.exec(list);
		for (final ParamBindInfo bindInfo : bindInfoList) {
			appender.exec(bindInfo);
		}
		info.setQuery(s);
		return info;
	}
}
