/**
 * Author: Nick Johnson (arachnid at notdot.net)
 * Copyright 2016; licensed CC-BY-SA.
 * 
 * BeerCoin is a new cryptocurrency intended to encapsulate and record the
 * concept of "I owe you a beer". Did someone answer a difficult question you
 * had? Send them a BeerCoin. Did they help you carry something heavy? Send
 * them a BeerCoin. Someone buy you a beer? Send them a BeerCoin.
 * 
 * Unlike traditional currency, anyone can issue BeerCoin simply by sending it
 * to someone else. A person's BeerCoin is only as valuable as the recipient's
 * belief that they're good for the beer, should it ever be redeemed; a beer
 * owed to you by Vitalik Buterin is probably worth more than a beer owed to you
 * by the DAO hacker (but your opinions may differ on that point).
 * 
 * BeerCoin is implemented as an ERC20 compatible token, with a few extensions.
 * Regular ERC20 transfers will create or resolve obligations between the two
 * parties; they will never transfer third-party BeerCoins. Additional methods
 * are provided to allow you transfer beers someone owes you to a third party;
 * if Satoshi Nakamoto owes you a beer, you can transfer that obligation to your
 * friend who just bought you one down at the pub. Methods are also provided for
 * determining the total number of beers a person owes, to help determine if
 * they're good for it, and for getting a list of accounts that owe someone a
 * beer.
 * 
 * BeerCoin may confuse some wallets, such as Mist, that expect you can only
 * send currency up to your current total balance; since BeerCoin operates as
 * individual IOUs, that restriction doesn't apply. As a result, you will
 * sometimes need to call the 'transfer' function on the contract itself
 * instead of using the wallet's built in token support.
 * 
 * If anyone finds a bug in the contract, I'll buy you a beer. If you find a bug
 * you can exploit to adjust balances without users' consent, I'll buy you two
 * (or more).
 * 
 * If you feel obliged to me for creating this, send me a ? at
 * 0x5fC8A61e097c118cE43D200b3c4dcf726Cf783a9. Don't do it unless you mean it;
 * if we meet I'll surely redeem it.
 */
contract BeerCoin {
    using Itmap for Itmap.AddressUintMap;
    
    event Transfer(address indexed _from, address indexed _to, uint256 _value);
    event Approval(address indexed _owner, address indexed _spender, uint256 _value);

    struct UserAccount {
        bool exists;
        Itmap.AddressUintMap debtors; // People who owe you a beer
        mapping(address=>uint) allowances;
        uint maxCredit; // Most beers any individual may owe you
        uint beersOwed; // Beers owed by this person
        uint beersOwing; // Beers owed to this person
    }
    uint beersOwing;
    uint defaultMaxCredit;
    
    function() {
        throw;
    }
    
    function BeerCoin(uint _defaultMaxCredit) {
        defaultMaxCredit = _defaultMaxCredit;
    }
    
    mapping(address=>UserAccount) accounts;

    function maximumCredit(address owner) constant returns (uint) {
        if(accounts[owner].exists) {
            return accounts[owner].maxCredit;
        } else {
            return defaultMaxCredit;
        }
    }

    function setMaximumCredit(uint credit) {
        //640k ought to be enough for anyone
        if(credit > 655360)
            return;

        if(!accounts[msg.sender].exists)
            accounts[msg.sender].exists = true;
        accounts[msg.sender].maxCredit = credit;
    }
    
    function numDebtors(address owner) constant returns (uint) {
        return accounts[owner].debtors.size();
    }
    
    function debtor(address owner, uint idx) constant returns (address) {
        return accounts[owner].debtors.index(idx);
    }
    
    function debtors(address owner) constant returns (address[]) {
        return accounts[owner].debtors.keys;
    }

    function totalSupply() constant returns (uint256 supply) {
        return beersOwing;   
    }
    
    function balanceOf(address owner) constant returns (uint256 balance) {
        return accounts[owner].beersOwing;
    }
    
    function balanceOf(address owner, address debtor) constant returns (uint256 balance) {
        return accounts[owner].debtors.get(debtor);
    }
    
    function totalDebt(address owner) constant returns (uint256 balance) {
        return accounts[owner].beersOwed;
    }
    
    function transfer(address to, uint256 value) returns (bool success) {
        return doTransfer(msg.sender, to, value);
    }
    
    function transferFrom(address from, address to, uint256 value) returns (bool) {
        if(accounts[from].allowances[msg.sender] >= value && doTransfer(from, to, value)) {
            accounts[from].allowances[msg.sender] -= value;
            return true;
        }
        return false;
    }
    
    function doTransfer(address from, address to, uint value) internal returns (bool) {
        if(from == to)
            return false;
            
        if(!accounts[to].exists) {
            accounts[to].exists = true;
            accounts[to].maxCredit = defaultMaxCredit;
        }
        
        // Don't allow transfers that would exceed the recipient's credit limit.
        if(value > accounts[to].maxCredit + accounts[from].debtors.get(to))
            return false;
        
        Transfer(from, to, value);

        value -= reduceDebt(to, from, value);
        createDebt(from, to, value);

        return true;
    }
    
    // Transfers beers owed to you by `debtor` to `to`.
    function transferOther(address to, address debtor, uint value) returns (bool) {
        return doTransferOther(msg.sender, to, debtor, value);
    }

    // Allows a third party to transfer debt owed to you by `debtor` to `to`.    
    function transferOtherFrom(address from, address to, address debtor, uint value) returns (bool) {
        if(accounts[from].allowances[msg.sender] >= value && doTransferOther(from, to, debtor, value)) {
            accounts[from].allowances[msg.sender] -= value;
            return true;
        }
        return false;
    }
    
    function doTransferOther(address from, address to, address debtor, uint value) internal returns (bool) {
        if(from == to || to == debtor)
            return false;
            
        if(!accounts[to].exists) {
            accounts[to].exists = true;
            accounts[to].maxCredit = defaultMaxCredit;
        }
        
        if(transferDebt(from, to, debtor, value)) {
            Transfer(from, to, value);
            return true;
        } else {
            return false;
        }
    }
    
    // Creates debt owed by `debtor` to `creditor` of amount `value`.
    // Returns false without making changes if this would exceed `creditor`'s
    // credit limit.
    function createDebt(address debtor, address creditor, uint value) internal returns (bool) {
        if(value == 0)
            return true;
        
        if(value > accounts[creditor].maxCredit)
            return false;

        accounts[creditor].debtors.set(
            debtor, accounts[creditor].debtors.get(debtor) + value);
        accounts[debtor].beersOwed += value;
        accounts[creditor].beersOwing += value;
        beersOwing += value;
        
        return true;
    }
    
    // Reduces debt owed by `debtor` to `creditor` by `value` or the total amount,
    // whichever is less. Returns the amount of debt erased.
    function reduceDebt(address debtor, address creditor, uint value) internal returns (uint) {
        var owed = accounts[creditor].debtors.get(debtor);
        if(value >= owed) {
            value = owed;
            
            accounts[creditor].debtors.remove(debtor);
        } else {
            accounts[creditor].debtors.set(debtor, owed - value);
        }
        
        accounts[debtor].beersOwed -= value;
        accounts[creditor].beersOwing -= value;
        beersOwing -= value;
        
        return value;
    }
    
    // Transfers debt owed by `debtor` from `oldCreditor` to `newCreditor`.
    // Returns false without making any changes if `value` exceeds the amount
    // owed or if the transfer would exceed `newCreditor`'s credit limit.
    function transferDebt(address oldCreditor, address newCreditor, address debtor, uint value) internal returns (bool) {
        var owedOld = accounts[oldCreditor].debtors.get(debtor);
        if(owedOld < value)
            return false;
        
        var owedNew = accounts[newCreditor].debtors.get(debtor);
        if(value + owedNew > accounts[newCreditor].maxCredit)
            return false;
        
        
        if(owedOld == value) {
            accounts[oldCreditor].debtors.remove(debtor);
        } else {
            accounts[oldCreditor].debtors.set(debtor, owedOld - value);
        }
        accounts[oldCreditor].beersOwing -= value;
        
        accounts[newCreditor].debtors.set(debtor, owedNew + value);
        accounts[newCreditor].beersOwing += value;
        
        return true;
    }

    function approve(address spender, uint256 value) returns (bool) {
        accounts[msg.sender].allowances[spender] = value;
        Approval(msg.sender, spender, value);
        return true;
    }
    
    function allowance(address owner, address spender) constant returns (uint256) {
        return accounts[owner].allowances[spender];
    }
}


library Itmap {
    struct AddressUintMapEntry {
        uint value;
        uint idx;
    }
    
    struct AddressUintMap {
        mapping(address=>AddressUintMapEntry) entries;
        address[] keys;
    }
    
    function set(AddressUintMap storage self, address k, uint v) internal {
        var entry = self.entries[k];
        if(entry.idx == 0) {
            entry.idx = self.keys.length + 1;
            self.keys.push(k);
        }
        entry.value = v;
    }
    
    function get(AddressUintMap storage self, address k) internal returns (uint) {
        return self.entries[k].value;
    }
    
    function contains(AddressUintMap storage self, address k) internal returns (bool) {
        return self.entries[k].idx > 0;
    }
    
    function remove(AddressUintMap storage self, address k) internal {
        var entry = self.entries[k];
        if(entry.idx > 0) {
            var otherkey = self.keys[self.keys.length - 1];
            self.keys[entry.idx - 1] = otherkey;
            self.keys.length -= 1;
            
            self.entries[otherkey].idx = entry.idx;
            entry.idx = 0;
            entry.value = 0;
        }
    }
    
    function size(AddressUintMap storage self) internal returns (uint) {
        return self.keys.length;
    }
    
    function index(AddressUintMap storage self, uint idx) internal returns (address) {
        return self.keys[idx];
    }
}