package org.opengion.fukurou.model;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileFilter;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.opengion.fukurou.system.Closer;
import org.opengion.fukurou.util.StringUtil;

/**
 * クラウドストレージ対応用の抽象クラスです。
 * 各ベンダーのストレージに対応したプラグインを作成する場合はこのクラスを継承してください。
 *
 *
 * @og.group ファイル操作
 *
 * @og.rev 5.10.8.0 (2019/02/01) 新規作成
 * @og.rev 5.10.9.0 (2019/03/01) 変更対応
 * @author oota
 * @since JDK7.0
 */
public abstract class CloudFileOperation extends FileOperation {
	//* このプログラムのVERSION文字列を設定します。{@VALUE} */
	private static final String VERSION = "7.0.2.1 (2019/03/04)" ;
	private static final long serialVersionUID = 702120190304L ;

	/* クラス定数 */
	private static final int BUFFER_SIZE = 1024 * 4;
	/* クラス変数 */
	// パス
	protected final String conPath;
	// バケット名
	protected final String conBucket;

	private static final String UNIMPLEMNTED_ERR="このクラスでは未実装のメソッドです。";
	private static final char   FS = '/' ;
	// 5.10.12.2 (2019/06/17) 相対パス対応「../」と１つ前のディレクトリ情報を抽出(１つ前が先頭の場合は、/ではなく^)
	// 7.2.9.4 (2020/11/20) PMD:Variables that are final and static should be all capitals, 'ptnPreDir' is not all capitals.
//	private static final Pattern ptnPreDir = Pattern.compile("(?<=/|^)[^/]+/\\.\\./");
	private static final Pattern PTN_PRE_DIR = Pattern.compile("(?<=/|^)[^/]+/\\.\\./");

	/**
	 * コンストラクタ
	 *
	 *
	 * @param bucket バケット名
	 * @param inPath ファイルパス
	 */
	public CloudFileOperation(final String bucket, final String inPath) {
		super(inPath);

		this.conPath = editPath(replaceFileSeparetor(inPath));

		this.conBucket = bucket;

		if (StringUtil.isNull(conBucket)) {
			final String errMsg = "バケット未指定です。hayabusa利用ではシステム変数の「CLOUD_BUCKET」にバケット名を設定して下さい。";
			throw new RuntimeException(errMsg);
		}
	}

	/**
	 * データ書き込み
	 *
	 * InputStreamのデータを書き込みます。
	 *
	 * @param is 書き込みデータのInputStream
	 * @throws IOException IO関連のエラー情報
	 */
	@Override
	public abstract void write(InputStream is) throws IOException;

	/**
	 * データ読み込み
	 *
	 * データを読み込み、InputStreamを返します。
	 *
	 * @return 読み込みデータのInputStream
	 * @throws FileNotFoundException ファイル非存在エラー情報
	 */
	@Override
	public abstract InputStream read() throws FileNotFoundException;

	/**
	 * ファイル削除
	 *
	 * ファイルを削除します。
	 *
	 * @return 成否フラグ
	 */
	@Override
	public abstract boolean delete();

	/**
	 * ファイルコピー
	 *
	 * ファイルを指定先にコピーします。
	 *
	 * @param afPath コピー先
	 * @return 成否フラグ
	 */
	@Override
	public abstract boolean copy(String afPath);

	/**
	 * ファイルサイズ取得
	 *
	 * ファイルサイズを返します。
	 *
	 * @return ファイルサイズ
	 */
	@Override
	public abstract long length();

	/**
	 * 最終更新時刻取得
	 *
	 * 最終更新時刻を返します。
	 *
	 * @return 最終更新時刻
	 */
	@Override
	public abstract long lastModified();

	/**
	 * ファイル判定
	 *
	 * ファイルの場合は、trueを返します。
	 *
	 * @return ファイルフラグ
	 */
	@Override
	public abstract boolean isFile();

	/**
	 * ディレクトリ判定
	 *
	 * ディレクトリの場合は、trueを返します。
	 *
	 * @return ディレクトリフラグ
	 */
	@Override
	public abstract boolean isDirectory();

	/**
	 * 一覧取得
	 *
	 * パスのファイルと、ディレクトリ一覧を取得します。
	 *
	 * @param filter ﾌｧｲﾙﾌｨﾙﾀｰ
	 * @return ファイルとティレクトリ一覧
	 */
	@Override
	public abstract File[] listFiles(FileFilter filter);

	/**
	 * 親ディレクトリの取得
	 *
	 * 親のディレクトリ情報を返します。
	 *
	 * @return 親のディレクトリ
	 */
	@Override
	public abstract File getParentFile();

	/**
	 * ファイルパス取得
	 *
	 * ファイルパスを取得します。
	 *
	 * @return 設定パス
	 */
	@Override
	public String getPath() {
		return conPath;
	}

	/**
	 * 絶対パス取得
	 *
	 * 絶対パスを取得します。
	 *
	 * @return 絶対パス
	 */
	@Override
	public String getAbsolutePath() {
		return conPath;
	}

	/**
	 * ファイル名取得
	 *
	 * ファイル名を取得します。
	 *
	 * @return 名称
	 */
	@Override
	public String getName() {
		return drawName(conPath);
	}

	/**
	 * 親のパス取得
	 *
	 * 親のパスを取得します。
	 *
	 * @return 親のパス
	 */
	@Override
	public String getParent() {
		return drawParent(conPath);
	}

	/**
	 * ファイル移動
	 *
	 * ファイルを指定先に移動します。
	 *
	 * @param afPath 移動先
	 * @return 成否フラグ
	 */
	@Override
	public boolean move(final String afPath) {
		boolean flgRtn = false;

		flgRtn = copy(afPath);
		if (flgRtn) {
			flgRtn = delete();
		}

		return flgRtn;
	}

	/**
	 * 存在チェック
	 *
	 * 存在する場合は、trueを返します。
	 *
	 * @return 存在フラグ
	 */
	@Override
	public boolean exists() {
		return isDirectory() | isFile();
	}

	/**
	 * ディレクトリの作成
	 *
	 * ※１つのディレクトリのみ作成します。
	 * クラウドストレージにはディレクトリの概念が無いため、
	 * 作成は行わず、trueを返します。
	 *
	 * @return 成否フラグ
	 */
	@Override
	public boolean mkdir() {
		return true;
	}

	/**
	 * ディレクトリの作成(複数)
	 *
	 * ※複数のディレクトリを作成します。
	 * クラウドストレージにはディレクトリの概念が無いため、
	 * 作成は行わず、trueを返します。
	 *
	 *
	 * @return 成否フラグ
	 */
	@Override
	public boolean mkdirs() {
		return true;
	}

	/**
	 * ファイル名変更
	 *
	 * 指定のファイル情報のファイル名に変更します。
	 *
	 * @param dest 変更後のファイル情報
	 * @return 成否フラグ
	 */
	@Override
	public boolean renameTo(final File dest) {
		return move(dest.getPath());
	}

	/**
	 * 書き込み可能フラグ
	 *
	 * ※クラウドストレージの場合は、
	 * 存在すればtrueを返します。
	 *
	 * @return 書き込み可能フラグ
	 */
	@Override
	public boolean canWrite() {
		return exists();
	}

	/**
	 * 読み取り可能フラグ
	 *
	 * ※クラウドストレージの場合は、
	 * 存在すればtrueを返します。
	 *
	 * @return 読み取り可能フラグ
	 */
	@Override
	public boolean canRead() {
		return exists();
	}

	/**
	 * 隠しファイルフラグ
	 *
	 * ※クラウドストレージの場合は、
	 * 必ずfalseを返します。
	 *
	 * @return 隠しファイルフラグ
	 */
	@Override
	public boolean isHidden() {
		return false;
	}

	/**
	 * 新規ファイル作成
	 *
	 * 既にファイルが存在しない場合のみ、
	 * 空のファイルを作成します。
	 *
	 * @return 成否フラグ
	 * @throws IOException ファイル関連エラー情報
	 */
	@Override
	public boolean createNewFile() throws IOException {
		boolean rtn = false;

		if (!exists()) {
			InputStream is = null;
			try {
				is = new ByteArrayInputStream(new byte[0]);
				write(is);
				rtn = true;
			} finally {
				Closer.ioClose(is);
			}
		}

		return rtn;
	}

	/**
	 * 最終更新時刻の更新
	 *
	 * 最終更新時刻の更新を行います。
	 * ※クラウドストレージの場合は、
	 * 最終更新時刻の更新を行えません。
	 *
	 * @param time 更新する最終更新時刻
	 * @return 成否フラグ
	 */
	@Override
	public boolean setLastModified(final long time) {
		// クラウドストレージでは、setLastModifiedによる、
		// 最終更新時刻の設定はできないので、
		// 処理を行わずにtrueを返します。
		return true;
	}

	/**
	 * カノニカルファイル情報の取得
	 *
	 * ※ローカルサーバのみ通常ファイルと、
	 * カノニカルファイルで異なります。
	 *
	 * @return カノニカルファイル情報
	 * @throws IOException ファイル関連エラー情報
	 */
	@Override
	public FileOperation getCanonicalFile() throws IOException {
		return this;
	}

	/**
	 * toString
	 *
	 * パスを返します。
	 *
	 * @return ファイルパス
	 */
	@Override
	public String toString() {
		return conPath;
	}

	/** 共通関数 **/
	/**
	 * ファイルパスの編集
	 *
	 * パスの先頭が「/」の場合は「/」の除去と、「//」を「/」に置換処理の追加。
	 *
	 * @og.rev 5.10.12.2 (2019/06/17) 相対パス対応
	 *
	 * @param path ファイルパス
	 * @return 変更後パス
	 */
	protected String editPath(final String path) {
		if (StringUtil.isNull(path)) {
			return "";
		}
		String rtn = path;

		// 「//+」は「/」に置換
		rtn = rtn.replaceAll("//+", "/");
		// 先頭が「/」の場合は除去
//		if ("/".equals(rtn.substring(0, 1))) {
		if( FS == rtn.charAt(0) ) {
			rtn = rtn.substring(1);
		}
		// 後尾の「.」は除去
		rtn = rTrim(rtn, '.');
		// 後尾の「/」は除去
		rtn = rTrim(rtn, FS);

		// 5.10.12.2 (2019/06/17)
		// 「../」の文字列は１つ上のディレクトリに変換を行います。
		Matcher mtc = PTN_PRE_DIR.matcher(rtn);

		// 「../」が無くなるまで、１つずづ変換します。
		while(mtc.find()) {
			rtn = mtc.replaceFirst("");
			mtc = PTN_PRE_DIR.matcher(rtn);
		}

		return rtn;
	}

	/**
	 * 親のパスを抽出
	 *
	 * キーから親のパスを抽出します。
	 *
	 * @param key キー
	 * @return 親のパス
	 */
	protected String drawParent(final String key) {
		final int k = key.lastIndexOf(FS);

		String rtn = "";
		if (k > 0) {
			rtn = key.substring(0, key.lastIndexOf(FS));
		}
		if ("/".equals(File.separator)) {
			rtn = File.separator + rtn;
		}

		return rtn;
	}

	/**
	 * 名称の抽出
	 *
	 * 引数のkeyから名称を抽出します。
	 *
	 * @param key キー(パス)
	 * @return 名称
	 */
	protected String drawName(final String key) {
		final int k = key.lastIndexOf(FS);

		String rtn = key;
		if (k > 0) {
			rtn = key.substring(key.lastIndexOf(FS) + 1);
		}
		return rtn;
	}

	/**
	 * ディレクトリ用のパス編集
	 *
	 * 後尾に「/」がない場合は、付与します。
	 *
	 * @param path パス
	 * @return 後尾に「/」ありのパス
	 */
	protected String setDirTail(final String path) {
		if (StringUtil.isNull(path)) {
			return path;
		}

		final StringBuilder sb = new StringBuilder(path);
//		if (!"/".equals(path.substring(path.length() - 1))) {
		if ( FS != path.charAt(path.length() - 1) ) {
			sb.append(FS);
		}
		return sb.toString();
	}

	/**
	 * 右側トリム処理
	 *
	 * 右側の文字が、指定の文字の場合、除去します。
	 *
	 * @param str 対象文字列
	 * @param chr 指定文字
	 * @return 右側から指定文字を除去後の文字列
	 */
	protected String rTrim(final String str, final char chr) {
		String rtn = str;
		int trgPos = 0;
		for (int i = str.length() - 1; i >= 0; i--) {
			if (str.charAt(i) == chr) {
				trgPos = i;
				// すべて合致した場合は、から文字を返す
				if (trgPos == 0) {
					rtn = "";
				}
			} else {
				break;
			}
		}

		if (trgPos > 0) {
			rtn = str.substring(0, trgPos);
		}

		return rtn;
	}

	/**
	 * ファイル区切り文字変換
	 *
	 * ファイル区切り文字を変換します。
	 *
	 * @param path 変換前文字列
	 * @return 返還後文字列
	 */
	protected String replaceFileSeparetor(final String path) {
		if (StringUtil.isNull(path)) {
			return "";
		}

		return path.replaceAll("\\\\", "/");
	}

	/**
	 * フィルター処理
	 *
	 * フィルター処理を行います。
	 *
	 * @param list フィルタを行うリスト
	 * @param filter フィルタ情報
	 * @return フィルタ後のリスト
	 */
	protected File[] filter(final List<File> list, final FileFilter filter) {
		final List<File> files = new ArrayList<File>();
		for (final File file : list) {
			if (filter.accept(file)) {
				files.add(file);
			}
		}
		return files.toArray(new File[files.size()]);
	}

	/**
	 * ストリームの変換処理
	 *
	 * InputStreamをbyte[]に変換。
	 * InputStreamのサイズ計算に利用。
	 *
	 * @param is byte配列変換するInputStream
	 * @return InpusStreamをbyte配列に変換した値
	 * @throws IOException ファイル関連エラー情報
	 */
	protected byte[] toByteArray(final InputStream is) throws IOException {
		final ByteArrayOutputStream output = new ByteArrayOutputStream();
		try {
			// 7.2.9.4 (2020/11/20) Avoid variables with short names like b   b → bt , n → no
			final byte[] bt = new byte[BUFFER_SIZE];
			int no = 0;
			while ((no = is.read(bt)) != -1) {
				output.write(bt, 0, no);
			}
			return output.toByteArray();
		} finally {
			output.close();
		}
	}

	/**
	 * ローカル実行フラグ判定
	 *
	 * このabstract クラスの継承クラスはクラウド上で実行されるため、
	 * falseを返します。
	 *
	 * @return ローカル実行フラグ
	 */
	@Override
	public boolean isLocal() {
		return false;
	}

	/** java.io.Fileに実装されており、クラウド用ファイルクラスに未実装のメソッドの対応 */
	/**
	 * canExecuteの実行
	 *
	 * クラウド側では未実装のメソッドです。
	 *
	 * @return フラグ
	 */
	@Override
	public boolean canExecute() {
		throw new RuntimeException(UNIMPLEMNTED_ERR);
	}

	/**
	 * deleteOnExitの実行
	 *
	 * クラウド側では未実装のメソッドです。
	 *
	 */
	@Override
	public void deleteOnExit() {
		throw new RuntimeException(UNIMPLEMNTED_ERR);
	}

	/**
	 * getAbsoluteFileの実行
	 *
	 * クラウド側では未実装のメソッドです。
	 *
	 * @return Fileｵﾌﾞｼﾞｪｸﾄ
	 */
	@Override
	public File getAbsoluteFile() {
		throw new RuntimeException(UNIMPLEMNTED_ERR);
	}

	/**
	 * getFreeSpaceの実行
	 *
	 * クラウド側では未実装のメソッドです。
	 *
	 * @return 数値
	 */
	@Override
	public long getFreeSpace() {
		throw new RuntimeException(UNIMPLEMNTED_ERR);
	}

	/**
	 * getTotalSpaceの実行
	 *
	 * クラウド側では未実装のメソッドです。
	 *
	 * @return 数値
	 */
	@Override
	public long getTotalSpace() {
		throw new RuntimeException(UNIMPLEMNTED_ERR);
	}

	/**
	 * getUsableSpaceの実行
	 *
	 * クラウド側では未実装のメソッドです。
	 *
	 * @return 数値
	 */
	@Override
	public long getUsableSpace() {
		throw new RuntimeException(UNIMPLEMNTED_ERR);
	}

	/**
	 * isAbsoluteの実行
	 *
	 * クラウド側では未実装のメソッドです。
	 *
	 * @return フラグ
	 */
	@Override
	public boolean isAbsolute() {
		throw new RuntimeException(UNIMPLEMNTED_ERR);
	}

	/**
	 * setReadableの実行
	 *
	 * クラウド側では未実装のメソッドです。
	 *
	 * @param readable フラグ
	 * @return フラグ
	 */
	@Override
	public boolean setReadable(final boolean readable) {
		throw new RuntimeException(UNIMPLEMNTED_ERR);
	}

	/**
	 * setReadableの実行
	 *
	 * クラウド側では未実装のメソッドです。
	 *
	 * @param readable フラグ
	 * @param ownerOnly フラグ
	 * @return フラグ
	 */
	@Override
	public boolean setReadable(final boolean readable, final boolean ownerOnly) {
		throw new RuntimeException(UNIMPLEMNTED_ERR);
	}

	/**
	 * setWritableの実行
	 *
	 * クラウド側では未実装のメソッドです。
	 *
	 * @param writable フラグ
	 * @return フラグ
	 */
	@Override
	public boolean setWritable(final boolean writable) {
		throw new RuntimeException(UNIMPLEMNTED_ERR);
	}

	/**
	 * canExecuteの実行
	 *
	 * クラウド側では未実装のメソッドです。
	 *
	 * @param writable フラグ
	 * @param ownerOnly フラグ
	 * @return フラグ
	 */
	@Override
	public boolean setWritable(final boolean writable, final boolean ownerOnly) {
		throw new RuntimeException(UNIMPLEMNTED_ERR);
	}

	/**
	 * canExecuteの実行
	 *
	 * クラウド側では未実装のメソッドです。
	 *
	 * @return URI情報
	 */
	@Override
	public URI toURI() {
		throw new RuntimeException(UNIMPLEMNTED_ERR);
	}
}
