package online.model;

import java.io.Serializable;
import java.util.Date;
import java.util.HashMap;
import java.util.ListIterator;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.function.BiFunction;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import core.config.Factory;
import core.exception.LogicalException;

/**
 * バンド
 *
 * @author Tadashi Nakayama
 */
public class Band {
	/** 汎用モデル */
	private final UniModel model;
	/** 現在位置 */
	private final int index;
	/** 全サイズ */
	private final int size;

	/**
	 * コンストラクタ
	 *
	 * @param um 汎用モデル
	 * @param loc 位置
	 * @param len 長さ
	 */
	Band(final UniModel um, final int loc, final int len) {
		this.model = um;
		this.index = loc;
		this.size = len;
	}

	/**
	 * 次存在確認
	 *
	 * @return 次が存在する場合 true を返す。
	 */
	public boolean hasNext() {
		return this.index + 1 < this.size;
	}

	/**
	 * 値存在確認
	 *
	 * @param key 項目名
	 * @return 存在する場合 true を返す。
	 */
	public boolean hasValue(final String key) {
		return this.index < this.model.getArraySize(key);
	}

	/**
	 * 現在位置
	 *
	 * @return 現在位置
	 */
	public int index() {
		return this.index;
	}

	/**
	 * 文字列取得
	 *
	 * @param key 項目名
	 * @return 文字列
	 */
	public String string(final String key) {
		checkIndex(key);
		return ModelUtil.getValueAsString(this.model, key, this.index);
	}

	/**
	 * 数値取得
	 *
	 * @param <T> Type
	 * @param key 項目名
	 * @return 数値
	 */
	public <T extends Number> T number(final String key) {
		checkIndex(key);
		return Factory.cast(this.model.getNumberArray(key)[this.index]);
	}

	/**
	 * 日時取得
	 *
	 * @param <T> type
	 * @param key 項目名
	 * @return 日時
	 */
	public <T extends Date> T date(final String key) {
		checkIndex(key);
		return Factory.cast(this.model.getDateArray(key)[this.index]);
	}

	/**
	 * 真偽値取得
	 *
	 * @param key 項目名
	 * @return 真偽値
	 */
	public Boolean bool(final String key) {
		checkIndex(key);
		return this.model.getBooleanArray(key)[this.index];
	}

	/**
	 * キーが含まれているかを返す。
	 *
	 * @param key キー
	 * @return boolean
	 */
	public boolean containsKey(final String key) {
		return this.model.containsKey(key);
	}

	/**
	 * BeanにBandの値をセットする。
	 *
	 * @param bean 設定Bean
	 */
	public void setValueTo(final Object bean) {
		ModelUtil.setModelValue(bean, this.model, this.index);
	}

	/**
	 * マップ取得
	 *
	 * @return マップ
	 */
	public Map<String, Serializable> toMap() {
		final var map = new HashMap<String, Serializable>();
		this.model.keySet().forEach(key -> map.put(key, getValue(key)));
		return map;
	}

	/**
	 * 値取得
	 *
	 * @param key キー
	 * @return 値
	 */
	private Serializable getValue(final String key) {
		Serializable ret = null;
		final var cls = this.model.getValueClass(key);
		if (Factory.isSubclassOf(Date[].class, cls) || Factory.isSubclassOf(Date.class, cls)) {
			ret = date(key);
		} else if (Boolean[].class.equals(cls) || Boolean.class.equals(cls)) {
			ret = bool(key);
		} else if (String[].class.equals(cls) || String.class.equals(cls)) {
			ret = string(key);
		} else if (Factory.isSubclassOf(Number[].class, cls)
						|| Factory.isSubclassOf(Number.class, cls)) {
			ret = number(key);
		}
		return ret;
	}

	/**
	 * 値設定
	 *
	 * @param key 項目名
	 * @param val 値
	 */
	public void set(final String key, final String val) {
		checkIndex(key);
		final var vals = this.model.getStringArray(key);
		vals[this.index] = val;
	}

	/**
	 * 値設定
	 *
	 * @param key 項目名
	 * @param val 値
	 */
	public void set(final String key, final Number val) {
		checkIndex(key);
		final var vals = this.model.getNumberArray(key);
		vals[this.index] = val;
	}

	/**
	 * 値設定
	 *
	 * @param key 項目名
	 * @param val 値
	 */
	public void set(final String key, final Date val) {
		checkIndex(key);
		final var vals = this.model.getDateArray(key);
		vals[this.index] = val;
	}

	/**
	 * 値設定
	 *
	 * @param key 項目名
	 * @param val 値
	 */
	public void set(final String key, final Boolean val) {
		checkIndex(key);
		final var vals = this.model.getBooleanArray(key);
		vals[this.index] = val;
	}

	/**
	 * 配列長確認
	 *
	 * @param key 項目名
	 */
	private void checkIndex(final String key) {
		if (this.model.getArraySize(key) <= this.index) {
			throw new LogicalException("Invalid Index.[size of "
					+ key + ":" + this.model.getArraySize(key) + " index:" + this.index + "]");
		}
	}

	/**
	 * Stream取得
	 *
	 * @param um 汎用モデル
	 * @param leader 項目名
	 * @return Stream
	 */
	public static Stream<Band> stream(final UniModel um, final String leader) {
		return StreamSupport.stream(Band.iterable(um, leader).spliterator(), false);
	}

	/**
	 * Iterable取得
	 *
	 * @param um 汎用モデル
	 * @param leader 項目名
	 * @return BandIterable
	 */
	public static Iterable<Band> iterable(final UniModel um, final String leader) {
		return () -> new InnerIterator<>(um.getArraySize(leader), (lc, ln) -> new Band(um, lc, ln));
	}

	/**
	 * 内部イテレータ
	 *
	 * @author Tadashi Nakayama
	 * @param <E> Type
	 */
	private static final class InnerIterator<E> implements ListIterator<E> {
		/** 長さ */
		private final int length;
		/** Eの取得関数 */
		private final BiFunction<Integer, Integer, E> function;

		/** 現在位置 */
		private int location = 0;

		/**
		 * コンストラクタ
		 *
		 * @param len 長さ
		 * @param func Eの取得関数
		 */
		InnerIterator(final int len, final BiFunction<Integer, Integer, E> func) {
			this.length = len;
			this.function = func;
		}

		/**
		 * @see java.util.ListIterator#hasNext()
		 */
		@Override
		public boolean hasNext() {
			return 0 <= this.location && this.location < this.length;
		}

		/**
		 * @see java.util.ListIterator#next()
		 */
		@Override
		public E next() {
			if (this.length <= this.location) {
				throw new NoSuchElementException();
			}
			this.location++;
			return getItem(this.location, this.length);
		}

		/**
		 * @see java.util.ListIterator#nextIndex()
		 */
		@Override
		public int nextIndex() {
			return this.location;
		}

		/**
		 * @see java.util.ListIterator#hasPrevious()
		 */
		@Override
		public boolean hasPrevious() {
			return 0 < this.location;
		}

		/**
		 * @see java.util.ListIterator#previous()
		 */
		@Override
		public E previous() {
			if (this.location <= 0) {
				throw new NoSuchElementException();
			}
			this.location--;
			return getItem(this.location, this.length);
		}

		/**
		 * @see java.util.ListIterator#previousIndex()
		 */
		@Override
		public int previousIndex() {
			return this.location - 1;
		}

		/**
		 * アイテム取得
		 *
		 * @param loc 位置
		 * @param len 長さ
		 * @return アイテム
		 */
		public E getItem(final int loc, final int len) {
			return this.function.apply(loc, len);
		}

		/**
		 * @see java.util.ListIterator#remove()
		 */
		@Override
		public void remove() {
			throw new UnsupportedOperationException();
		}

		/**
		 * @see java.util.ListIterator#set(java.lang.Object)
		 */
		@Override
		public void set(final E e) {
			throw new UnsupportedOperationException();
		}

		/**
		 * @see java.util.ListIterator#add(java.lang.Object)
		 */
		@Override
		public void add(final E e) {
			throw new UnsupportedOperationException();
		}
	}
}
