contract IkuraAssociation is DSMath, DSAuth {
  //
  // public
  //

  // 提案が承認されるために必要な賛成票の割合
  uint public confirmTotalTokenThreshold = 50;

  //
  // private
  //

  // データの永続化ストレージ
  IkuraStorage _storage;
  IkuraToken _token;

  // 提案一覧
  Proposal[] mintProposals;
  Proposal[] burnProposals;

  mapping (bytes32 => Proposal[]) proposals;

  struct Proposal {
    address proposer;                     // 提案者
    bytes32 digest;                       // チェックサム
    bool executed;                        // 実行の有無
    uint createdAt;                       // 提案作成日時
    uint expireAt;                        // 提案の締め切り
    address[] confirmers;                 // 承認者
    uint amount;                          // 鋳造量
  }

  //
  // Events
  //

  event MintProposalAdded(uint proposalId, address proposer, uint amount);
  event MintConfirmed(uint proposalId, address confirmer, uint amount);
  event MintExecuted(uint proposalId, address proposer, uint amount);

  event BurnProposalAdded(uint proposalId, address proposer, uint amount);
  event BurnConfirmed(uint proposalId, address confirmer, uint amount);
  event BurnExecuted(uint proposalId, address proposer, uint amount);

  constructor() public {
    proposals[keccak256('mint')] = mintProposals;
    proposals[keccak256('burn')] = burnProposals;

    // @NOTE リリース時にcontractのdeploy -> watch contract -> setOwnerの流れを
    //省略したい場合は、ここで直接controllerのアドレスを指定するとショートカットできます
    // 勿論テストは通らなくなるので、テストが通ったら試してね
    /*address controllerAddress = 0x34c5605A4Ef1C98575DB6542179E55eE1f77A188;
    owner = controllerAddress;
    LogSetOwner(controllerAddress);*/
  }

  /**
   * 永続化ストレージを設定する
   *
   * @param newStorage 永続化ストレージのインスタンス（アドレス）
   */
  function changeStorage(IkuraStorage newStorage) public auth returns (bool) {
    _storage = newStorage;
    return true;
  }

  function changeToken(IkuraToken token_) public auth returns (bool) {
    _token = token_;
    return true;
  }

  /**
   * 提案を作成する
   *
   * @param proposer 提案者のアドレス
   * @param amount 鋳造量
   */
  function newProposal(bytes32 type_, address proposer, uint amount, bytes transationBytecode) public returns (uint) {
    uint proposalId = proposals[type_].length++;
    Proposal storage proposal = proposals[type_][proposalId];
    proposal.proposer = proposer;
    proposal.amount = amount;
    proposal.digest = keccak256(proposer, amount, transationBytecode);
    proposal.executed = false;
    proposal.createdAt = now;
    proposal.expireAt = proposal.createdAt + 86400;

    // 提案の種類毎に実行すべき内容を実行する
    // @NOTE literal_stringとbytesは単純に比較できないのでkeccak256のハッシュ値で比較している
    if (type_ == keccak256('mint')) emit MintProposalAdded(proposalId, proposer, amount);
    if (type_ == keccak256('burn')) emit BurnProposalAdded(proposalId, proposer, amount);

    // 本人は当然承認
    confirmProposal(type_, proposer, proposalId);

    return proposalId;
  }

  /**
   * トークン所有者が提案に対して賛成する
   *
   * @param type_ 提案の種類
   * @param confirmer 承認者のアドレス
   * @param proposalId 提案ID
   */
  function confirmProposal(bytes32 type_, address confirmer, uint proposalId) public {
    Proposal storage proposal = proposals[type_][proposalId];

    // 既に承認済みの場合はエラーを返す
    require(!hasConfirmed(type_, confirmer, proposalId));

    // 承認行為を行ったフラグを立てる
    proposal.confirmers.push(confirmer);

    // 提案の種類毎に実行すべき内容を実行する
    // @NOTE literal_stringとbytesは単純に比較できないのでkeccak256のハッシュ値で比較している
    if (type_ == keccak256('mint')) emit MintConfirmed(proposalId, confirmer, proposal.amount);
    if (type_ == keccak256('burn')) emit BurnConfirmed(proposalId, confirmer, proposal.amount);

    if (isProposalExecutable(type_, proposalId, proposal.proposer, '')) {
      proposal.executed = true;

      // 提案の種類毎に実行すべき内容を実行する
      // @NOTE literal_stringとbytesは単純に比較できないのでkeccak256のハッシュ値で比較している
      if (type_ == keccak256('mint')) executeMintProposal(proposalId);
      if (type_ == keccak256('burn')) executeBurnProposal(proposalId);
    }
  }

  /**
   * 既に承認済みの提案かどうかを返す
   *
   * @param type_ 提案の種類
   * @param addr 承認者のアドレス
   * @param proposalId 提案ID
   *
   * @return 承認済みであればtrue、そうでなければfalse
   */
  function hasConfirmed(bytes32 type_, address addr, uint proposalId) internal view returns (bool) {
    Proposal storage proposal = proposals[type_][proposalId];
    uint length = proposal.confirmers.length;

    for (uint i = 0; i < length; i++) {
      if (proposal.confirmers[i] == addr) return true;
    }

    return false;
  }

  /**
   * 指定した提案を承認したトークンの総量を返す
   *
   * @param type_ 提案の種類
   * @param proposalId 提案ID
   *
   * @return 承認に投票されたトークン数
   */
  function confirmedTotalToken(bytes32 type_, uint proposalId) internal view returns (uint) {
    Proposal storage proposal = proposals[type_][proposalId];
    uint length = proposal.confirmers.length;
    uint total = 0;

    for (uint i = 0; i < length; i++) {
      total = add(total, _storage.tokenBalance(proposal.confirmers[i]));
    }

    return total;
  }

  /**
   * 指定した提案の締め切りを返す
   *
   * @param type_ 提案の種類
   * @param proposalId 提案ID
   *
   * @return 提案の締め切り
   */
  function proposalExpireAt(bytes32 type_, uint proposalId) public view returns (uint) {
    Proposal storage proposal = proposals[type_][proposalId];
    return proposal.expireAt;
  }

  /**
   * 提案が実行条件を満たしているかを返す
   *
   * 【承認条件】
   * - まだ実行していない
   * - 提案の有効期限内である
   * - 指定した割合以上の賛成トークンを得ている
   *
   * @param proposalId 提案ID
   *
   * @return 実行条件を満たしている場合はtrue、そうでない場合はfalse
   */
  function isProposalExecutable(bytes32 type_, uint proposalId, address proposer, bytes transactionBytecode) internal view returns (bool) {
    Proposal storage proposal = proposals[type_][proposalId];

    // オーナーがcontrollerを登録したユーザーしか存在しない場合は
    if (_storage.numOwnerAddress() < 2) {
      return true;
    }

    return  proposal.digest == keccak256(proposer, proposal.amount, transactionBytecode) &&
            isProposalNotExpired(type_, proposalId) &&
            mul(100, confirmedTotalToken(type_, proposalId)) / _storage.totalSupply() > confirmTotalTokenThreshold;
  }

  /**
   * 指定した種類の提案数を取得する
   *
   * @param type_ 提案の種類（'mint' | 'burn' | 'transferMinimumFee' | 'transferFeeRate'）
   *
   * @return 提案数（承認されていないものも含む）
   */
  function numberOfProposals(bytes32 type_) public constant returns (uint) {
    return proposals[type_].length;
  }

  /**
   * 未承認で有効期限の切れていない提案の数を返す
   *
   * @param type_ 提案の種類（'mint' | 'burn' | 'transferMinimumFee' | 'transferFeeRate'）
   *
   * @return 提案数
   */
  function numberOfActiveProposals(bytes32 type_) public view returns (uint) {
    uint numActiveProposal = 0;

    for(uint i = 0; i < proposals[type_].length; i++) {
      if (isProposalNotExpired(type_, i)) {
        numActiveProposal++;
      }
    }

    return numActiveProposal;
  }

  /**
   * 提案の有効期限が切れていないかチェックする
   *
   * - 実行されていない
   * - 有効期限が切れていない
   *
   * 場合のみtrueを返す
   */
  function isProposalNotExpired(bytes32 type_, uint proposalId) internal view returns (bool) {
    Proposal storage proposal = proposals[type_][proposalId];

    return  !proposal.executed &&
            now < proposal.expireAt;
  }

  /**
   * dJPYを鋳造する
   *
   * - 鋳造する量が0より大きい
   *
   * 場合は成功する
   *
   * @param proposalId 提案ID
   */
  function executeMintProposal(uint proposalId) internal returns (bool) {
    Proposal storage proposal = proposals[keccak256('mint')][proposalId];

    // ここでも念のためチェックを入れる
    require(proposal.amount > 0);

    emit MintExecuted(proposalId, proposal.proposer, proposal.amount);

    // 総供給量 / 実行者のdJPY / 実行者のSHINJI tokenを増やす
    _storage.addTotalSupply(proposal.amount);
    _storage.addCoinBalance(proposal.proposer, proposal.amount);
    _storage.addTokenBalance(proposal.proposer, proposal.amount);

    return true;
  }

  /**
   * dJPYを消却する
   *
   * - 消却する量が0より大きい
   * - 提案者の所有するdJPYの残高がamount以上
   * - 提案者の所有するSHINJIがamountよりも大きい
   *
   * 場合は成功する
   *
   * @param proposalId 提案ID
   */
  function executeBurnProposal(uint proposalId) internal returns (bool) {
    Proposal storage proposal = proposals[keccak256('burn')][proposalId];

    // ここでも念のためチェックを入れる
    require(proposal.amount > 0);
    require(_storage.coinBalance(proposal.proposer) >= proposal.amount);
    require(_storage.tokenBalance(proposal.proposer) >= proposal.amount);

    emit BurnExecuted(proposalId, proposal.proposer, proposal.amount);

    // 総供給量 / 実行者のdJPY / 実行者のSHINJI tokenを減らす
    _storage.subTotalSupply(proposal.amount);
    _storage.subCoinBalance(proposal.proposer, proposal.amount);
    _storage.subTokenBalance(proposal.proposer, proposal.amount);

    return true;
  }

  function isAuthorized(address src, bytes4 sig) internal view returns (bool) {
    sig; // #HACK

    return  src == address(this) ||
            src == owner ||
            src == address(_token);
  }
}
/**
 * 経過時間とSHINJI Tokenの所有比率によって特定のアクションの承認を行うクラス
 */
function changeStorage(IkuraStorage newStorage) public auth returns (bool) {
  _storage = newStorage;
  return true;
}

/**
 * 永続化ストレージを設定する
 *
 * @param newStorage 永続化ストレージのインスタンス（アドレス）
 */