#!/usr/bin/python2.6
# -*- coding: utf-8 -*-
#
# This program is part of purity-ng.
# Copyright © 2011, Purity Development Team, see AUTHORS for details.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
'''general purpose purity testing software'''

import os
import sys
import decimal
import termios
import tty
import string

from optparse import OptionParser

## Constants ##

__version__ = "0.2"

# List of places to look for tests
SEARCHPATH = ('/usr/share/games/purity/',)

# Exclude these things from the "list" command, as they're non-test 
# artifacts of the way legacy purity worked. (or some other reason)
EXCLUDE = ('intro', 'list', 'format')

## Classes ##

class Question(object):
    '''Stores the text and result of a question.'''
    
    def __init__(self, text):
        '''Takes the text of a question as its sole parameter.
        
        Score can be set later. '''
        self.text = text
        self.score = None
    
    @property
    def is_answered(self):
        '''Returns True if answered, False if not.'''
        return self.score is not None
    
    def yes(self):
        '''The user said "yes".'''
        self.score = 1
    
    def no(self):
        '''The user said "no".'''
        self.score = 0

## Methods ##

# {{ Adapted from http://code.activestate.com/recipes/134892/
def getch():
    '''Get a single character from the terminal, and return it as a
    string.'''
    # FIXME: This is not portable
    filedescriptor = sys.stdin.fileno()
    old_settings = termios.tcgetattr(filedescriptor)
    try:
        tty.setraw(sys.stdin.fileno())
        character = sys.stdin.read(1)
    finally:
        termios.tcsetattr(filedescriptor, termios.TCSADRAIN, old_settings)
    return character
# End adaptation }}

def get_answer(question, question_number, no_prompt):
    '''Get the answer from the user, and handle the visual feedback'''
    # FIXME: magic numbers are a bad way to do this
    
    print "   " + str(question_number+1) + ":" + question
    if no_prompt:
        return 0
    print "   [ynsqb?]",
    char = getch()
    if char == 'y':
        print "yes"
        return 1
    elif char == 'n':
        print "no"
        return 0
    elif char == 'q':
        print "quit"
        return -1
    elif char == '?':
        print "help"
        return -2
    elif char == '\x03':
        # ctrl-c recieved, quit!
        return -4
    elif char == 's':
        print "score"
        return -5
    elif char == 'b':
        print "back\n"
        return -6
    # nothing else applies
    return -3

def get_tests_list(searchpath=SEARCHPATH, exclude=EXCLUDE):
    '''Return a list of all tests in the searchpath.
    @arg searchpath should be an array of directory paths containing 
    purity test files'''
    # TODO: verify that tests are in fact purity tests and don't
    # return them if they are invalid. 
    # TODO: exclude directories, see above.
    
    tests = []
    for path in searchpath:
        for test in os.listdir(path):
            if test not in exclude:
                # Some things are reserved names,
                # or pseudo-tests there for historical reasons
                tests.append(test)
    return tests
    
def score_report(questions, question_num):
    '''Generate a text score report
    
    score and questions should be ints'''
    
    if question_num == 0:
        return "   you can't get a score without answering any " + \
        "questions, silly."
    score = 0
    for i in range(question_num):
        score += questions[i].score
    
    # score is the number of "yes"s, we want the "no"s
    purity = question_num - score
    
    # Here we use the Decimal class so we get arbitrary precision floats
    # round to four digits (##.##%)
    decimal.getcontext().prec = 4
    percent = decimal.Decimal(purity) / decimal.Decimal(question_num) * 100
    
    # TODO: fix this for "1 question"
    return "   you answered %s 'no' answers out of %s questions,\n" %  \
    (purity, question_num) + \
    "   which makes your purity score %s%%." % (percent)


def go_back(questions, question_number, no_prompt, sections):
    '''Goes back to the previous question. 
    
    Note that this calls ask_question, which could call go_back again. If 
    the previous one returns without the user quiting, it returns back to
    the original ask_question, then back to the loop though the file.'''

    if questions[question_number-1].score == 1:
        print "   Previous answer: yes" 
    else:
        print "   Previous answer: no" 
    rtr = ask_question(questions, question_number - 1, no_prompt, sections)
    print ""
    if rtr != None:
        return rtr
    return ask_question(questions, question_number, no_prompt, sections)


def ask_question(questions, question_number, no_prompt, sections):
    '''Ask the question and prompt the user for a response, then handle
    the response. '''
    
    # If the question number has section header text associated with
    # it, print a lone of '-''s then the section text.
    if question_number in sections:
        print "\n" + "-"*78
        print sections[question_number]

    answer = -10
    while answer < 0: 
        # FIXME: there doesn't seem to be a good separation between this 
        #        function and the get_answer function. Either fix that
        #        or merge. I'm leaning towards merge. ~lfaraone 20110108
        
        answer = get_answer(questions[question_number].text, 
                            question_number, no_prompt)
        if answer == 0:
            questions[question_number].no()
        elif answer == 1:
            questions[question_number].yes()
        elif answer == -1:
            print "   exiting\n   "
            print score_report(questions, question_number) + "\n"
            return 0
        elif answer == -2:
            print ""
            print "   y - answer 'yes' to current question"
            print "   n - answer 'no' to current question"
            print "   q - quit program"
            print "   b - go back a question"
            print "   ? - print this help screen"
            print ""
        elif answer == -3:
            print "huh?\n"
        elif answer == -4:
            print "\n\nFine. Be that way. Don't use the 'q' option."
            return 130
        elif answer == -5:
            print "\n" + score_report(questions, question_number) + "\n"
        elif answer == -6:
            if not question_number > 0:
                print "   cannot go back\n"
                continue
            # When we go back, we no longer need to read from the
            # file, so we enter a seperate function that pulls from
            # the stored questions. 
            return go_back(questions, question_number, no_prompt, sections)


def print_answers(questions, question_number):
    '''Loop through all the questions answered and print them out.'''
    for i in range(question_number):
        print questions[i].text ,
        if questions[i].score == 1:
            print "Yes\n\n"
        else:
            print "No\n\n"



def main(argv=sys.argv[1:]):
    '''Run the main purity-ng program. 
    
    argv should be an array containing options passed to purity-ng.'''
    
    # Initalize variables for use in the datafile parsing
    
    questions = []
    sections = {}
    begining = ""
    question_text = ""
    question_section = ""
    end = ""
    section_num = 0
    question_number = 0
    in_begining = False
    in_question = False
    in_question_section = False
    in_end = False
    escaped = False

    # Set up the options parser
    usage = ("usage: %prog <test_name> [options] \n" + 
             "For a list of tests, try \"purity list\"")
    parser = OptionParser(usage=usage, version=__version__) 
    parser.add_option("-p", action="store_true", 
                      dest="no_prompt", default=False,
                      help="print the test without prompting for answers.")
    parser.add_option("-r", action="store_true", 
                      dest="rot13", default=False,
                      help="decrypt the test using the Rot 13 algorithm.")
    (options, args) = parser.parse_args(argv)

    # Non-kwargs should be exactly one, "list", or a test name / path
    if len(args) != 1:
        # exit if they aren't the way we want them
        parser.print_help()
        return 1

    if args[0] == 'list':
        # magic keyword, display test list
        print 'the currently available tests are:\n'
        for test in get_tests_list():
            # TODO: extract a short description from tests
            print test
        return 0
    
    # The first (and only) non-kwarg should be the path / name of test
    file_path = args[0]
    
    questions_file = None
    
    try:
        # Maybe it's an absolute path, let's try that. 
        with open(file_path) as test_file:
            questions_file = test_file.read()
    except IOError:
        # Opening the file failed for some reason, assume relative and try
        # alternative paths.
        # Note there is no protection against directory traversal in this 
        # function. 
        for path in SEARCHPATH:
            try:
                with open(os.path.join(path, file_path)) as test_file:
                    questions_file = test_file.read()
            except IOError:
                continue
        if questions_file == None:
            # After all that, still nothing! Let's bail.
            print 'Cannot open test file.'
            return -1

    if options.rot13:
        print "yes".encode('rot13')
        questions_file = questions_file.encode('rot13')

    # Decoder code. Loops though the file, one char at a time. First,
    # it checks to see if it is currently inside each of the
    # sections. (begining, question section, and end.) If it is not in
    # any of them, if it checks to see if it is at a char that denotes
    # that it is at the begining of one. If not, it continues on the
    # the next char.
    for i in range(len(questions_file)):

        # if the current char is a '\' and it is not escaped, note
        # that the next char should be escaped, and continue on to it.
        if (questions_file[i] == '\\' and not escaped):
            escaped = True
            continue

        # check if in the begining section
        if (in_begining):
            # Is the current char a '}', denoting that we are at the
            # end?  If so, mark that we have left it and 
            if (questions_file[i] == '}' and not escaped):
                # If so, mark that we have left it
                in_begining = False
                # print the begining section we have collected
                print begining
                begining = ""
                continue
            # if not, add the char to string containg the text of the
            # begining section
            else:
                begining += questions_file[i]
           
        # check if in the question section
        elif (in_question):
            # Is the current char a ')', denoting that we are at the
            # end? 
            if (questions_file[i] == ')' and not escaped):
                # If so, mark that we have left it
                in_question = False
                
                # I have no idea what this is suposed to be doing, so
                # commenting it out and seeing if anything bad happends
                #if len(questions) >= question_number:
                #    questions.append(Question(question_text))

                # add the text to the array of questions
                questions.append(Question(question_text))

                # call ask_question on the question, and return any
                # value other than 0
                question_return = ask_question(questions, question_number, 
                                               options.no_prompt, sections)
                if question_return != None:
                    return question_return

                if not options.no_prompt:
                    print ""
                question_text = ""
                question_number += 1
                continue

            # otherwise just add the char to the string of question text
            else:
                question_text += questions_file[i]

        # check if in the question section header section
        elif (in_question_section):
            # Is the current char a ')', denoting that we are at the
            # end? 
            if (questions_file[i] == ']' and not escaped):
                section_num += 1
                # Mark that we have left it
                in_question_section = False
                # Ad astring including the section number and the text
                # of the section header into the hash with the section
                # number as the key
                sections[question_number] = ("" + str(section_num) + 
                                               ".  " + question_section)
                question_section = ""
                continue

            # otherwise just add the char to the string of question
            # section head text
            else:
                question_section += questions_file[i]
            
        # check if in the end section
        elif (in_end):
            # Is the current char a '>', denoting that we are at the
            # end? 
            if (questions_file[i] == '>' and not escaped):
                # If we have been promting for answers, print a score
                # report
                if not (options.no_prompt):
                    print score_report(questions, question_number)
                # mark that we are no long in the end section
                in_end = False
                # Print the text we have collected
                print end
                end = ""
                continue
                
            # otherwise just add the char to the string of end text
            else:
                end += questions_file[i]


        # If it has gotten this far, it means we are not in one of the
        # sections. New check to see if we are at the begining of any
        # of the sections
        elif (questions_file[i] == '{' and not escaped):
            in_begining = True
        elif (questions_file[i] == '(' and not escaped):
            in_question = True
        elif (questions_file[i] == '[' and not escaped):
            in_question_section = True
        elif (questions_file[i] == '<' and not escaped):
            in_end = True
            
        # if this the char was escaped, mark it as not escaped
        if (escaped):
            escaped = False
    
    if (not options.no_prompt) and question_number > 0: 
        # The test is over, ask the user if they want to get a copy of it
        print "Would you like to display your answers? [yn]",
        answer = 0
        while True:
            if question_number in sections:
                # If the question number has section header text
                # associated with it, print a lone of '-''s then the
                # section text 
                print "\n" + "-"*78
                print sections[question_number]

            answer = getch()
            if (answer in 'yN\r'):
                # We've got a reply we want.
                break
            # Well, we didn't get one of those, so let's continue
            print "\nPlease enter 'y' or 'n'",
                
        if answer == 'y':
            print "yes"
            print_answers(questions, question_number)
        else:
            # Ergo, answer was n or \r
            print "no\n\n Good bye"
    

if __name__ == '__main__':
    # If we're being run directly (not imported), run the main method
    sys.exit(main())
