use crate::{Dialogue, Ending, Font, Image, Item, Palette, Room, Sprite, TextDirection, Tile, Variable, transform_line_endings, segments_from_string, new_unique_id, try_id, Instance, Error};

use loe::TransformMode;

use std::collections::HashMap;
use std::borrow::BorrowMut;
use std::fmt;

/// in very early versions of Bitsy, room tiles were defined as single alphanumeric characters -
/// so there was a maximum of 36 unique tiles. later versions are comma-separated.
/// RoomFormat is implemented here so we can save in the original format.
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
pub enum RoomFormat {Contiguous, CommaSeparated}

#[derive(Debug)]
pub struct InvalidRoomFormat;

impl RoomFormat {
    fn from(str: &str) -> Result<RoomFormat, InvalidRoomFormat> {
        match str {
            "0" => Ok(RoomFormat::Contiguous),
            "1" => Ok(RoomFormat::CommaSeparated),
            _   => Err(InvalidRoomFormat),
        }
    }
}

impl fmt::Display for RoomFormat {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", match &self {
            RoomFormat::Contiguous     => 0,
            RoomFormat::CommaSeparated => 1,
        })
    }
}

/// in very early versions of Bitsy, a room was called a "set"
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
pub enum RoomType {Room, Set}

impl ToString for RoomType {
    fn to_string(&self) -> String {
        match &self {
            RoomType::Set  => "SET",
            RoomType::Room => "ROOM",
        }.to_string()
    }
}

#[derive(Debug, Eq, PartialEq, Copy, Clone)]
pub struct Version {
    pub major: u8,
    pub minor: u8,
}

#[derive(Debug)]
pub enum VersionError {
    MissingParts,
    ExtraneousParts,
    MalformedInteger,
}

impl fmt::Display for VersionError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", match self {
            VersionError::MissingParts     => "Not enough parts supplied for version",
            VersionError::ExtraneousParts  => "Too many parts supplied for version",
            VersionError::MalformedInteger => "Version did not contain valid integers",
        })
    }
}

impl std::error::Error for VersionError {}

impl Version {
    fn from(str: &str) -> Result<Version, VersionError> {
        let parts: Vec<&str> = str.split('.').collect();

        if parts.len() < 2 {
            Err(VersionError::MissingParts)
        } else if parts.len() > 2 {
            Err(VersionError::ExtraneousParts)
        } else if let (Ok(major), Ok(minor)) = (parts[0].parse(), parts[1].parse()) {
            Ok(Version { major, minor })
        } else {
            Err(VersionError::MalformedInteger)
        }
    }
}

#[derive(Clone, Debug, PartialEq)]
pub struct Game {
    pub name: String,
    pub version: Option<Version>,
    pub room_format: Option<RoomFormat>,
    pub(crate) room_type: RoomType,
    pub font: Font,
    pub custom_font: Option<String>, // used if font is Font::Custom
    pub text_direction: TextDirection,
    pub palettes: Vec<Palette>,
    pub rooms: Vec<Room>,
    pub tiles: Vec<Tile>,
    pub sprites: Vec<Sprite>,
    pub items: Vec<Item>,
    pub dialogues: Vec<Dialogue>,
    pub endings: Vec<Ending>,
    pub variables: Vec<Variable>,
    pub font_data: Option<String>, // todo make this an actual struct for parsing
    pub(crate) line_endings_crlf: bool, // otherwise lf (unix/mac)
}

impl Game {
    pub fn from(string: String) -> Result<(Game, Vec<crate::Error>), crate::error::NotFound> {
        if string.trim() == "" {
            return Err(crate::error::NotFound::Anything);
        }

        let mut warnings = Vec::new();

        let line_endings_crlf = string.contains("\r\n");
        let mut string = string;
        if line_endings_crlf {
            string = transform_line_endings(string, TransformMode::LF)
        }

        let string = string.trim_start_matches('\n').to_string();
        let mut segments = segments_from_string(string);

        let mut name = "".to_string();

        // game names can be empty - so when we strip out the leading whitespace above,
        // it means that the first segment might not be the game name.
        // so, check if the first segment is actually the next segment of game data
        // to avoid setting the game name to "# BITSY VERSION 7.0" or something
        if
            segments[0].starts_with("\"\"\"") // multi-line game name
            ||
            (
                ! segments[0].starts_with("# BITSY VERSION ")
                &&
                ! segments[0].starts_with("! ROOM_FORMAT ")
                &&
                ! segments[0].starts_with("PAL ")
                &&
                ! segments[0].starts_with("DEFAULT_FONT ")
                &&
                ! segments[0].starts_with("TEXT_DIRECTION ")
            )
        {
            name = segments[0].to_string();
            segments = segments[1..].to_owned();
        }

        let segments = segments;

        let name = name;
        let mut dialogues: Vec<Dialogue> = Vec::new();
        let mut endings: Vec<Ending> = Vec::new();
        let mut variables: Vec<Variable> = Vec::new();
        let mut font_data: Option<String> = None;

        let mut version = None;
        let mut room_format = None;
        let mut room_type = RoomType::Room;
        let mut font = Font::AsciiSmall;
        let mut custom_font = None;
        let mut text_direction = TextDirection::LeftToRight;
        let mut palettes: Vec<Palette> = Vec::new();
        let mut rooms: Vec<Room> = Vec::new();
        let mut tiles: Vec<Tile> = Vec::new();
        let mut sprites: Vec<Sprite> = Vec::new();
        let mut items: Vec<Item> = Vec::new();
        // let mut avatar_exists = false;

        for segment in segments {
            if segment.starts_with("# BITSY VERSION") {
                let segment = segment.replace("# BITSY VERSION ", "");
                let result = Version::from(&segment);

                if let Ok(v) = result {
                    version = Some(v);
                } else {
                    warnings.push(Error::Version);
                }
            } else if segment.starts_with("! ROOM_FORMAT") {
                let segment = segment.replace("! ROOM_FORMAT ", "");
                room_format = Some(
                    RoomFormat::from(&segment).unwrap_or(RoomFormat::CommaSeparated)
                );
            } else if segment.starts_with("DEFAULT_FONT") {
                let segment = segment.replace("DEFAULT_FONT ", "");

                font = Font::from(&segment);

                if font == Font::Custom {
                    custom_font = Some(segment.to_string());
                }
            } else if segment.trim() == "TEXT_DIRECTION RTL" {
                text_direction = TextDirection::RightToLeft;
            } else if segment.starts_with("PAL ") {
                let result = Palette::from_str(&segment);
                if let Ok((palette, mut errors)) = result {
                    palettes.push(palette);
                    warnings.append(&mut errors);
                } else {
                    warnings.push(result.unwrap_err());
                }
            } else if segment.starts_with("ROOM ") || segment.starts_with("SET ") {
                if segment.starts_with("SET ") {
                    room_type = RoomType::Set;
                }
                rooms.push(Room::from(segment));
            } else if segment.starts_with("TIL ") {
                tiles.push(Tile::from(segment));
            } else if segment.starts_with("SPR ") {
                let sprite = Sprite::from(segment);

                if let Ok(sprite) = sprite {
                    // avatar_exists |= sprite.id == "A";

                    sprites.push(sprite);
                }
            } else if segment.starts_with("ITM ") {
                items.push(Item::from(segment));
            } else if segment.starts_with("DLG ") {
                let result = Dialogue::from_str(&segment);

                if let Ok(dialogue) = result {
                    dialogues.push(dialogue);
                } else {
                    warnings.push(result.unwrap_err());
                }
            } else if segment.starts_with("END ") {
                let result = Ending::from_str(&segment);
                if let Ok(ending) = result {
                    endings.push(ending);
                } else {
                    warnings.push(result.unwrap_err());
                }
            } else if segment.starts_with("VAR ") {
                variables.push(Variable::from(segment));
            } else if segment.starts_with("FONT ") {
                font_data = Some(segment);
            }
        }

        // if ! avatar_exists {
        //     return Err(crate::Error::NotFound::Avatar);
        // }

        Ok(
            (
                Game {
                    name,
                    version,
                    room_format,
                    room_type,
                    font,
                    custom_font,
                    text_direction,
                    palettes,
                    rooms,
                    tiles,
                    sprites,
                    items,
                    dialogues,
                    endings,
                    variables,
                    font_data,
                    line_endings_crlf,
                },
                warnings
            )
        )
    }

    /// todo refactor this into "get T by ID", taking a Vec<T> and an ID name?
    pub fn get_sprite_by_id(&self, id: String) -> Result<&Sprite, crate::error::NotFound> {
        let index = self.sprites.iter().position(
            |sprite| sprite.id == id
        );

        match index {
            Some(index) => Ok(&self.sprites[index]),
            None => Err(crate::error::NotFound::Sprite),
        }
    }

    pub fn get_tile_by_id(&self, id: String) -> Result<&Tile, crate::error::NotFound> {
        let index = self.tiles.iter().position(
            |tile| tile.id == id
        );

        match index {
            Some(index) => Ok(&self.tiles[index]),
            None => Err(crate::error::NotFound::Tile),
        }
    }

    pub fn get_room_by_id(&self, id: String) -> Result<&Room, crate::error::NotFound> {
        let index = self.rooms.iter().position(
            |room| room.id == id
        );

        match index {
            Some(index) => Ok(&self.rooms[index]),
            None => Err(crate::error::NotFound::Room),
        }
    }

    pub fn get_avatar(&self) -> Result<&Sprite, crate::error::NotFound> {
        self.get_sprite_by_id("A".to_string())
    }

    // todo result
    pub fn get_tiles_by_ids(&self, ids: Vec<String>) -> Vec<&Tile> {
        let mut tiles: Vec<&Tile> = Vec::new();

        for id in ids {
            if let Ok(tile) = self.get_tile_by_id(id) {
                tiles.push(tile);
            }
        }

        tiles
    }

    pub fn get_tiles_for_room(&self, id: String) -> Result<Vec<&Tile>, crate::error::NotFound> {
        let room = self.get_room_by_id(id)?;
        let mut tile_ids = room.tiles.clone();
        tile_ids.sort();
        tile_ids.dedup();
        // remove 0 as this isn't a real tile
        let zero_index  = tile_ids.iter()
            .position(|i| i == "0");
        if let Some(zero_index) = zero_index {
            tile_ids.remove(zero_index);
        }
        // remove Ok once this function returns a result
        Ok(self.get_tiles_by_ids(tile_ids))
    }

    // return? array of changes made? error/ok?
    pub fn merge(&mut self, game: &Game) {
        // ignore title, version, room format, room type, font, text direction

        let mut palette_id_changes:  HashMap<String, String> = HashMap::new();
        let mut tile_id_changes:     HashMap<String, String> = HashMap::new();
        let mut dialogue_id_changes: HashMap<String, String> = HashMap::new();
        let mut ending_id_changes:   HashMap<String, String> = HashMap::new();
        let mut item_id_changes:     HashMap<String, String> = HashMap::new();
        let mut room_id_changes:     HashMap<String, String> = HashMap::new();
        let mut sprite_id_changes:   HashMap<String, String> = HashMap::new();

        fn insert_if_different(map: &mut HashMap<String, String>, old: String, new: String) {
            if old != new && ! map.contains_key(&old) {
                map.insert(old, new);
            }
        }

        // alternatively - instead of handling these types in a specific order,
        // we could calculate the new IDs for each type first,
        // then handle the sections one by one

        // a room has a palette, so handle palettes before rooms
        for palette in &game.palettes {
            insert_if_different(
                palette_id_changes.borrow_mut(),
                palette.id.clone(),
                self.add_palette(palette.clone())
            );
        }

        // a room has tiles, so handle before room
        for tile in &game.tiles {
            insert_if_different(
                tile_id_changes.borrow_mut(),
                tile.id.clone(),
                self.add_tile(tile.clone())
            );
        }

        for variable in &game.variables {
            // don't change ID - just avoid duplicates
            if ! self.variable_ids().contains(&variable.id) {
                self.add_variable(variable.clone());
            }
        }

        for item in &game.items {
            let old_id = item.id.clone();
            let new_id = try_id(&self.item_ids(), &item.id);
            insert_if_different(item_id_changes.borrow_mut(), old_id, new_id)
        }

        // a sprite has a dialogue, so handle before sprites
        // dialogue can have variables, so handle before after variables
        for dialogue in &game.dialogues {
            let mut dialogue = dialogue.clone();

            for (old, new) in &item_id_changes {
                // todo is there a better way of doing this?
                dialogue.contents = dialogue.contents.replace(
                    &format!("item \"{}\"", old),
                    &format!("item \"{}\"", new)
                );
            }

            let old_id = dialogue.id.clone();
            let new_id = self.add_dialogue(dialogue);
            insert_if_different(dialogue_id_changes.borrow_mut(), old_id, new_id);
        }

        // an ending lives in a room, so handle endings before rooms
        for ending in &game.endings {
            insert_if_different(
                ending_id_changes.borrow_mut(),
                ending.id.clone(),
                self.add_ending(ending.clone())
            );
        }

        // an item has a dialogue ID, so we need to handle these after dialogues
        // an item instance lives in a room so these must be handled before rooms
        for item in &game.items {
            let mut item = item.clone();

            if item_id_changes.contains_key(&item.id) {
                item.id = item_id_changes[&item.id].clone();
            }

            if let Some(key) = item.dialogue_id.clone() {
                if let Some(change) = dialogue_id_changes.get(&key) {
                    item.dialogue_id = Some(change.clone());
                }
            }

            self.add_item(item);
        }

        // calculate all of the new room IDs first
        // to insert any new room, we need to know the new IDs of every room
        // to maintain the integrity of exits and endings

        let mut all_room_ids = self.room_ids();

        for room in &game.rooms {
            let old = room.id.clone();
            let new = try_id(&all_room_ids, &room.id);
            insert_if_different(room_id_changes.borrow_mut(), old, new.clone());
            all_room_ids.push(new);
        }

        // needs to be handled after palettes, tiles, items, exits, endings
        // and before sprites
        for room in &game.rooms {
            let mut room = room.clone();

            if let Some(room_id_change) = room_id_changes.get(&room.id) {
                room.id = room_id_change.clone();
            }

            if let Some(key) = room.palette_id.clone() {
                if let Some(change) = palette_id_changes.get(&key) {
                    room.palette_id = Some(change.clone());
                }
            }

            room.change_tile_ids(&tile_id_changes);

            room.items = room.items.iter().map(|instance|
                if item_id_changes.contains_key(&instance.id) {
                    Instance {
                        position: instance.position.clone(),
                        id: item_id_changes[&instance.id].clone()
                    }
                } else {
                    instance.clone()
                }
            ).collect();

            room.exits = room.exits.iter().map(|exit| {
                let mut exit = exit.clone();

                let key = exit.exit.room_id.clone();

                if let Some(change) = room_id_changes.get(&key) {
                    exit.exit.room_id = change.clone();
                }

                if let Some(key) = exit.dialogue_id.clone() {
                    if let Some(dialogue_change) = dialogue_id_changes.get(&key) {
                        exit.dialogue_id = Some(dialogue_change.clone());
                    }
                }

                exit
            }).collect();

            room.endings = room.endings.iter().map(|ending| {
                let mut ending = ending.clone();
                let key = ending.id.clone();

                if let Some(change) = ending_id_changes.get(&key) {
                    ending.id = change.clone();
                }

                ending
            }).collect();

            self.add_room(room);
        }

        // a sprite has a dialogue ID, so we need to handle these after dialogues
        // a sprite has a position in a room, so we need to handle these after the rooms
        for sprite in &game.sprites {
            let mut sprite = sprite.clone();
            // avoid having two avatars
            if sprite.id == "A" {
                sprite.id = "0".to_string(); // just a default value for replacement
            }

            if let Some(key) = sprite.dialogue_id.clone() {
                if dialogue_id_changes.contains_key(&key) {
                    sprite.dialogue_id = Some(dialogue_id_changes[&key].clone());
                }
            }

            if let Some(key) = sprite.room_id.clone() {
                if let Some(change) = room_id_changes.get(&key) {
                    sprite.room_id = Some(change.clone());
                }
            }

            let old_id = sprite.id.clone();
            let new_id = self.add_sprite(sprite);
            insert_if_different(sprite_id_changes.borrow_mut(), old_id, new_id);
        }
    }
}

impl ToString for Game {
    fn to_string(&self) -> String {
        let mut segments: Vec<String> = Vec::new();

        // todo refactor

        for palette in &self.palettes {
            segments.push(palette.to_string());
        }

        for room in &self.rooms {
            segments.push(room.to_string(self.room_format(), self.room_type));
        }

        for tile in &self.tiles {
            segments.push(tile.to_string());
        }

        for sprite in &self.sprites {
            segments.push(sprite.to_string());
        }

        for item in &self.items {
            segments.push(item.to_string());
        }

        for dialogue in &self.dialogues {
            // this replacement is silly but see segments_from_string() for explanation
            segments.push(dialogue.to_string().replace("\"\"\"\n\"\"\"", ""));
        }

        for ending in &self.endings {
            segments.push(ending.to_string());
        }

        for variable in &self.variables {
            segments.push(variable.to_string());
        }

        if self.font_data.is_some() {
            segments.push(self.font_data.to_owned().unwrap())
        }

        transform_line_endings(
            format!(
                "{}{}{}{}{}\n\n{}\n\n",
                &self.name,
                &self.version_line(),
                &self.room_format_line(),
                &self.font_line(),
                &self.text_direction_line(),
                segments.join("\n\n"),
            ),
            if self.line_endings_crlf {TransformMode::CRLF} else {TransformMode::LF}
        )
    }
}

impl Game {
    // todo dedupe
    pub fn palette_ids(&self) -> Vec<String> {
        self.palettes.iter().map(|palette| palette.id.clone()).collect()
    }

    pub fn tile_ids(&self) -> Vec<String> {
        self.tiles.iter().map(|tile| tile.id.clone()).collect()
    }

    pub fn sprite_ids(&self) -> Vec<String> {
        self.sprites.iter().map(|sprite| sprite.id.clone()).collect()
    }
    pub fn room_ids(&self) -> Vec<String> {
        self.rooms.iter().map(|room| room.id.clone()).collect()
    }

    pub fn item_ids(&self) -> Vec<String> {
        self.items.iter().map(|item| item.id.clone()).collect()
    }

    pub fn dialogue_ids(&self) -> Vec<String> {
        self.dialogues.iter().map(|dialogue| dialogue.id.clone()).collect()
    }

    pub fn ending_ids(&self) -> Vec<String> {
        self.endings.iter().map(|ending| ending.id.clone()).collect()
    }

    pub fn variable_ids(&self) -> Vec<String> {
        self.variables.iter().map(|variable| variable.id.clone()).collect()
    }

    // todo dedupe?

    pub fn new_palette_id(&self) -> String {
        new_unique_id(self.palette_ids())
    }

    /// first available tile ID.
    /// e.g. if current tile IDs are [0, 2, 3] the result will be `1`
    ///      if current tile IDs are [0, 1, 2] the result will be `3`
    pub fn new_tile_id(&self) -> String {
        let mut ids = self.tile_ids();
        // don't allow 0 - this is a reserved ID for an implicit background tile
        ids.push("0".to_string());
        new_unique_id(ids)
    }

    pub fn new_sprite_id(&self) -> String {
        new_unique_id(self.sprite_ids())
    }

    pub fn new_room_id(&self) -> String {
        new_unique_id(self.room_ids())
    }

    pub fn new_item_id(&self) -> String {
        new_unique_id(self.item_ids())
    }

    pub fn new_dialogue_id(&self) -> String {
        new_unique_id(self.dialogue_ids())
    }

    pub fn new_ending_id(&self) -> String {
        new_unique_id(self.ending_ids())
    }

    pub fn new_variable_id(&self) -> String {
        new_unique_id(self.variable_ids())
    }

    /// todo refactor?
    pub fn get_tile_id(&self, matching_tile: &Tile) -> Option<String> {
        for tile in &self.tiles {
            if tile == matching_tile {
                return Some(tile.id.clone());
            }
        }

        None
    }

    pub fn find_tile_with_animation(&self, animation: &[Image]) -> Option<&Tile> {
        self.tiles.iter().find(|&tile| tile.animation_frames.as_slice() == animation)
    }

    /// adds a palette safely and returns the ID
    pub fn add_palette(&mut self, mut palette: Palette) -> String {
        let new_id = try_id(&self.palette_ids(), &palette.id);
        if new_id != palette.id {
            palette.id = new_id.clone();
        }
        self.palettes.push(palette);
        new_id
    }

    /// adds a tile safely and returns the ID
    pub fn add_tile(&mut self, mut tile: Tile) -> String {
        if tile.id == "0" || self.tile_ids().contains(&tile.id) {
            let new_id = self.new_tile_id();
            if new_id != tile.id {
                tile.id = new_id;
            }
        }

        let id = tile.id.clone();
        self.tiles.push(tile);
        id
    }

    /// adds a sprite safely and returns the ID
    pub fn add_sprite(&mut self, mut sprite: Sprite) -> String {
        let new_id = try_id(&self.sprite_ids(), &sprite.id);
        if new_id != sprite.id {
            sprite.id = new_id.clone();
        }
        self.sprites.push(sprite);
        new_id
    }

    /// adds an item safely and returns the ID
    pub fn add_item(&mut self, mut item: Item) -> String {
        let new_id = try_id(&self.item_ids(), &item.id);
        if new_id != item.id {
            item.id = new_id.clone();
        }
        self.items.push(item);
        new_id
    }

    /// adds a dialogue safely and returns the ID
    pub fn add_dialogue(&mut self, mut dialogue: Dialogue) -> String {
        let new_id = try_id(&self.dialogue_ids(), &dialogue.id);
        if new_id != dialogue.id {
            dialogue.id = new_id.clone();
        }
        self.dialogues.push(dialogue);
        new_id
    }

    /// adds an ending safely and returns the ID
    pub fn add_ending(&mut self, mut ending: Ending) -> String {
        let new_id = try_id(&self.ending_ids(), &ending.id);
        if new_id != ending.id {
            ending.id = new_id.clone();
        }
        self.endings.push(ending);
        new_id
    }

    /// Safely adds a room and returns the room ID (a new ID will be generated if clashing)
    /// You will need to be mindful that the room's palette, tile, exit and ending IDs
    /// will be valid after adding.
    pub fn add_room(&mut self, mut room: Room) -> String {
        let new_id = try_id(&self.room_ids(), &room.id);
        if new_id != room.id {
            room.id = new_id.clone();
        }
        self.rooms.push(room);
        new_id
    }

    pub fn add_variable(&mut self, mut variable: Variable) -> String {
        let new_id = try_id(&self.variable_ids(), &variable.id);
        if new_id != variable.id {
            variable.id = new_id.clone();
        }
        new_id
    }

    /// todo I think I need a generic `dedupe(&mut self, Vec<T>)` function
    /// it would have to take a closure for comparing a given T (see the background_tile below)
    /// and a closure for what to do with the changed IDs
    pub fn dedupe_tiles(&mut self) {
        let mut tiles_temp = self.tiles.clone();
        let mut unique_tiles: Vec<Tile> = Vec::new();
        let mut tile_id_changes: HashMap<String, String> = HashMap::new();

        while !tiles_temp.is_empty() {
            let tile = tiles_temp.pop().unwrap();

            if tile == crate::mock::tile_background() {
                tile_id_changes.insert(tile.id, "0".to_string());
            } else if tiles_temp.contains(&tile) {
                tile_id_changes.insert(
                    tile.id.clone(),
                    self.get_tile_id(&tile).unwrap()
                );
            } else {
                unique_tiles.push(tile);
            }
        }

        for room in &mut self.rooms {
            room.change_tile_ids(&tile_id_changes);
        }

        unique_tiles.reverse();

        self.tiles = unique_tiles;
    }

    fn version_line(&self) -> String {
        if self.version.is_some() {
            format!(
                "\n\n# BITSY VERSION {}.{}",
                self.version.as_ref().unwrap().major, self.version.as_ref().unwrap().minor
            )
        } else {
            "".to_string()
        }
    }

    fn room_format_line(&self) -> String {
        if self.room_format.is_some() {
            format!("\n\n! ROOM_FORMAT {}", self.room_format.unwrap().to_string())
        } else {
            "".to_string()
        }
    }

    fn font_line(&self) -> String {
        match self.font {
            Font::AsciiSmall => "".to_string(),
            Font::Custom     => format!("\n\nDEFAULT_FONT {}", self.custom_font.as_ref().unwrap()),
            _                => format!("\n\nDEFAULT_FONT {}", self.font.to_string().unwrap()),
        }
    }

    fn text_direction_line(&self) -> &str {
        match self.text_direction {
            TextDirection::RightToLeft => "\n\nTEXT_DIRECTION RTL",
            _ => "",
        }
    }

    /// older bitsy games do not specify a version, but we can infer 1.0
    pub fn version(&self) -> Version {
        self.version.unwrap_or(Version { major: 1, minor: 0 })
    }

    /// older bitsy games do not specify a room format, but we can infer 0
    pub fn room_format(&self) -> RoomFormat {
        self.room_format.unwrap_or(RoomFormat::Contiguous)
    }
}

#[cfg(test)]
mod test {
    use crate::{TextDirection, Font, Version, Game, Tile, Image};

    #[test]
    fn game_from_string() {
        let (output, _) = Game::from(include_str!["test-resources/default.bitsy"].to_string()).unwrap();
        let expected = crate::mock::game_default();

        assert_eq!(output, expected);
    }

    #[test]
    fn game_to_string() {
        let output = crate::mock::game_default().to_string();
        let expected = include_str!["test-resources/default.bitsy"].to_string();
        assert_eq!(output, expected);
    }

    #[test]
    fn tile_ids() {
        assert_eq!(crate::mock::game_default().tile_ids(), vec!["a".to_string()]);
    }

    #[test]
    fn new_tile_id() {
        // default tile has an id of 10 ("a"), and 0 is reserved
        assert_eq!(crate::mock::game_default().new_tile_id(), "1".to_string());

        // for a game with a gap in the tile IDs, check the gap is used

        let mut game = crate::mock::game_default();
        let mut tiles: Vec<Tile> = Vec::new();

        // 0 is reserved; upper bound is non-inclusive
        for n in 1..10 {
            if n != 4 {
                let mut new_tile = crate::mock::tile_default();
                new_tile.id = format!("{}", n).to_string();
                tiles.push(new_tile);
            }
        }

        game.tiles = tiles;

        assert_eq!(game.new_tile_id(), "4".to_string());

        // fill in the space created above, then test that tile IDs get sorted

        let mut new_tile = crate::mock::tile_default();
        new_tile.id = "4".to_string();
        game.tiles.push(new_tile);

        assert_eq!(game.new_tile_id(), "a".to_string());
    }

    #[test]
    fn add_tile() {
        let mut game = crate::mock::game_default();
        let new_id = game.add_tile(crate::mock::tile_default());
        assert_eq!(new_id, "1".to_string());
        assert_eq!(game.tiles.len(), 2);
        let new_id = game.add_tile(crate::mock::tile_default());
        assert_eq!(new_id, "2".to_string());
        assert_eq!(game.tiles.len(), 3);
    }

    #[test]
    fn arabic() {
        let (game, _) = Game::from(include_str!("test-resources/arabic.bitsy").to_string()).unwrap();

        assert_eq!(game.font, Font::Arabic);
        assert_eq!(game.text_direction, TextDirection::RightToLeft);
    }

    #[test]
    fn version_formatting() {
        let mut game = crate::mock::game_default();
        game.version = Some(Version { major: 5, minor: 0 });
        assert!(game.to_string().contains("# BITSY VERSION 5.0"))
    }

    #[test]
    fn get_tiles_for_room() {
        assert_eq!(
            crate::mock::game_default().get_tiles_for_room("0".to_string()).unwrap(),
            vec![&crate::mock::tile_default()]
        )
    }

    #[test]
    fn add_item() {
        let mut game = crate::mock::game_default();
        game.add_item(crate::mock::item());
        game.add_item(crate::mock::item());

        let expected = vec![
            "0".to_string(), "1".to_string(), "6".to_string(), "2".to_string()
        ];

        assert_eq!(game.item_ids(), expected);
    }

    #[test]
    fn merge() {
        // try merging two default games
        let mut game = crate::mock::game_default();
        game.merge(&crate::mock::game_default());

        assert_eq!(game.room_ids(), vec!["0".to_string(), "1".to_string()]);
        assert_eq!(game.tile_ids(), vec!["a".to_string(), "1".to_string()]); // 0 is reserved
        // duplicate avatar (SPR A) gets converted into a normal sprite
        assert_eq!(
            game.sprite_ids(),
            vec!["A".to_string(), "a".to_string(), "0".to_string(), "1".to_string()]
        );
        assert_eq!(
            game.item_ids(),
            vec!["0".to_string(), "1".to_string(), "2".to_string(), "3".to_string()]
        );
        assert_eq!(
            game.dialogue_ids(),
            vec![
                "0".to_string(),
                "1".to_string(),
                "2".to_string(),
                "3".to_string(),
                "4".to_string(),
                "5".to_string()
            ]
        );
        assert_eq!(game.palette_ids(), vec!["0".to_string(), "1".to_string()]);
        assert_eq!(
            game.get_room_by_id("1".to_string()).unwrap().palette_id,
            Some("1".to_string())
        );

        // test sprites in non-zero rooms in merged game
        let mut game_a = crate::mock::game_default();
        let mut game_b = crate::mock::game_default();
        let mut room = crate::mock::room();
        let mut sprite = crate::mock::sprite();
        let room_id = "2".to_string();
        room.id = room_id.clone();
        sprite.room_id = Some(room_id.clone());
        game_b.add_sprite(sprite);
        game_a.merge(&game_b);
        assert_eq!(game_a.get_sprite_by_id("2".to_string()).unwrap().room_id, Some(room_id));
    }

    #[test]
    fn dedupe_tiles() {
        let mut game = crate::mock::game_default();
        game.add_tile(crate::mock::tile_default());
        game.add_tile(crate::mock::tile_default());
        game.add_tile(crate::mock::tile_background());
        game.dedupe_tiles();
        assert_eq!(game.tiles, vec![crate::mock::tile_default()]);

        let tile_a = Tile {
            id: "0".to_string(),
            name: Some("apple".to_string()),
            wall: Some(true),
            animation_frames: vec![Image {
                pixels: vec![
                    0,1,1,0,1,1,0,1,
                    0,1,1,0,1,1,0,1,
                    1,0,1,0,1,0,0,1,
                    1,0,1,0,1,0,0,1,
                    0,0,0,0,1,1,1,1,
                    0,0,0,0,1,1,1,1,
                    1,1,0,1,1,0,1,1,
                    1,1,0,1,1,0,1,1,
                ]
            }],
            colour_id: Some(1)
        };

        let tile_b = Tile {
            id: "1".to_string(),
            name: Some("frogspawn".to_string()),
            wall: Some(false),
            animation_frames: vec![Image {
                pixels: vec![
                    1,0,1,0,1,0,0,1,
                    0,1,1,0,1,1,0,1,
                    0,1,1,0,1,1,0,1,
                    1,1,0,1,1,0,1,1,
                    1,0,1,0,1,0,0,1,
                    0,0,0,0,1,1,1,1,
                    0,0,0,0,1,1,1,1,
                    1,1,0,1,1,0,1,1,
                ]
            }],
            colour_id: None
        };

        game.add_tile(tile_a.clone());
        game.add_tile(tile_b.clone());
        game.add_tile(tile_a.clone());
        game.add_tile(tile_b.clone());

        game.dedupe_tiles();

        assert_eq!(game.tiles, vec![crate::mock::tile_default(), tile_a, tile_b]);
    }

    #[test]
    fn find_tile_with_animation() {
        let game = crate::mock::game_default();
        let animation = vec![Image { pixels: vec![
            1, 1, 1, 1, 1, 1, 1, 1,
            1, 0, 0, 0, 0, 0, 0, 1,
            1, 0, 0, 0, 0, 0, 0, 1,
            1, 0, 0, 1, 1, 0, 0, 1,
            1, 0, 0, 1, 1, 0, 0, 1,
            1, 0, 0, 0, 0, 0, 0, 1,
            1, 0, 0, 0, 0, 0, 0, 1,
            1, 1, 1, 1, 1, 1, 1, 1,
        ]}];
        let output = game.find_tile_with_animation(&animation);
        let expected = Some(&game.tiles[0]);
        assert_eq!(output, expected);
    }

    #[test]
    fn empty_game_data_throws_error() {
        assert_eq!(Game::from("".to_string()        ).unwrap_err(), crate::error::NotFound::Anything);
        assert_eq!(Game::from(" \n \r\n".to_string()).unwrap_err(), crate::error::NotFound::Anything);
    }
}