#pragma once

#include <Wincrypt.h>

#include <memory>
#include <string>
#include <map>
#include <exception>


/*!
 * 暗号化・暗号化解除の失敗を示す例外
 */
class CCryptException
	: public std::exception
{
private:
	DWORD errorcode_;

public:
	CCryptException(DWORD errorcode);

	DWORD GetErrorCode() const;
};


class CCryptPassword;


/*!
 * パスワードベースの暗号化・暗号化解除のためのクラス
 */
class CCrypt
{
public:
	/*!
	 * アルゴリズム名とアルゴリズムIDのマップ型
	 */
	typedef std::map<ALG_ID, tstring> algtypemap;

	/*!
	 * アルゴリズム名とアルゴリズムIDのペア
	 */
	typedef algtypemap::value_type algtype;

protected:
	friend class CCryptPassword;

	/*!
	 * 暗号化パスワード
	 */
	class CCryptPassword
	{
		friend class CCrypt;
	private:

		/*!
		 * 親
		 */
		CCrypt &crypt_;

		/*!
		 * パスワードのハッシュ
		 */
		HCRYPTHASH hPasswordHash_;

		/*!
		 * パスワードベースの対称暗号化キー
		 */
		HCRYPTKEY hPasswordKey_;

		/*!
		 * 暗号化に使用するアルゴリズム名
		 */
		tstring algName_;


		CCryptPassword(const CCryptPassword&); //<! 実装なし

		CCryptPassword& operator=(const CCryptPassword&); //<! 実装なし

	public:

		CCryptPassword(CCrypt &crypt);

		~CCryptPassword() throw();

		/*!
		 * パスワードを設定します。
		 * 空の場合はパスワードが解除されます。
		 * パスワードが設定され暗号化に使える場合はtrueを返します。
		 * \return 暗号化のパスワードが生成された場合はtrue
		 */
		bool setPassword(const CCrypt::algtype &algtyp, const tstring& password);

		/*!
		 * パスワードを解除します。
		 */
		void clearPassword();

	private:

		/*!
		 * 初期化し、プロバイダのハンドルを取得します。
		 * すでに初期化済みであれば、そのハンドルを返します。
		 */
		HCRYPTPROV init();

		/*!
		 * パスワードベースのハッシュを生成して返します。
		 * \param hProv プロバイダのハンドル
		 * \param password パスワード
		 * \return ハッシュ
		 */
		HCRYPTHASH createHash(HCRYPTPROV hProv, const tstring& password);

		/*!
		 * ハッシュから対称暗号化キーを生成します。
		 * \param hProv プロバイダのハンドル
		 * \param hHash ハッシュ
		 * \param alg アルゴリズム
		 * \return 暗号化キー
		 */
		HCRYPTKEY createKey(HCRYPTPROV hProv, HCRYPTHASH hHash, const CCrypt::algtype &alg);
	};

private:

	/*!
	 * 暗号プロバイダ
	 */
	HCRYPTPROV hProv_;

	/*!
	 * サポートする暗号化アルゴリズムのマップ
	 */
	algtypemap algtypemap_;


	CCrypt(const CCrypt&); //<! 実装なし

	CCrypt& operator=(const CCrypt&); //<! 実装なし

public:

	/*!
	 * 特に何もしません。
	 */
	CCrypt();

	/*!
	 * 暗号化プロバイダが初期化されていれば初期化解除します。
	 * パスワードハッシュ、対称暗号化キーが作成さていれば解除します。
	 */
	~CCrypt() throw();

	/*!
	 * 利用可能な暗号化アルゴリズムの中から優先するアルゴリズムを選択する。
	 * 利用できる暗号化リズムに該当がない場合は例外が発生する。
	 * \return アルゴリズム
	 */
	const algtype& chooseAlgorithm();

	/*!
	 * 指定した名前の暗号化アルゴリズムをサポートしている中から取得する。
	 * 該当がない場合は例外が発生する。
	 * \param アルゴリズム名
	 * \return アルゴリズム
	 */
	const algtype& findAlgorithm(const tstring& algName);

	/*!
	 * パスワードが設定されていれば、そのパスワードベースの暗号化を行います。
	 * パスワードが設定されていない場合は平文のまま返します。
	 * \param src 暗号化する文字列
	 * \param result 暗号化された文字列
	 */
	void encrypt(const tstring& password, const algtype& algtyp, const tstring& src, tstring& result);

	/*!
	 * パスワードが設定されていれば、そのパスワードで暗号化解除します。
	 * パスワードが設定されていなければ、そのまま返します。
	 */
	void decrypt(const tstring& password, const tstring& src, tstring& result);

private:

	/*!
	 * プロバイダを初期化します。
	 * すでに初期化済みであれば何もしません。
	 */
	void init();

	/*!
	 * サポートされているアルゴリズムの一覧を取得します。
	 */
	void loadSupportedAlgorithm();
};

