// +--------------------------------------------------------------------------+
// | Copyright 2016 Matthew D. Steele <mdsteele@alum.mit.edu>                 |
// |                                                                          |
// | This file is part of System Syzygy.                                      |
// |                                                                          |
// | System Syzygy 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.                                               |
// |                                                                          |
// | System Syzygy is distributed in the hope that it will be useful, but     |
// | WITHOUT ANY WARRANTY; without even the implied warranty of               |
// | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU        |
// | General Public License for details.                                      |
// |                                                                          |
// | You should have received a copy of the GNU General Public License along  |
// | with System Syzygy.  If not, see <http://www.gnu.org/licenses/>.         |
// +--------------------------------------------------------------------------+

use std::char;
use std::collections::HashSet;
use toml;

use super::PuzzleState;
use crate::save::util::{to_table, Tomlable, ACCESS_KEY};
use crate::save::{Access, Location};

// ========================================================================= //

const SEQUENCE_KEY: &str = "sequence";

const INITIAL_LETTERS: &[char] = &['L', 'E', 'G', 'E', 'N', 'D', 'S'];
const SOLVED_LETTERS: &[char] = &['H', 'I', 'S', 'T', 'O', 'R', 'Y'];
const SOLVED_SEQUENCE: &[i8] = &[2, 3, 0, 1, 4];

// ========================================================================= //

pub struct FictionState {
    access: Access,
    sequence: Vec<i8>,
    letters: Vec<char>,
}

impl FictionState {
    pub fn solve(&mut self) {
        self.access = Access::Solved;
        self.sequence = SOLVED_SEQUENCE.to_vec();
        self.regenerate_letters_from_sequence();
        debug_assert_eq!(&self.letters as &[char], SOLVED_LETTERS);
    }

    pub fn sequence(&self) -> &Vec<i8> {
        &self.sequence
    }

    pub fn set_sequence(&mut self, sequence: Vec<i8>) {
        self.sequence = sequence;
        self.regenerate_letters_from_sequence();
    }

    pub fn letters(&self) -> &Vec<char> {
        &self.letters
    }

    pub fn has_used(&self, index: i8) -> bool {
        self.sequence.contains(&index)
    }

    pub fn append(&mut self, index: i8) {
        assert!(index >= 0 && index < 5);
        assert!(!self.has_used(index));
        self.sequence.push(index);
        apply_transformation(&mut self.letters, index);
        if &self.letters as &[char] == SOLVED_LETTERS {
            self.access = Access::Solved;
        }
    }

    fn regenerate_letters_from_sequence(&mut self) {
        self.letters = INITIAL_LETTERS.to_vec();
        for &index in &self.sequence {
            apply_transformation(&mut self.letters, index);
        }
    }
}

impl PuzzleState for FictionState {
    fn location() -> Location {
        Location::FactOrFiction
    }

    fn access(&self) -> Access {
        self.access
    }

    fn access_mut(&mut self) -> &mut Access {
        &mut self.access
    }

    fn can_reset(&self) -> bool {
        !self.sequence.is_empty()
    }

    fn reset(&mut self) {
        self.sequence.clear();
        self.regenerate_letters_from_sequence();
    }
}

impl Tomlable for FictionState {
    fn to_toml(&self) -> toml::Value {
        let mut table = toml::value::Table::new();
        table.insert(ACCESS_KEY.to_string(), self.access.to_toml());
        if !self.is_solved() && !self.sequence.is_empty() {
            let seq = self
                .sequence
                .iter()
                .map(|&idx| toml::Value::Integer(idx as i64))
                .collect();
            table.insert(SEQUENCE_KEY.to_string(), toml::Value::Array(seq));
        }
        toml::Value::Table(table)
    }

    fn from_toml(value: toml::Value) -> FictionState {
        let mut table = to_table(value);
        let access = Access::pop_from_table(&mut table, ACCESS_KEY);
        let sequence = if access.is_solved() {
            SOLVED_SEQUENCE.iter().cloned().collect()
        } else {
            let mut seq = Vec::<i8>::pop_from_table(&mut table, SEQUENCE_KEY);
            seq.retain(|&idx| 0 <= idx && idx < 5);
            let unique: HashSet<i8> = seq.iter().cloned().collect();
            if unique.len() != seq.len() {
                Vec::new()
            } else {
                seq
            }
        };
        let mut state = FictionState { access, sequence, letters: Vec::new() };
        state.regenerate_letters_from_sequence();
        state
    }
}

// ========================================================================= //

fn increment_letter(letter: char, by: u32) -> char {
    debug_assert!(letter >= 'A' && letter <= 'Z');
    char::from_u32((letter as u32 - 'A' as u32 + by) % 26 + 'A' as u32)
        .unwrap()
}

fn apply_transformation(letters: &mut Vec<char>, index: i8) {
    match index {
        0 => {
            letters.swap(0, 6);
            letters.swap(1, 5);
        }
        1 => {
            for letter in letters.iter_mut() {
                *letter = match *letter {
                    'A' => 'U',
                    'E' => 'A',
                    'I' => 'E',
                    'O' => 'I',
                    'U' => 'O',
                    ltr => ltr,
                }
            }
        }
        2 => {
            letters[0] = increment_letter(letters[0], 13);
            letters[1] = increment_letter(letters[1], 13);
            letters[2] = increment_letter(letters[2], 13);
            letters[3] = increment_letter(letters[3], 7);
            letters[4] = increment_letter(letters[4], 7);
            letters[5] = increment_letter(letters[5], 7);
        }
        3 => {
            letters[3] = increment_letter(letters[3], 22);
            letters[5] = increment_letter(letters[5], 4);
        }
        4 => {
            letters[0..4].sort();
        }
        _ => panic!("bad transformation index: {}", index),
    }
}

// ========================================================================= //

#[cfg(test)]
mod tests {
    use toml;

    use super::{
        apply_transformation, FictionState, INITIAL_LETTERS, SEQUENCE_KEY,
        SOLVED_LETTERS, SOLVED_SEQUENCE,
    };
    use crate::save::util::{Tomlable, ACCESS_KEY};
    use crate::save::Access;

    #[test]
    fn transform_letters() {
        let mut letters = "AWESOME".chars().collect::<Vec<char>>();
        apply_transformation(&mut letters, 0);
        assert_eq!(letters, "EMESOWA".chars().collect::<Vec<char>>());

        let mut letters = "AWESOME".chars().collect::<Vec<char>>();
        apply_transformation(&mut letters, 1);
        assert_eq!(letters, "UWASIMA".chars().collect::<Vec<char>>());

        let mut letters = "AWESOME".chars().collect::<Vec<char>>();
        apply_transformation(&mut letters, 2);
        assert_eq!(letters, "NJRZVTE".chars().collect::<Vec<char>>());

        let mut letters = "AWESOME".chars().collect::<Vec<char>>();
        apply_transformation(&mut letters, 3);
        assert_eq!(letters, "AWEOOQE".chars().collect::<Vec<char>>());

        let mut letters = "AWESOME".chars().collect::<Vec<char>>();
        apply_transformation(&mut letters, 4);
        assert_eq!(letters, "AESWOME".chars().collect::<Vec<char>>());
    }

    #[test]
    fn toml_round_trip() {
        let mut state = FictionState::from_toml(toml::Value::Boolean(false));
        state.access = Access::Replaying;
        state.append(3);
        state.append(1);
        state.append(4);
        let letters = state.letters.clone();

        let state = FictionState::from_toml(state.to_toml());
        assert_eq!(state.access, Access::Replaying);
        assert_eq!(state.sequence, vec![3, 1, 4]);
        assert_eq!(letters, state.letters);
    }

    #[test]
    fn from_empty_toml() {
        let state = FictionState::from_toml(toml::Value::Boolean(false));
        assert_eq!(state.access, Access::Unvisited);
        assert_eq!(state.sequence, vec![]);
        assert_eq!(state.letters, INITIAL_LETTERS.to_vec());
    }

    #[test]
    fn from_solved_toml() {
        let mut table = toml::value::Table::new();
        table.insert(ACCESS_KEY.to_string(), Access::Solved.to_toml());

        let state = FictionState::from_toml(toml::Value::Table(table));
        assert_eq!(state.access, Access::Solved);
        assert_eq!(state.sequence, SOLVED_SEQUENCE.to_vec());
        assert_eq!(state.letters, SOLVED_LETTERS.to_vec());
    }

    #[test]
    fn from_valid_sequence_toml() {
        let mut table = toml::value::Table::new();
        table.insert(
            SEQUENCE_KEY.to_string(),
            toml::Value::Array(vec![
                toml::Value::Integer(1),
                toml::Value::Integer(2),
                toml::Value::Integer(3),
            ]),
        );
        let state = FictionState::from_toml(toml::Value::Table(table));
        assert_eq!(state.sequence, vec![1, 2, 3]);
    }

    #[test]
    fn from_invalid_repeat_sequence_toml() {
        let mut table = toml::value::Table::new();
        table.insert(
            SEQUENCE_KEY.to_string(),
            toml::Value::Array(vec![toml::Value::Integer(1); 2]),
        );
        let state = FictionState::from_toml(toml::Value::Table(table));
        assert_eq!(state.sequence, vec![]);
        assert_eq!(state.letters, INITIAL_LETTERS.to_vec());
    }

    #[test]
    fn from_invalid_index_sequence_toml() {
        let mut table = toml::value::Table::new();
        table.insert(
            SEQUENCE_KEY.to_string(),
            toml::Value::Array(vec![toml::Value::Integer(5)]),
        );
        let state = FictionState::from_toml(toml::Value::Table(table));
        assert_eq!(state.sequence, vec![]);
        assert_eq!(state.letters, INITIAL_LETTERS.to_vec());
    }
}

// ========================================================================= //
