use crate::{
    answer::Answer,
    quiz::{EmptyError, QuizError},
    xml_util::{write_named_formatted_scope, write_text_tag},
};
use std::fs::File;
use xml::writer::{EventWriter, XmlEvent};
pub trait Question {
    fn get_name(&self) -> &str;
    fn get_description(&self) -> &str;
    fn set_text_format(&mut self, format: TextFormat);
    fn add_answers(&mut self, answers: Vec<Answer>) -> Result<(), QuizError>;
    fn to_xml(&self, writer: &mut EventWriter<&File>) -> Result<(), QuizError>;
}
#[derive(Debug, Default, Copy, Clone)]
pub enum TextFormat {
    #[default]
    HTML,
    Moodle,
    Markdown,
    PlainText,
}
impl TextFormat {
    pub fn name(&self) -> &'static str {
        match self {
            TextFormat::HTML => "html",
            TextFormat::Moodle => "moodle_auto_format",
            TextFormat::Markdown => "markdown",
            TextFormat::PlainText => "plain_text",
        }
    }
}
#[derive(Debug, Clone)]
struct QuestionBase {
    pub name: String,
    pub description: String,
    pub question_text_format: TextFormat,
    pub answers: Vec<Answer>,
}
impl QuestionBase {
    fn new(name: String, description: String) -> Self {
        Self {
            name,
            description,
            question_text_format: TextFormat::default(),
            answers: Vec::new(),
        }
    }
    fn check_answer_fraction(&mut self) -> Result<(), QuizError> {
        let mut total_fraction = 0usize;
        for answer in &self.answers {
            total_fraction += answer.fraction as usize;
        }
        if total_fraction < 100 {
            self.answers.clear();
            return Err(QuizError::AnswerFractionError(
                "The total fraction of answers must be at least 100".to_string(),
            ));
        }
        Ok(())
    }
}
impl Question for QuestionBase {
    fn get_name(&self) -> &str {
        self.name.as_str()
    }
    fn get_description(&self) -> &str {
        self.description.as_str()
    }
    fn set_text_format(&mut self, format: TextFormat) {
        self.question_text_format = format;
    }
    fn add_answers(&mut self, answers: Vec<Answer>) -> Result<(), QuizError> {
        self.answers.extend(answers);
        self.check_answer_fraction()?;
        Ok(())
    }
    fn to_xml(&self, writer: &mut EventWriter<&File>) -> Result<(), QuizError> {
        writer.write(XmlEvent::start_element("name"))?;
        write_text_tag(writer, self.name.as_str(), false)?;
        writer.write(XmlEvent::end_element())?;
        writer.write(
            XmlEvent::start_element("questiontext")
                .attr("format", self.question_text_format.name()),
        )?;
        write_text_tag(writer, self.description.as_str(), true)?;
        writer.write(XmlEvent::end_element())?;
        if self.answers.is_empty() {
            return Err(EmptyError.into());
        }
        for answer in &self.answers {
            answer.to_xml(writer)?;
        }
        Ok(())
    }
}
#[derive(Debug, Clone)]
pub struct MultiChoiceQuestion {
    base: QuestionBase,
    pub single: bool,
    pub shuffleanswers: bool, pub correctfeedback: String,
    pub partiallycorrectfeedback: String,
    pub incorrectfeedback: String,
    pub answernumbering: String,
}
impl MultiChoiceQuestion {
    #[allow(clippy::too_many_arguments)]
    pub fn new(
        name: String,
        description: String,
        single: Option<bool>,
        shuffleanswers: Option<bool>,
        correctfeedback: Option<String>,
        partiallycorrectfeedback: Option<String>,
        incorrectfeedback: Option<String>,
        answernumbering: Option<String>,
    ) -> Self {
        Self {
            base: QuestionBase::new(name, description),
            single: single.unwrap_or(true),
            shuffleanswers: shuffleanswers.unwrap_or(true),
            correctfeedback: correctfeedback.unwrap_or_default(),
            partiallycorrectfeedback: partiallycorrectfeedback.unwrap_or_default(),
            incorrectfeedback: incorrectfeedback.unwrap_or_default(),
            answernumbering: answernumbering.unwrap_or_default(),
        }
    }
}
impl Question for MultiChoiceQuestion {
    fn get_name(&self) -> &str {
        self.base.get_name()
    }
    fn get_description(&self) -> &str {
        self.base.get_description()
    }
    fn set_text_format(&mut self, format: TextFormat) {
        self.base.question_text_format = format;
    }
    fn add_answers(&mut self, answers: Vec<Answer>) -> Result<(), QuizError> {
        self.base.add_answers(answers)
    }
    fn to_xml(&self, writer: &mut EventWriter<&File>) -> Result<(), QuizError> {
        writer.write(XmlEvent::start_element("question").attr("type", "multichoice"))?;
        self.base.to_xml(writer)?;
        write_named_formatted_scope(writer, "single", None, |writer| {
            writer.write(XmlEvent::characters(&self.single.to_string()))?;
            Ok(())
        })?;
        write_named_formatted_scope(writer, "shuffleanswers", None, |writer| {
            writer.write(XmlEvent::characters(
                &(self.shuffleanswers as u8).to_string(),
            ))?;
            Ok(())
        })?;
        write_named_formatted_scope(
            writer,
            "correctfeedback",
            TextFormat::default().into(),
            |writer| write_text_tag(writer, &self.correctfeedback, false),
        )?;
        write_named_formatted_scope(
            writer,
            "partiallycorrectfeedback",
            TextFormat::default().into(),
            |writer| write_text_tag(writer, &self.partiallycorrectfeedback, false),
        )?;
        write_named_formatted_scope(
            writer,
            "incorrectfeedback",
            TextFormat::default().into(),
            |writer| write_text_tag(writer, &self.incorrectfeedback, false),
        )?;
        write_named_formatted_scope(writer, "answernumbering", None, |writer| {
            writer.write(XmlEvent::characters(&self.answernumbering.to_string()))?;
            Ok(())
        })?;
        writer.write(XmlEvent::end_element())?;
        Ok(())
    }
}
#[derive(Debug, Clone)]
pub struct TrueFalseQuestion {
    base: QuestionBase,
}
impl TrueFalseQuestion {
    pub fn new(name: String, description: String) -> Self {
        Self {
            base: QuestionBase::new(name, description),
        }
    }
}
impl Question for TrueFalseQuestion {
    fn get_name(&self) -> &str {
        self.base.get_name()
    }
    fn get_description(&self) -> &str {
        self.base.get_description()
    }
    fn set_text_format(&mut self, format: TextFormat) {
        self.base.question_text_format = format;
    }
    fn add_answers(&mut self, answers: Vec<Answer>) -> Result<(), QuizError> {
        if answers.len() != 2 {
            return Err(QuizError::AnswerCountError(
                "True/False questions must have exactly 2 answers".to_string(),
            ));
        }
        if answers[0].fraction == 100 {
            if answers[1].fraction == 0 {
                } else {
                return Err(QuizError::AnswerFractionError(
                    "Only fractions 100 and 0 are allowed in True/False questions".to_string(),
                ));
            }
        } else if answers[1].fraction == 100 {
            if answers[0].fraction == 0 {
                } else {
                return Err(QuizError::AnswerFractionError(
                    "Only fractions 100 and 0 are allowed in True/False questions".to_string(),
                ));
            }
        } else {
            return Err(QuizError::AnswerFractionError(
                "Only fractions 100 and 0 are allowed in True/False questions".to_string(),
            ));
        }
        self.base.add_answers(answers)
    }
    fn to_xml(&self, writer: &mut EventWriter<&File>) -> Result<(), QuizError> {
        writer.write(XmlEvent::start_element("question").attr("type", "truefalse"))?;
        self.base.to_xml(writer)?;
        writer.write(XmlEvent::end_element())?;
        Ok(())
    }
}
#[derive(Debug, Clone)]
pub struct ShortAnswerQuestion {
    base: QuestionBase,
    pub usecase: bool,
}
impl ShortAnswerQuestion {
    pub fn new(name: String, description: String, usecase: Option<bool>) -> Self {
        Self {
            base: QuestionBase::new(name, description),
            usecase: usecase.unwrap_or_default(),
        }
    }
}
impl Question for ShortAnswerQuestion {
    fn get_name(&self) -> &str {
        self.base.get_name()
    }
    fn get_description(&self) -> &str {
        self.base.get_description()
    }
    fn set_text_format(&mut self, format: TextFormat) {
        self.base.question_text_format = format;
    }
    fn add_answers(&mut self, answers: Vec<Answer>) -> Result<(), QuizError> {
        self.base.add_answers(answers)
    }
    fn to_xml(&self, writer: &mut EventWriter<&File>) -> Result<(), QuizError> {
        writer.write(XmlEvent::start_element("question").attr("type", "shortanswer"))?;
        self.base.to_xml(writer)?;
        write_named_formatted_scope(writer, "usecase", None, |writer| {
            writer.write(XmlEvent::characters(&(self.usecase as u8).to_string()))?;
            Ok(())
        })?;
        writer.write(XmlEvent::end_element())?;
        Ok(())
    }
}
#[derive(Debug, Clone)]
pub struct EssayQuestion {
    base: QuestionBase,
}
impl EssayQuestion {
    pub fn new(name: String, description: String) -> Self {
        Self {
            base: QuestionBase::new(name, description),
        }
    }
}
impl Question for EssayQuestion {
    fn get_name(&self) -> &str {
        self.base.get_name()
    }
    fn get_description(&self) -> &str {
        self.base.get_description()
    }
    fn set_text_format(&mut self, format: TextFormat) {
        self.base.question_text_format = format;
    }
    fn add_answers(&mut self, answers: Vec<Answer>) -> Result<(), QuizError> {
        if !answers.is_empty() {
            return Err(QuizError::AnswerCountError(
                "Essay questions must not have any answers".to_string(),
            ));
        }
        Ok(())
    }
    fn to_xml(&self, writer: &mut EventWriter<&File>) -> Result<(), QuizError> {
        writer.write(XmlEvent::start_element("question").attr("type", "essay"))?;
        self.base.to_xml(writer)?;
        writer.write(XmlEvent::end_element())?;
        Ok(())
    }
}
pub enum QuestionType {
    Multichoice(MultiChoiceQuestion),
    TrueFalse(TrueFalseQuestion),
    ShortAnswer(ShortAnswerQuestion),
    Essay(EssayQuestion),
    }
impl QuestionType {
    pub fn to_xml(&self, writer: &mut EventWriter<&File>) -> Result<(), QuizError> {
        match self {
            QuestionType::Multichoice(q) => q.to_xml(writer),
            QuestionType::TrueFalse(q) => q.to_xml(writer),
            QuestionType::ShortAnswer(q) => q.to_xml(writer),
            QuestionType::Essay(q) => q.to_xml(writer),
        }
    }
}
macro_rules! impl_from_question {
    ($(($question_type:ty, $variant:ident)),+) => {
        $(
            impl<Q> From<$question_type> for Vec<Q>
            where
                Q: Question,
                $question_type: Into<Q>,
            {
                fn from(question: $question_type) -> Self {
                    vec![question.into()]
                }
            }
            impl From<$question_type> for Vec<Box<dyn Question>>
            where
                $question_type: Question + 'static,
            {
                fn from(question: $question_type) -> Self {
                    vec![Box::new(question)]
                }
            }
            impl From<$question_type> for QuestionType {
                fn from(question: $question_type) -> Self {
                    QuestionType::$variant(question)
                }
            }
            impl From<$question_type> for Vec<QuestionType> {
                fn from(question: $question_type) -> Self {
                    vec![QuestionType::$variant(question)]
                }
            }
        )+
    };
}
impl_from_question!(
    (MultiChoiceQuestion, Multichoice),
    (TrueFalseQuestion, TrueFalse),
    (ShortAnswerQuestion, ShortAnswer),
    (EssayQuestion, Essay)
);
#[cfg(test)]
mod tests {
    use super::*;
    use std::io::{Read, Seek};
    use xml::writer::EmitterConfig;
    #[test]
    fn test_multichoice_question_xml() {
        let mut tmp_file = tempfile::tempfile().unwrap();
        let mut writer = EmitterConfig::new()
            .perform_indent(true)
            .create_writer(&tmp_file);
        let multichoice_question = MultiChoiceQuestion {
            base: QuestionBase {
                name: "Name of question".to_string(),
                description: "What is the answer to this question?".to_string(),
                question_text_format: TextFormat::HTML,
                answers: vec![
                    Answer {
                        fraction: 100,
                        text: "The correct answer".to_string(),
                        feedback: "Correct!".to_string().into(),
                        text_format: TextFormat::HTML,
                    },
                    Answer {
                        fraction: 0,
                        text: "A distractor".to_string(),
                        feedback: "Ooops!".to_string().into(),
                        text_format: TextFormat::HTML,
                    },
                    Answer {
                        fraction: 0,
                        text: "Another distractor".to_string(),
                        feedback: "Ooops!".to_string().into(),
                        text_format: TextFormat::HTML,
                    },
                ],
            },
            single: true,
            shuffleanswers: true,
            correctfeedback: "Correct!".to_string(),
            partiallycorrectfeedback: "Partially correct!".to_string(),
            incorrectfeedback: "Incorrect!".to_string(),
            answernumbering: "abc".to_string(),
        };
        multichoice_question.to_xml(&mut writer).unwrap();
        let mut buf = String::new();
        tmp_file.seek(std::io::SeekFrom::Start(0)).unwrap();
        tmp_file.read_to_string(&mut buf).unwrap();
        let expected = r#"<?xml version="1.0" encoding="utf-8"?>
<question type="multichoice">
  <name>
    <text>Name of question</text>
  </name>
  <questiontext format="html">
    <text><![CDATA[What is the answer to this question?]]></text>
  </questiontext>
  <answer fraction="100" format="html">
    <text>The correct answer</text>
    <feedback format="html">
      <text>Correct!</text>
    </feedback>
  </answer>
  <answer fraction="0" format="html">
    <text>A distractor</text>
    <feedback format="html">
      <text>Ooops!</text>
    </feedback>
  </answer>
  <answer fraction="0" format="html">
    <text>Another distractor</text>
    <feedback format="html">
      <text>Ooops!</text>
    </feedback>
  </answer>
  <single>true</single>
  <shuffleanswers>1</shuffleanswers>
  <correctfeedback format="html">
    <text>Correct!</text>
  </correctfeedback>
  <partiallycorrectfeedback format="html">
    <text>Partially correct!</text>
  </partiallycorrectfeedback>
  <incorrectfeedback format="html">
    <text>Incorrect!</text>
  </incorrectfeedback>
  <answernumbering>abc</answernumbering>
</question>"#;
        assert_eq!(expected, buf);
    }
    #[test]
    fn test_truefalse_question_xml() {
        let mut tmp_file = tempfile::tempfile().unwrap();
        let mut writer = EmitterConfig::new()
            .perform_indent(true)
            .create_writer(&tmp_file);
        let truefalse_question = TrueFalseQuestion {
            base: QuestionBase {
                name: "Name of question".to_string(),
                description: "What is the answer to this question?".to_string(),
                question_text_format: TextFormat::HTML,
                answers: vec![
                    Answer {
                        fraction: 100,
                        text: "True".to_string(),
                        feedback: "Correct!".to_string().into(),
                        text_format: TextFormat::HTML,
                    },
                    Answer {
                        fraction: 0,
                        text: "False".to_string(),
                        feedback: "Ooops!".to_string().into(),
                        text_format: TextFormat::HTML,
                    },
                ],
            },
        };
        truefalse_question.to_xml(&mut writer).unwrap();
        let mut buf = String::new();
        tmp_file.seek(std::io::SeekFrom::Start(0)).unwrap();
        tmp_file.read_to_string(&mut buf).unwrap();
        let expected = r#"<?xml version="1.0" encoding="utf-8"?>
<question type="truefalse">
  <name>
    <text>Name of question</text>
  </name>
  <questiontext format="html">
    <text><![CDATA[What is the answer to this question?]]></text>
  </questiontext>
  <answer fraction="100" format="html">
    <text>True</text>
    <feedback format="html">
      <text>Correct!</text>
    </feedback>
  </answer>
  <answer fraction="0" format="html">
    <text>False</text>
    <feedback format="html">
      <text>Ooops!</text>
    </feedback>
  </answer>
</question>"#;
        assert_eq!(expected, buf);
    }
}