package project.common.file;

import java.io.FilterOutputStream;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.StringJoiner;
import java.util.TreeMap;
import java.util.zip.ZipEntry;

import common.db.JdbcSource;
import core.file.ZippedLineOutputStream;
import project.common.CsvUtil;

/**
 * CSV改頁出力
 *
 * @author Tadashi Nakayama
 */
public final class CsvPageBreakOutputStream extends FilterOutputStream {

	/** 改頁値保持 */
	private final Map<Integer, byte[]> keybreak = new TreeMap<>();
	/** 値保存行リスト */
	private final List<byte[]> line = new ArrayList<>();
	/** Charset */
	private final Charset charset;
	/** CRLF */
	private final byte[] crlf;
	/** カンマ */
	private final byte[] comma;

	/**
	 * コンストラクタ
	 *
	 * @param os ZippedLineOutputStream
	 * @param pb 改頁位置
	 */
	public CsvPageBreakOutputStream(final ZippedLineOutputStream os, final int[] pb) {
		this(os, pb, JdbcSource.getCharset());
	}

	/**
	 * コンストラクタ
	 *
	 * @param os ZippedLineOutputStream
	 * @param pb 改頁位置
	 * @param cs Charset
	 */
	public CsvPageBreakOutputStream(final ZippedLineOutputStream os,
			final int[] pb, final Charset cs) {
		super(os);
		this.charset = cs;
		this.crlf = CsvUtil.crlf(cs);
		this.comma = CsvUtil.comma(cs);
		initPageBreak(pb);
	}

	/**
	 * 改頁キー初期化
	 *
	 * @param val 位置キー
	 */
	private void initPageBreak(final int[] val) {
		for (final var loc : val) {
			this.keybreak.put(loc, null);
		}
	}

	/**
	 * 改頁キー再設定
	 */
	private void resetPageBreak() {
		for (final var ent : this.keybreak.entrySet()) {
			ent.setValue(this.line.get(ent.getKey()));
		}
	}

	/**
	 * @see java.io.FilterOutputStream#write(byte[])
	 */
	@Override
	public void write(final byte[] val) throws IOException {
		if (endsWith(this.crlf, val)) {
			if (val.length == this.crlf.length) {
				pagebreak();
				output();
			} else {
				getZipStream().setTitle(val);
			}
		} else if (!isComma(val)) {
			this.line.add(val);
		}
	}

	/**
	 * 改頁処理
	 *
	 * @throws IOException IO例外
	 */
	private void pagebreak() throws IOException {
		String entry = null;
		for (var i = 0; i < this.line.size(); i++) {
			final var kw = this.keybreak.get(i);
			if ((kw == null && this.keybreak.containsKey(i))
					|| (kw != null && !Arrays.equals(kw, this.line.get(i)))) {
				resetPageBreak();
				entry = getEntryName();
				break;
			}
		}
		if (entry != null) {
			getZipStream().setEntryName(entry);
			getZipStream().putNextEntry(new ZipEntry(getZipStream().getNextEntryName(true)));
		}
	}

	/**
	 * エントリ名取得
	 *
	 * @return エントリ名
	 */
	private String getEntryName() {
		final var sj = new StringJoiner("_", "", ".csv");
		for (final var ent : this.keybreak.entrySet()) {
			sj.add(new String(ent.getValue(), this.charset));
		}
		return sj.toString();
	}

	/**
	 * 一行出力
	 *
	 * @throws IOException IO例外
	 */
	private void output() throws IOException {
		super.out.write(this.line.get(0));
		for (var i = 1; i < this.line.size(); i++) {
			super.out.write(this.comma);
			super.out.write(this.line.get(i));
		}
		super.out.write(this.crlf);
		this.line.clear();
	}

	/**
	 * 終了値終了確認
	 *
	 * @param end 終了値
	 * @param val 確認対象
	 * @return 終了値で終了している場合 true を返す。
	 */
	private boolean endsWith(final byte[] end, final byte[] val) {
		for (var i = 1; i <= end.length; i++) {
			if (end.length > val.length || end[end.length - i] != val[val.length - i]) {
				return false;
			}
		}
		return true;
	}

	/**
	 * カンマ確認
	 *
	 * @param val 値
	 * @return カンマの場合 true を返す。
	 */
	private boolean isComma(final byte[] val) {
		return Arrays.equals(this.comma, val);
	}

	/**
	 * ZippedLineOutputStream取得
	 *
	 * @return ZippedLineOutputStream
	 */
	private ZippedLineOutputStream getZipStream() {
		return ZippedLineOutputStream.class.cast(super.out);
	}

	/**
	 * @see java.io.FilterOutputStream#write(byte[], int, int)
	 */
	@Override
	public void write(final byte[] b, final int off, final int len) throws IOException {
		write(Arrays.copyOfRange(b, off, off + len));
	}

	/**
	 * @see java.io.FilterOutputStream#write(int)
	 */
	@Override
	public void write(final int val) {
		throw new UnsupportedOperationException();
	}
}
