import datetime
import locale
import os
from decimal import Decimal


try:  # XXX This can't be right.
    locale.setlocale(locale.LC_ALL, "en_US.utf8")
except locale.Error:
    locale.setlocale(locale.LC_ALL, "en_US.UTF-8")


BIRTHDAY = datetime.date(2012, 6, 1)
CARDINALS = ['zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine']
MONTHS = [None, 'January', 'February', 'March', 'April', 'May', 'June', 'July',
          'August', 'September', 'October', 'November', 'December']

def age():
    today = datetime.date.today()
    nmonths = today.month - BIRTHDAY.month
    plural = 's' if nmonths != 1 else ''
    if nmonths < 10:
        nmonths = CARDINALS[nmonths]
    else:
        nmonths = str(nmonths)
    return "%s month%s" % (nmonths, plural)


db = None # This global is wired in wireup. It's an instance of
          # gittip.postgres.PostgresManager.

# Not sure we won't want this for something yet. Prune if you don't find it in
# the codebase in a month.
OLD_OLD_AMOUNTS= [Decimal(a) for a in ('0.00', '0.08', '0.16', '0.32', '0.64', '1.28')]
OLD_AMOUNTS= [Decimal(a) for a in ('0.25',)]

AMOUNTS= [Decimal(a) for a in ('0.00', '1.00', '3.00', '6.00', '12.00', '24.00')]


__version__ = "~~VERSION~~"


def get_tip(tipper, tippee):
    """Given two user ids, return a Decimal.
    """
    TIP = """\

        SELECT amount
          FROM tips
         WHERE tipper=%s
           AND tippee=%s
      ORDER BY mtime DESC
         LIMIT 1

    """
    rec = db.fetchone(TIP, (tipper, tippee))
    if rec is None:
        tip = Decimal(0.00)
    else:
        tip = rec['amount']
    return tip


def get_backed_amount(participant_id):
    """Given a unicode, return a Decimal.
    """

    BACKED = """\

        SELECT sum(amount) AS backed
          FROM ( SELECT DISTINCT ON (tipper)
                        amount
                      , tipper
                   FROM tips
                   JOIN participants p ON p.id = tipper
                  WHERE tippee=%s
                    AND last_bill_result = ''
               ORDER BY tipper
                      , mtime DESC
                ) AS foo

    """
    rec = db.fetchone(BACKED, (participant_id,))
    if rec is None:
        amount = None
    else:
        amount = rec['backed']  # might be None

    if amount is None:
        amount = Decimal(0.00)

    return amount


def get_number_of_backers(participant_id):
    """Given a unicode, return an int.
    """

    BACKED = """\

        SELECT count(amount) AS nbackers
          FROM ( SELECT DISTINCT ON (tipper)
                        amount
                      , tipper
                   FROM tips
                   JOIN participants p ON p.id = tipper
                  WHERE tippee=%s
                    AND last_bill_result = ''
               ORDER BY tipper
                      , mtime DESC
                ) AS foo
         WHERE amount > 0

    """
    rec = db.fetchone(BACKED, (participant_id,))
    if rec is None:
        nbackers = None
    else:
        nbackers = rec['nbackers']  # might be None

    if nbackers is None:
        nbackers = 0

    return nbackers


def get_tips_and_total(tipper, for_payday=False, db=None):
    """Given a participant id and a date, return a list and a Decimal.

    This function is used to populate a participant's page for their own
    viewing pleasure, and also by the payday function. If for_payday is not
    False it must be a date object.

    A half-injected dependency, that's what db is.

    """
    if db is None:
        from gittip import db

    if for_payday:

        # For payday we want the oldest relationship to be paid first.
        order_by = "ctime ASC"


        # This is where it gets crash-proof.
        # ==================================
        # We need to account for the fact that we may have crashed during
        # Payday and we're re-running that function. We only want to select
        # tips that existed before Payday started, but haven't been processed
        # as part of this Payday yet.
        #
        # It's a bug if the paydays subselect returns > 1 rows.
        #
        # XXX If we crash during Payday and we rerun it after a timezone
        # change, will we get burned? How?

        ts_filter = """\

               AND mtime < %s
               AND ( SELECT id
                       FROM transfers
                      WHERE tipper=t.tipper
                        AND tippee=t.tippee
                        AND timestamp >= %s
                    ) IS NULL

        """
        args = (tipper, for_payday, for_payday)
    else:
        order_by = "amount DESC"
        ts_filter = ""
        args = (tipper,)

    TIPS = """\

        SELECT * FROM (
            SELECT DISTINCT ON (tippee)
                   amount
                 , tippee
                 , t.ctime
                 , p.claimed_time
              FROM tips t
              JOIN participants p ON p.id = t.tippee
             WHERE tipper = %%s
               %s
          ORDER BY tippee
                 , t.mtime DESC
        ) AS foo
        ORDER BY %s
               , tippee

    """ % (ts_filter, order_by)  # XXX, No injections here, right?!
    tips = list(db.fetchall(TIPS, args))


    # Compute the total.
    # ==================
    # For payday we only want to process payments to tippees who have
    # themselves opted into Gittip. For the tipper's profile page we want to
    # show the total amount they've pledged (so they're not surprised when
    # someone *does* start accepting tips and all of a sudden they're hit with
    # bigger charges.

    if for_payday:
        to_total = [t for t in tips if t['claimed_time'] is not None]
    else:
        to_total = tips
    total = sum([t['amount'] for t in to_total])

    if not total:
        # If to_total is an empty list then total is int 0. We want a Decimal.
        total = Decimal('0.00')

    return tips, total


# canonizer
# =========
# This is an Aspen hook to ensure that requests are served on a certain root
# URL, even if multiple domains point to the application.

class X: pass
canonical_scheme = None
canonical_host = None

def canonize(request):
    """Enforce a certain scheme and hostname. Store these on request as well.
    """
    scheme = request.headers.get('X-Forwarded-Proto', 'http') # per Heroku
    host = request.headers['Host']
    bad_scheme = scheme != canonical_scheme
    bad_host = bool(canonical_host) and (host != canonical_host)
                # '' and False => ''
    if bad_scheme or bad_host:
        url = '%s://%s' % (canonical_scheme, canonical_host)
        if request.line.method in ('GET', 'HEAD', 'OPTIONS', 'TRACE'):
            # Redirect to a particular path for idempotent methods.
            url += request.line.uri.path.raw
            if request.line.uri.querystring:
                url += '?' + request.line.uri.querystring.raw
        else:
            # For non-idempotent methods, redirect to homepage.
            url += '/'
        request.redirect(url, permanent=True)


def configure_payments(request):
    # Work-around for https://github.com/balanced/balanced-python/issues/5
    import balanced
    balanced.configure(os.environ['BALANCED_API_SECRET'])

gittip/__init__.py
import datetime
import locale
import os
from decimal import Decimal


try:  # XXX This can't be right.
    locale.setlocale(locale.LC_ALL, "en_US.utf8")
except locale.Error:
    locale.setlocale(locale.LC_ALL, "en_US.UTF-8")


BIRTHDAY = datetime.date(2012, 6, 1)
CARDINALS = ['zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine']
MONTHS = [None, 'January', 'February', 'March', 'April', 'May', 'June', 'July',
          'August', 'September', 'October', 'November', 'December']

def age():
    today = datetime.date.today()
    nmonths = today.month - BIRTHDAY.month
    plural = 's' if nmonths != 1 else ''
    if nmonths < 10:
        nmonths = CARDINALS[nmonths]
    else:
        nmonths = str(nmonths)
    return "%s month%s" % (nmonths, plural)


db = None # This global is wired in wireup. It's an instance of
          # gittip.postgres.PostgresManager.

# Not sure we won't want this for something yet. Prune if you don't find it in
# the codebase in a month.
OLD_OLD_AMOUNTS= [Decimal(a) for a in ('0.00', '0.08', '0.16', '0.32', '0.64', '1.28')]
OLD_AMOUNTS= [Decimal(a) for a in ('0.25',)]

AMOUNTS= [Decimal(a) for a in ('0.00', '1.00', '3.00', '6.00', '12.00', '24.00')]


__version__ = "~~VERSION~~"


def get_tip(tipper, tippee):
    """Given two user ids, return a Decimal.
    """
    TIP = """\

        SELECT amount
          FROM tips
         WHERE tipper=%s
           AND tippee=%s
      ORDER BY mtime DESC
         LIMIT 1

    """
    rec = db.fetchone(TIP, (tipper, tippee))
    if rec is None:
        tip = Decimal(0.00)
    else:
        tip = rec['amount']
    return tip


def get_backed_amount(participant_id):
    """Given a unicode, return a Decimal.
    """

    BACKED = """\

        SELECT sum(amount) AS backed
          FROM ( SELECT DISTINCT ON (tipper)
                        amount
                      , tipper
                   FROM tips
                   JOIN participants p ON p.id = tipper
                  WHERE tippee=%s
                    AND last_bill_result = ''
               ORDER BY tipper
                      , mtime DESC
                ) AS foo

    """
    rec = db.fetchone(BACKED, (participant_id,))
    if rec is None:
        amount = None
    else:
        amount = rec['backed']  # might be None

    if amount is None:
        amount = Decimal(0.00)

    return amount


def get_number_of_backers(participant_id):
    """Given a unicode, return an int.
    """

    BACKED = """\

        SELECT count(amount) AS nbackers
          FROM ( SELECT DISTINCT ON (tipper)
                        amount
                      , tipper
                   FROM tips
                   JOIN participants p ON p.id = tipper
                  WHERE tippee=%s
                    AND last_bill_result = ''
               ORDER BY tipper
                      , mtime DESC
                ) AS foo
         WHERE amount > 0

    """
    rec = db.fetchone(BACKED, (participant_id,))
    if rec is None:
        nbackers = None
    else:
        nbackers = rec['nbackers']  # might be None

    if nbackers is None:
        nbackers = 0

    return nbackers


def get_tips_and_total(tipper, for_payday=False, db=None):
    """Given a participant id and a date, return a list and a Decimal.

    This function is used to populate a participant's page for their own
    viewing pleasure, and also by the payday function. If for_payday is not
    False it must be a date object.

    A half-injected dependency, that's what db is.

    """
    if db is None:
        from gittip import db

    if for_payday:

        # For payday we want the oldest relationship to be paid first.
        order_by = "ctime ASC"


        # This is where it gets crash-proof.
        # ==================================
        # We need to account for the fact that we may have crashed during
        # Payday and we're re-running that function. We only want to select
        # tips that existed before Payday started, but haven't been processed
        # as part of this Payday yet.
        #
        # It's a bug if the paydays subselect returns > 1 rows.
        #
        # XXX If we crash during Payday and we rerun it after a timezone
        # change, will we get burned? How?

        ts_filter = """\

               AND mtime < %s
               AND ( SELECT id
                       FROM transfers
                      WHERE tipper=t.tipper
                        AND tippee=t.tippee
                        AND timestamp >= %s
                    ) IS NULL

        """
        args = (tipper, for_payday, for_payday)
    else:
        order_by = "amount DESC"
        ts_filter = ""
        args = (tipper,)

    TIPS = """\

        SELECT * FROM (
            SELECT DISTINCT ON (tippee)
                   amount
                 , tippee
                 , t.ctime
                 , p.claimed_time
              FROM tips t
              JOIN participants p ON p.id = t.tippee
             WHERE tipper = %%s
               %s
          ORDER BY tippee
                 , t.mtime DESC
        ) AS foo
        ORDER BY %s
               , tippee

    """ % (ts_filter, order_by)  # XXX, No injections here, right?!
    tips = list(db.fetchall(TIPS, args))


    # Compute the total.
    # ==================
    # For payday we only want to process payments to tippees who have
    # themselves opted into Gittip. For the tipper's profile page we want to
    # show the total amount they've pledged (so they're not surprised when
    # someone *does* start accepting tips and all of a sudden they're hit with
    # bigger charges.

    if for_payday:
        to_total = [t for t in tips if t['claimed_time'] is not None]
    else:
        to_total = tips
    total = sum([t['amount'] for t in to_total])

    if not total:
        # If to_total is an empty list then total is int 0. We want a Decimal.
        total = Decimal('0.00')

    return tips, total


def get_histogram_of_giving(user):
    SQL = """
    SELECT amount, count(amount) num_contributing FROM tips t WHERE
    tippee=%s GROUP BY (amount)
    """
    results = dict()
    for amount_dict in db.fetchall(SQL, (user,)):
        results[amount_dict['amount']] = amount_dict['num_contributing']
    return results


# canonizer
# =========
# This is an Aspen hook to ensure that requests are served on a certain root
# URL, even if multiple domains point to the application.

class X: pass
canonical_scheme = None
canonical_host = None

def canonize(request):
    """Enforce a certain scheme and hostname. Store these on request as well.
    """
    scheme = request.headers.get('X-Forwarded-Proto', 'http') # per Heroku
    host = request.headers['Host']
    bad_scheme = scheme != canonical_scheme
    bad_host = bool(canonical_host) and (host != canonical_host)
                # '' and False => ''
    if bad_scheme or bad_host:
        url = '%s://%s' % (canonical_scheme, canonical_host)
        if request.line.method in ('GET', 'HEAD', 'OPTIONS', 'TRACE'):
            # Redirect to a particular path for idempotent methods.
            url += request.line.uri.path.raw
            if request.line.uri.querystring:
                url += '?' + request.line.uri.querystring.raw
        else:
            # For non-idempotent methods, redirect to homepage.
            url += '/'
        request.redirect(url, permanent=True)


def configure_payments(request):
    # Work-around for https://github.com/balanced/balanced-python/issues/5
    import balanced
    balanced.configure(os.environ['BALANCED_API_SECRET'])

gittip/__init__.py
diff --git a/gittip/__init__.py b/gittip/__init__.py
index 856ffa0418..a176a4934c 100644
--- a/gittip/__init__.py
+++ b/gittip/__init__.py
@@ -215,6 +215,17 @@ def get_tips_and_total(tipper, for_payday=False, db=None):
     return tips, total
 
 
+def get_histogram_of_giving(user):
+    SQL = """
+    SELECT amount, count(amount) num_contributing FROM tips t WHERE
+    tippee=%s GROUP BY (amount)
+    """
+    results = dict()
+    for amount_dict in db.fetchall(SQL, (user,)):
+        results[amount_dict['amount']] = amount_dict['num_contributing']
+    return results
+
+
 # canonizer
 # =========
 # This is an Aspen hook to ensure that requests are served on a certain root
diff --git a/tests/test_stats.py b/tests/test_stats.py
index aa8cb5289b..5a931bbade 100644
--- a/tests/test_stats.py
+++ b/tests/test_stats.py
@@ -1,7 +1,9 @@
 from datetime import datetime
+from decimal import Decimal
 
 from mock import patch
 
+import gittip
 from gittip.billing.payday import Payday
 from gittip import testing
 from gittip import wireup
@@ -21,6 +23,59 @@ def test_commaize_commaizes_and_obeys_decimal_places():
     assert actual == "1,000.0000", actual
 
 
+class HistogramOfGivingTests(testing.GittipBaseDBTest):
+    def setUp(self):
+        super(HistogramOfGivingTests, self).setUp()
+
+        user_ids = [x[1] for x in testing.GITHUB_USERS]
+        prices = (0, 1, 3, 6, 12, 24)
+        donation_map = {
+            'lgtest': {
+                'lglocktest': 1,
+                'gittip-test-0': 3,
+                'gittip-test-1': 6,
+                'gittip-test-2': 0,
+                },
+            'lglocktest': {
+                'lgtest': 3,
+                'gittip-test-0': 6,
+                'gittip-test-1': 6,
+                'gittip-test-2': 3,
+                },
+            'gittip-test-0': {
+                'lgtest': 12,
+                },
+            'gittip-test-1': {
+                'lgtest': 3,
+                },
+            'gittip-test-2': {
+                'lgtest': 6,
+                },
+            'gittip-test-3': {
+                },
+        }
+        for tipper, donation in donation_map.iteritems():
+            for tippee, amount in donation.iteritems():
+                self.db.execute(
+                    "INSERT INTO tips (ctime, tipper, tippee, amount) " \
+                    "VALUES (now(), %s, %s, %s);",
+                    (tipper, tippee, amount))
+
+    def test_histogram(self):
+        expected = {
+            Decimal('3.00'): 2,
+            Decimal('6.00'): 1,
+            Decimal('12.00'):1
+        }
+        actual = gittip.get_histogram_of_giving('lgtest')
+        self.assertEqual(expected, actual)
+
+    def test_histogram_no_tips(self):
+        expected = {}
+        actual = gittip.get_histogram_of_giving('gittip-test-3')
+        self.assertEqual(expected, actual)
+
+
 # rendering
 
 class TestStatsPage(testing.GittipBaseTest):
diff --git a/www/%participant_id/index.html b/www/%participant_id/index.html
index 15605c5301..9f71300751 100644
--- a/www/%participant_id/index.html
+++ b/www/%participant_id/index.html
@@ -14,7 +14,8 @@
 import requests
 from aspen import json, Response
 from aspen.utils import to_age
-from gittip import AMOUNTS, db, get_tip, get_tips_and_total, get_backed_amount
+from gittip import AMOUNTS, db, get_tip, get_tips_and_total, \
+     get_backed_amount, get_histogram_of_giving
 from gittip.utils import wrap
 from gittip.networks import github, resolve_unclaimed
 
@@ -64,6 +65,9 @@
     else:
         my_tip = get_tip(user.id, participant['id'])
 
+giving_histogram = get_histogram_of_giving(user.id)
+total_givers = sum(giving_histogram.values())
+
 can_tip = True
 backed_amount = get_backed_amount(participant['id'])
 tip_or_pledge = "tip"
@@ -72,7 +76,36 @@
 # ========================================================================== ^L
 {% extends templates/participant.html %}
 {% block their_voice %}
+
     <style>
+        /** This is blatantly copied from stats.html. We should really fix
+            that somehow. **/
+        .dollar-sign {
+            padding: 0 2pt 0 24pt;
+            text-align: right;
+        }
+        .amount {
+            padding: 0 6pt 0 0;
+            text-align: right;
+        }
+        .amount-change {
+            padding: 6pt 0 6pt 24pt;
+            text-align: left;
+        }
+        .count {
+            text-align: left;
+            white-space: nowrap;
+        }
+        .count SPAN.number {
+            font-size: 8pt;
+        }
+        .count SPAN.bar {
+            background: #B2A196;
+            display: inline-block;
+            margin-right: 3pt;
+            height: 9pt;
+        }
+
         #matrix TR.not-over BUTTON.empty {
             background: transparent;
             color: #F7F7F6;
@@ -485,6 +518,30 @@ <h2>You receive ${{ backed_amount }} per week.
         <span class="small"><a href="history.html">History</a></span>
     </h2>
 
+    <table id="tip-distribution">
+        {% for amount, contributors in giving_histogram.iteritems() %}
+        <tr>
+            <td class="dollar-sign">$</td>
+            <td class="amount">{{ amount }}</td>
+            <td class="count">
+                <span class="bar" style="width: {{ ((1.0 * contributors) / total_givers) * 300 }}pt"></span>
+                {{((1.0 * contributors) / total_givers) * 100 }}%
+                <span class="number">({{ contributors }})</span>
+            </td>
+        </tr>
+        {% if str(amount) == '0.25' %}
+        <td class="amount-change help" colspan="3">
+            <a href="https://github.com/whit537/www.gittip.com/issues/180"
+               target="_blank">Read
+            here</a> about raising the minimum.
+        </td>
+        {% end %}
+        {% end %}
+    </table>
+
+
+
+
     {% if backed_amount == 0 %}
 
     <p>How can you convince people to tip you? <b>Make the world

Show how many total fundees and the donation amount they're in for.
As a reciever of money I'd like to know how diversified my $13/wk is so that I know how volatile my income is likely to be.

How I see this working is a Histogram similar to:

 $1 [---]  1
 $3 []
 $6 []
$12 [---]  1
$24 []

which shows that I have 2 funders, one in for $1 and one in for $12.

