"""$URL: svn+ssh://svn/repos/trunk/dulcinea/lib/survey.py $
$Id: survey.py 25896 2005-01-19 18:57:50Z rmasse $
"""

from durus.persistent_list import PersistentList
from durus.persistent_dict import PersistentDict
from dulcinea.address import ContactAddress
from dulcinea.keyed import Keyed, KeyedMap
from dulcinea.base import DulcineaBase, DulcineaPersistent
from dulcinea.spec import any, sequence, mapping, require
from dulcinea.timestamped import Timestamped
from dulcinea.typeutils import typecheck, typecheck_seq
from dulcinea.user import DulcineaUser


class Question(Keyed, DulcineaPersistent):
    """
    Instance attributes:
      title : string
        text of the question
      sub_title : string
        sub title
    """

    title_is = any(str, None)
    sub_title_is = any(str, None)

    def __init__(self, title, sub_title=None):
        assert self.__class__ is not Question, "abstract class"
        Keyed.__init__(self)
        self.title = title
        self.sub_title = sub_title

    def __str__(self):
        if self.sub_title:
            return self.title + "\n" + self.sub_title
        return self.title

    def get_title(self):
        return self.title

    def get_sub_title(self):
        return self.sub_title

    def get_new_answer(self):
        raise NotImplementedError, "abstract method"


class TextQuestion(Question):

    """
    Instance attributes:
      dimensions : (rows:int, cols:int) | None
    """
    dimensions_is = (int, int)

    def __init__(self, title, sub_title=None, rows=5, cols=80):
        Question.__init__(self, title, sub_title)
        self.dimensions = (rows, cols)

    def get_new_answer(self):
        return TextAnswer(self)

    def get_dimensions(self):
        # Fix with update_db.
        return getattr(self, 'dimensions', (5, 80))

class ContactAddressQuestion(Question):

    """
    Instance attributes: none
    """
    def get_new_answer(self):
        return ContactAddressAnswer(self)


class SelectQuestion(Question):
    """
    Instance attributes:
      column_headings : [ string ]
        text to place above each column
      row_headings : [ string ]
        text that preceeds each row
      range_labels : [ string ]
        labels to be placed in a row above column headings at the right and left
      last_row_has_text : boolean
        flag to tell the widget to display a TextWidget after the last row label

    A SelectQuestion is an abstract class for representing selections
    from either checkboxes or radio buttons
    """

    column_headings_is = [ any(str, None) ]
    row_headings_is = [ any (str, None) ]
    range_labels_is = [ str ]
    last_row_has_text_is = bool

    def __init__(self, title, sub_title=None,
                 column_headings=None, row_headings=None,
                 range_labels=None, last_row_has_text=False):
        Question.__init__(self, title, sub_title=sub_title)
        self.column_headings = column_headings or []
        self.row_headings = row_headings or []
        self.range_labels = range_labels or []
        assert len(self.range_labels) in (0,2), "must have two range labels"
        self.last_row_has_text = last_row_has_text

    def __str__(self):
        question = self.title
        if len(self.column_headings) > 1: # Radioset
            if len(self.column_headings) > 4:
                headings = '(%s ... %s)' % (self.column_headings[0],
                                            self.column_headings[-1])
            else:
                headings = '(%s)' % (', '.join(self.column_headings))
            question += "\n" + headings
            if self.range_labels:
                question += "\n  ...where %s is %s and %s is %s" % (
                    self.column_headings[0], self.range_labels[0],
                    self.column_headings[-1], self.range_labels[-1])
        if self.sub_title:
            question += "\n" + self.sub_title
        return question

    def get_row_headings(self):
        return self.row_headings

    def get_column_headings(self):
        return self.column_headings

    def get_range_labels(self):
        return self.range_labels

    def get_new_answer(self):
        return SelectAnswer(self)


class Answer(DulcineaBase):
    """
    Instance attributes:
      question : Question
        the question associated with this answer
    """

    question_is = Question

    def __init__(self, question):
        assert self.__class__ is not Answer, "abstract class"
        self.question = question

    def get_question(self):
        return self.question



class ContactAddressAnswer(Answer):
    """
    Instance attributes:
      answer : ContactAddress
    """

    answer_is = ContactAddress

    def __init__(self, question):
        Answer.__init__(self, question)
        self.answer = ContactAddress()

    def __str__(self):
        return self.answer.format()

    def get_answer(self):
        return self.answer

    def set_answer(self, answer):
        self.answer = answer

    def has_answer(self):
        return self.answer != ContactAddress()



class TextAnswer(Answer):
    """
    Instance attributes:
      answer : string
        text answer to a question
    """

    answer_is = str

    def __init__(self, question):
        Answer.__init__(self, question)
        self.answer = None

    def __str__(self):
        return self.answer

    def get_answer(self):
        return self.answer

    def set_answer(self, answer):
        self.answer = answer

    def has_answer(self):
        return self.answer is not None

class SelectAnswer(Answer):
    """
    Instance attributes:
      selected : [ string | boolean ]
        list of selected row answers
      other_text : string
        contents of the filled in 'last_row_has_text' textfield
    """
    selected_is = [any(str, bool)]

    def __init__(self, question):
        Answer.__init__(self, question)
        self.selected = None
        self.other_text = None

    def __str__(self):
        if self.question.row_headings:
            def format_heading_answer(heading, answer):
                if answer:
                    return "%s: %s" % (heading, answer)
                return ''
            return "\n".join([format_heading_answer(*heading_answer) for
                              heading_answer in
                              zip(self.question.row_headings, self.selected)])
        return ", ".join(self.selected)

    def set_other_text(self, other_text):
        self.other_text = other_text

    def get_other_text(self):
        return self.other_text

    def set_answer(self, selected):
        self.selected = selected

    def get_answer(self):
        return self.selected

    def has_answer(self):
        return self.selected is not None


class Survey(DulcineaPersistent, Timestamped):
    """A set of Questions and Answers attached to a single object.

    Instance attributes:
      user : DulcineaUser
        user taking the survey
      answers : [ Answer ]
        answers to the questions that comprise the object's survey
    """

    user_is = any(DulcineaUser, None)
    answers_is = [Answer]

    def __init__(self, answers):
        """(answers:[Answer])
        """
        Timestamped.__init__(self)
        typecheck_seq(answers, Answer)
        self.answers = answers
        self.user = None

    def __str__(self):
        return "By %s on %s: %d answers" % (
            self.user, self.get_timestamp(), len(self.answers))

    def __len__(self):
        return len(self.answers)

    def get_questions(self):
        return [answer.get_question() for answer in self.answers]

    def get_answers(self):
        return self.answers

    def get_answer_for_question_key(self, question_key):
        for answer in self.answers:
            if answer.get_question().get_key() == question_key:
                return answer
        return None

    def get_user(self):
        return self.user

    def set_user(self, user):
        """(user : DuclineaUser)
        """
        typecheck(user, DulcineaUser, allow_none=1)
        self.user = user

    def set_answers(self, answers, user):
        """(answers : [Answer], user : DulcineaUser)
        """
        typecheck_seq(answers, Answer)
        self.answers = answers
        self.set_user(user)
        self.set_timestamp()

    def has_answers(self, omit_questions=None):
        """(omit_questions : [int] | None) -> boolean

        Return true if any answers have been answered for this survey
        ignoring omitted questions if specified
        """
        for answer in self.answers:
            if answer.get_question().get_key() in (omit_questions or []):
                continue
            if answer.has_answer():
                return True
        return False


class Surveyable:
    """A mixin for objects that have Surveys

    Instance attributes:
      surveys : PersistentList [ Survey ]
    """

    surveys_is = any(sequence(Survey, PersistentList), None)

    def __init__(self):
        self.surveys = None

    def get_surveys(self):
        """() -> [Survey]
        """
        return self.surveys or []

    def get_survey(self, index):
        return self.surveys and self.surveys[index]

    def get_new_survey(self):
        """() -> Survey
        """
        raise NotImplementedError, "abstract method"

    def add_survey(self, survey):
        if self.surveys is None:
            self.surveys = PersistentList()
        self.surveys.append(survey)


class QuestionDatabase(KeyedMap, DulcineaPersistent):
    """
    Instance attributes: none
    """
    def __init__(self):
        KeyedMap.__init__(self, value_spec=Question)

    def get_answers(self, question_keys=None):
        """(question_keys: [int]) -> [Answer]
        """
        if not question_keys:
            question_keys = self.get_mapping().keys()
            question_keys.sort()
        return [self.get(question_key).get_new_answer()
                for question_key in question_keys]


class SurveysAndQuestions(Surveyable, QuestionDatabase):
    """
    Instance attributes:
      id : str
      description : str
      title : str
    """

    id_is = str
    title_is = str
    description_is = str

    def __init__(self, id, questions):
        """(id : str, questions : [Question])
        """
        Surveyable.__init__(self)
        QuestionDatabase.__init__(self)
        self.id = id
        for question in questions:
            self.add(question)
        self.title = ''
        self.description = ''

    def get_id(self):
        return self.id

    def set_title(self, title):
        require(title, self.title_is)
        self.title = title

    def get_title(self):
        return self.title

    def set_description(self, description):
        require(description, self.description_is)
        self.description = description

    def get_description(self):
        return self.description

    def get_new_survey(self):
        return Survey(self.get_answers())


class SurveysAndQuestionsDatabase(DulcineaPersistent):
    """
    Instance attributes:
      surveys_and_questions :  PersistentDict { str : SurveysAndQuestions }
    """

    surveys_and_questions_is = mapping({str:SurveysAndQuestions},
                                       PersistentDict)

    def __init__(self):
        self.surveys_and_questions = PersistentDict()

    def add(self, surveys_and_questions):
        require(surveys_and_questions, SurveysAndQuestions)
        id = surveys_and_questions.get_id()
        assert id not in self.surveys_and_questions, id
        self.surveys_and_questions[id] = surveys_and_questions

    def get(self, id):
        return self.surveys_and_questions[id]

    def get_mapping(self):
        return self.surveys_and_questions

