#!/usr/bin/env python
# -*- coding:utf-8 -*-
# Copyright (c) 2007, Dima Dogadaylo (www.mysoftparade.com)

import re
import sha
import pickle
import base64
import time
from random import randint
from django import newforms as forms
from django.conf import settings

class MathCaptchaForm(forms.Form):
    """Lightweight mathematical captcha where human is asked to solve
    a simple mathematical calculation like 3+5=?. It don't use database
    and don't require external libraries.
        
    From concatenation of time, question, answer, settings.SITE_URL and
    settings.SECRET_KEY is built hash that is validated on each form
    submission. It makes impossible to "record" valid captcha form
    submission and "replay" it later - form will not be validated
    because captcha will be expired.
    
    For more info see:
    http://www.mysoftparade.com/blog/improved-mathematical-captcha/
    """
    A_RE = re.compile("^(\d+)$")
    
    captcha_answer = forms.CharField(max_length = 2, required=True,
        widget = forms.TextInput(attrs={'size':'2'}))
    captcha_token = forms.CharField(max_length=200, required=True,
        widget=forms.HiddenInput())
    
    def __init__(self, *args, **kwargs):
        """Initalise captcha_question and captcha_token for the form."""
        super(MathCaptchaForm, self).__init__(*args, **kwargs)
        # reset captcha for unbound forms
        if not self.data:
            self.reset_captcha()

    def reset_captcha(self):
        """Generate new question and valid token
        for it, reset previous answer if any."""
        q, a = self._generate_captcha()
        expires = time.time() +\
        getattr(settings, 'CAPTCHA_EXPIRES_SECONDS', 60*60)
        token = self._make_token(q, a, expires)
        self.initial['captcha_token'] = token
        self._plain_question = q
        # reset captcha fields for bound form
        if self.data:                   
            def _reset():
                self.data['captcha_token'] = token
                self.data['captcha_answer'] = ''
            if hasattr(self.data, '_mutable') and not self.data._mutable:
                self.data._mutable = True
                _reset()
                self.data._mutable = False
            else:
                _reset()

    def _generate_captcha(self):
        """Generate question and return it along with correct answer."""
        a, b = randint(1,9), randint(1,9)
        return ("%s+%s" % (a,b), a+b)

    def _make_token(self, q, a, expires):
        data = base64.urlsafe_b64encode(\
            pickle.dumps({'q': q, 'expires': expires}))
        return self._sign(q, a, expires) + data
    
    def _sign(self, q, a, expires):
        plain = [getattr(settings, 'SITE_URL', ''), settings.SECRET_KEY,\
                 q, a, expires]
        plain = "".join([str(p) for p in plain])
        return sha.new(plain).hexdigest()
    
    @property
    def plain_question(self):
        return self._plain_question
    
    @property
    def knotty_question(self):
        """Wrap plain_question in some invisibe for humans markup with random
        nonexisted classes, that makes life of spambots a bit harder because
        form of question is vary from request to request."""
        digits = self._plain_question.split('+')
        return "+".join(['<span class="captcha-random-%s">%s</span>' %\
                         (randint(1,9), d) for d in digits])

    def clean_captcha_token(self):
        t = self._parse_token(self.cleaned_data['captcha_token'])
        if time.time() > t['expires']:
            raise forms.ValidationError("Captcha is expired.")
        self._plain_question = t['q']
        return t
        
    def _parse_token(self, t):
        try:
            sign, data = t[:40], t[40:]
            data = pickle.loads(base64.urlsafe_b64decode(str(data)))
            return {'q': data['q'],
                    'expires': float(data['expires']),
                    'sign': sign} 
        except Exception, e:
            import sys
            sys.stderr.write("Captcha error: %r\n" % e)
            raise forms.ValidationError("Invalid captcha!")
        
    def clean_captcha_answer(self):
        a = self.A_RE.match(self.cleaned_data.get('captcha_answer'))
        if not a:
            raise forms.ValidationError("Number is expected!")
        return int(a.group(0))
        
    def clean(self):
        """Check captcha answer."""
        cd = self.cleaned_data
        # don't check captcha if no answer
        if 'captcha_answer' not in cd:
            return cd

        t = cd.get('captcha_token')
        if t:
            form_sign = self._sign(t['q'], cd['captcha_answer'],
                                   t['expires'])
            if form_sign != t['sign']:
                self._errors['captcha_answer'] = ["Are you human?"]
        else:
            self.reset_captcha()
        return super(MathCaptchaForm, self).clean()