library BimodalLib {
  using SafeMathLib256 for uint256;

  // ENUMS
  enum ChallengeType {
    NONE,
    STATE_UPDATE,
    TRANSFER_DELIVERY,
    SWAP_ENACTMENT
  }

  // DATA MODELS
  /**
   * Aggregate field datastructure used to sum up deposits / withdrawals for an eon.
   */
  struct AmountAggregate {
    uint256 eon;
    uint256 amount;
  }

  /**
   * The structure for a submitted commit-chain checkpoint.
   */
  struct Checkpoint {
    uint256 eonNumber;
    bytes32 merkleRoot;
    uint256 liveChallenges;
  }

  /**
   * A structure representing a single commit-chain wallet.
   */
  struct Wallet {
    // Deposits performed in the last three eons
    AmountAggregate[3] depositsKept;
    // Withdrawals requested and not yet confirmed
    Withdrawal[] withdrawals;
    // Recovery flag denoting whether this account has retrieved its funds
    bool recovered;
  }

  /**
   * A structure denoting a single withdrawal request.
   */
  struct Withdrawal {
    uint256 eon;
    uint256 amount;
  }

  /**
   * A structure containing the information of a single challenge.
   */
  struct Challenge {
    // State Update Challenges
    ChallengeType challengeType; // 0
    uint256 block; // 1
    uint256 initialStateEon; // 2
    uint256 initialStateBalance; // 3
    uint256 deltaHighestSpendings; // 4
    uint256 deltaHighestGains; // 5
    uint256 finalStateBalance; // 6
    uint256 deliveredTxNonce; // 7
    uint64 trailIdentifier; // 8
  }

  /**
   * The types of parent-chain operations logged into the accumulator.
   */
  enum Operation {
    DEPOSIT,
    WITHDRAWAL,
    CANCELLATION
  }

  /* solhint-disable var-name-mixedcase */
  /**
   * The structure for an instance of the commit-chain ledger.
   */
  struct Ledger {
    // OPERATIONAL CONSTANTS
    uint8 EONS_KEPT;
    uint8 DEPOSITS_KEPT;
    uint256 MIN_CHALLENGE_GAS_COST;
    uint256 BLOCKS_PER_EON;
    uint256 BLOCKS_PER_EPOCH;
    uint256 EXTENDED_BLOCKS_PER_EPOCH;
    // STATE VARIABLES
    uint256 genesis;
    address operator;
    Checkpoint[5] checkpoints;
    bytes32[5] parentChainAccumulator; // bytes32[EONS_KEPT]
    uint256 lastSubmissionEon;
    mapping (address => mapping (address => mapping (address => Challenge))) challengeBook;
    mapping (address => mapping (address => Wallet)) walletBook;
    mapping (address => AmountAggregate[5]) deposits;
    mapping (address => AmountAggregate[5]) pendingWithdrawals;
    mapping (address => AmountAggregate[5]) confirmedWithdrawals;
    mapping (address => uint64) tokenToTrail;
    address[] trailToToken;
  }
  /* solhint-enable */

  // INITIALIZATION
  function init(
    Ledger storage self,
    uint256 blocksPerEon,
    address operator
  )
    public
  {
    self.BLOCKS_PER_EON = blocksPerEon;
    self.BLOCKS_PER_EPOCH = self.BLOCKS_PER_EON.div(4);
    self.EXTENDED_BLOCKS_PER_EPOCH = self.BLOCKS_PER_EON.div(3);
    self.EONS_KEPT = 5; // eons kept on chain
    self.DEPOSITS_KEPT = 3; // deposit aggregates kept on chain
    self.MIN_CHALLENGE_GAS_COST = 0.005 szabo; // 5 gwei minimum gas reimbursement cost
    self.operator = operator;
    self.genesis = block.number;
  }

  // DATA ACCESS
  /**
   * This method calculates the current eon number using the genesis block number
   * and eon duration.
   */
  function currentEon(
    Ledger storage self
  )
    public
    view
    returns (uint256)
  {
    return block.number.sub(self.genesis).div(self.BLOCKS_PER_EON).add(1);
  }

  /**
   * This method calculates the current era number
   */
  function currentEra(
    Ledger storage self
  )
    public
    view
    returns (uint256)
  {
    return block.number.sub(self.genesis).mod(self.BLOCKS_PER_EON);
  }

  /**
   * This method is used to embed a parent-chain operation into the accumulator
   * through hashing its values. The on-chain accumulator is used to provide a
   * reference with respect to which the operator can commit checkpoints.
   */
  function appendOperationToEonAccumulator(
    Ledger storage self,
    uint256 eon,
    ERC20 token,
    address participant,
    Operation operation,
    uint256 value
  )
    public
  {
    self.parentChainAccumulator[eon.mod(self.EONS_KEPT)] = keccak256(abi.encodePacked(
      self.parentChainAccumulator[eon.mod(self.EONS_KEPT)],
      eon,
      token,
      participant,
      operation,
      value));
  }

  /**
   * Retrieves the total pending withdrawal amount at a specific eon.
   */
  function getPendingWithdrawalsAtEon(
    Ledger storage self,
    ERC20 token,
    uint256 eon
  )
    public
    view
    returns (uint256)
  {
    uint256 lastAggregateEon = 0;
    for (uint256 i = 0; i < self.EONS_KEPT; i++) {
      AmountAggregate storage currentAggregate = self.pendingWithdrawals[token][eon.mod(self.EONS_KEPT)];
      if (currentAggregate.eon == eon) {
        return currentAggregate.amount;
      } else if (currentAggregate.eon > lastAggregateEon && currentAggregate.eon < eon) {
        // As this is a running aggregate value, if the target eon value is not set,
        // the most recent value is provided and assumed to have remained constant.
        lastAggregateEon = currentAggregate.eon;
      }
      if (eon == 0) {
        break;
      }
      eon = eon.sub(1);
    }
    if (lastAggregateEon == 0) {
      return 0;
    }
    return self.pendingWithdrawals[token][lastAggregateEon.mod(self.EONS_KEPT)].amount;
  }

  /**
   * Increases the total pending withdrawal amount at a specific eon.
   */
  function addToRunningPendingWithdrawals(
    Ledger storage self,
    ERC20 token,
    uint256 eon,
    uint256 value
  )
    public
  {
    AmountAggregate storage aggregate = self.pendingWithdrawals[token][eon.mod(self.EONS_KEPT)];
    // As this is a running aggregate, the target eon and all those that
    // come after it are updated to reflect the increase.
    if (aggregate.eon < eon) { // implies eon > 0
      aggregate.amount = getPendingWithdrawalsAtEon(self, token, eon.sub(1)).add(value);
      aggregate.eon = eon;
    } else {
      aggregate.amount = aggregate.amount.add(value);
    }
  }
  
  /**
   * Decreases the total pending withdrawal amount at a specific eon.
   */
  function deductFromRunningPendingWithdrawals(
    Ledger storage self,
    ERC20 token,
    uint256 eon,
    uint256 latestEon,
    uint256 value
  )
    public
  {
    /* Initalize empty aggregates to running values */
    for (uint256 i = 0; i < self.EONS_KEPT; i++) {
      uint256 targetEon = eon.add(i);
      AmountAggregate storage aggregate = self.pendingWithdrawals[token][targetEon.mod(self.EONS_KEPT)];
      if (targetEon > latestEon) {
        break;
      } else if (aggregate.eon < targetEon) { // implies targetEon > 0
        // Set constant running value
        aggregate.eon = targetEon;
        aggregate.amount = getPendingWithdrawalsAtEon(self, token, targetEon.sub(1));
      }
    }
    /* Update running values */
    for (i = 0; i < self.EONS_KEPT; i++) {
      targetEon = eon.add(i);
      aggregate = self.pendingWithdrawals[token][targetEon.mod(self.EONS_KEPT)];
      if (targetEon > latestEon) {
        break;
      } else if (aggregate.eon < targetEon) {
        revert('X'); // This is impossible.
      } else {
        aggregate.amount = aggregate.amount.sub(value);
      }
    }
  }

  /**
   * Get the total number of live challenges for a specific eon.
   */
  function getLiveChallenges(
    Ledger storage self,
    uint256 eon
  )
    public
    view
    returns (uint)
  {
    Checkpoint storage checkpoint = self.checkpoints[eon.mod(self.EONS_KEPT)];
    if (checkpoint.eonNumber != eon) {
      return 0;
    }
    return checkpoint.liveChallenges;
  }

  /**
   * Get checkpoint data or assume it to be empty if non-existant.
   */
  function getOrCreateCheckpoint(
    Ledger storage self,
    uint256 targetEon,
    uint256 latestEon
  )
    public
    returns (Checkpoint storage checkpoint)
  {
    require(latestEon < targetEon.add(self.EONS_KEPT) && targetEon <= latestEon);

    uint256 index = targetEon.mod(self.EONS_KEPT);
    checkpoint = self.checkpoints[index];

    if (checkpoint.eonNumber != targetEon) {
      checkpoint.eonNumber = targetEon;
      checkpoint.merkleRoot = bytes32(0);
      checkpoint.liveChallenges = 0;
    }

    return checkpoint;
  }

  /**
   * Get the total amount pending withdrawal by a wallet at a specific eon.
   */
  function getWalletPendingWithdrawalAmountAtEon(
    Ledger storage self,
    ERC20 token,
    address holder,
    uint256 eon
  )
    public
    view
    returns (uint256 amount)
  {
    amount = 0;

    Wallet storage accountingEntry = self.walletBook[token][holder];
    Withdrawal[] storage withdrawals = accountingEntry.withdrawals;
    for (uint32 i = 0; i < withdrawals.length; i++) {
      Withdrawal storage withdrawal = withdrawals[i];
      if (withdrawal.eon == eon) {
        amount = amount.add(withdrawal.amount);
      } else if (withdrawal.eon > eon) {
        break;
      }
    }
  }

  /**
   * Get the total amounts deposited and pending withdrawal at the current eon.
   */
  function getCurrentEonDepositsWithdrawals(
    Ledger storage self,
    ERC20 token,
    address holder
  )
    public
    view
    returns (uint256 currentEonDeposits, uint256 currentEonWithdrawals)
  {

    currentEonDeposits = 0;
    currentEonWithdrawals = 0;

    Wallet storage accountingEntry = self.walletBook[token][holder];
    Challenge storage challengeEntry = self.challengeBook[token][holder][holder];

    AmountAggregate storage depositEntry =
      accountingEntry.depositsKept[challengeEntry.initialStateEon.mod(self.DEPOSITS_KEPT)];

    if (depositEntry.eon == challengeEntry.initialStateEon) {
      currentEonDeposits = currentEonDeposits.add(depositEntry.amount);
    }

    currentEonWithdrawals = getWalletPendingWithdrawalAmountAtEon(self, token, holder, challengeEntry.initialStateEon);

    return (currentEonDeposits, currentEonWithdrawals);
  }

  // UTILITY
  function addToAggregate(
    AmountAggregate storage aggregate,
    uint256 eon,
    uint256 value
  )
    public
  {
    if (eon > aggregate.eon) {
      aggregate.eon = eon;
      aggregate.amount = value;
    } else {
      aggregate.amount = aggregate.amount.add(value);
    }
  }

  function clearAggregate(
    AmountAggregate storage aggregate
  )
    public
  {
    aggregate.eon = 0;
    aggregate.amount = 0;
  }

  function signedMessageECRECOVER(
    bytes32 message,
    bytes32 r, bytes32 s, uint8 v
  )
    public
    pure
    returns (address)
  {
    return ecrecover(
      keccak256(abi.encodePacked(
        "\x19Ethereum Signed Message:\n32",
        keccak256(abi.encodePacked(
          "\x19Liquidity.Network Authorization:\n32",
          message)))),
      v, r, s);
  }
}
/**
 * This library defines the bi-modal commit-chain ledger. It provides data
 * structure definitions, accessors and mutators.
 */
function deductFromRunningPendingWithdrawals(
  Ledger storage self,
  ERC20 token,
  uint256 eon,
  uint256 latestEon,
  uint256 value
)
  public
{
  /* Initalize empty aggregates to running values */
  for (uint256 i = 0; i < self.EONS_KEPT; i++) {
    uint256 targetEon = eon.add(i);
    AmountAggregate storage aggregate = self.pendingWithdrawals[token][targetEon.mod(self.EONS_KEPT)];
    if (targetEon > latestEon) {
      break;
    } else if (aggregate.eon < targetEon) { // implies targetEon > 0
      // Set constant running value
      aggregate.eon = targetEon;
      aggregate.amount = getPendingWithdrawalsAtEon(self, token, targetEon.sub(1));
    }
  }
  /* Update running values */
  for (i = 0; i < self.EONS_KEPT; i++) {
    targetEon = eon.add(i);
    aggregate = self.pendingWithdrawals[token][targetEon.mod(self.EONS_KEPT)];
    if (targetEon > latestEon) {
      break;
    } else if (aggregate.eon < targetEon) {
      revert('X'); // This is impossible.
    } else {
      aggregate.amount = aggregate.amount.sub(value);
    }
  }
}

/**
 * Decreases the total pending withdrawal amount at a specific eon.
 */