use std::{error::Error, fs::read_to_string, path::Path, borrow::Cow};
use image::{DynamicImage, GenericImageView};
use roxmltree::Node;
use crate::reskit::{utility::print_warning, cli::settings::TileOrder};
use super::ecs;

#[derive(Debug)]
pub struct TiledTilemap {
    pub tileset: Vec<TiledTileset>,
    pub layers: Vec<Layer>,
    pub ecs: Vec<ecs::Entity>,
    pub width: usize,
    pub height: usize
}

#[derive(Debug)]
pub struct TiledTileset {
    pub first_gid: usize,
    pub image: DynamicImage,
    pub palettes: Vec<Option<u8>>,
    pub tile_order: TileOrder
}

#[derive(Debug)]
pub enum SystemPlane {
    MdPlaneA,
    MdPlaneB
}

#[derive(Debug)]
pub enum Layer {
    Tile {
        system_plane: SystemPlane,
        tiles: Vec<u32>
    },
    Collision {
        tiles: Vec<u32>
    }
}

fn get_layer( layer: Node, map_width: usize, map_height: usize ) -> Result<Option<Layer>, Box<dyn Error>> {
    let layer_id = layer.attribute( "id" ).ok_or( "invalid file: on layer: no id" )?;
    let layer_name = layer.attribute( "name" ).unwrap_or( "<no name>" );
    let layer_name = format!( "({}, ID: {})", layer_name, layer_id );

    // Validate layer is same as total map size
    let ( layer_width, layer_height ): ( usize, usize ) = (
        layer.attribute( "width" ).ok_or( format!( "invalid file: on layer {}: no width attribute", layer_name ) )?.parse()?,
        layer.attribute( "height" ).ok_or( format!( "invalid file: on layer {}: no height attribute", layer_name ) )?.parse()?
    );

    if layer_width != map_width || layer_height != map_height {
        return Err( format!( "invalid file: on layer {}: layer width must match map width", layer_name ) )?
    }

    let properties = layer.descendants().find( | node | node.tag_name() == "properties".into() );
    if let Some( properties ) = properties {
        let properties: Vec<Node> = properties.descendants().filter( | node | {
            if let Some( name ) = node.attribute( "name" ) {
                name == "reskit-layer"
            } else {
                false
            }
        } ).collect();

        // Should be either one or none
        if let Some( layer_property ) = properties.first() {
            let layer_type = layer_property.attribute( "value" ).ok_or( format!( "invalid file: on layer {}: no value for property", layer_name ) )?;

            let data = layer.descendants()
                            .find( | node | node.tag_name() == "data".into() )
                            .ok_or( format!( "invalid file: on layer {}: no data for layer", layer_name ) )?;

            let encoding = data.attribute( "encoding" ).ok_or( format!( "invalid file: on layer {}: no encoding attribute", layer_name ) )?;
            if encoding != "csv" {
                return Err( format!( "invalid file: on layer {}: only csv is supported for layer encoding", layer_name ) )?
            }

            let data: Vec<&str> = data.text().ok_or( format!( "invalid file: on layer {}: no layer data", layer_name ) )?.split( "," ).collect();
            let tiles: Vec<u32> = data.into_iter().map( | string | Ok( string.trim().parse()? ) ).collect::< Result< Vec<u32>, Box<dyn Error> > >()?;

            match layer_type.to_lowercase().as_str() {
                "a" => Ok( Some( Layer::Tile {
                    system_plane: SystemPlane::MdPlaneA,
                    tiles
                } ) ),
                "b" => Ok( Some( Layer::Tile {
                    system_plane: SystemPlane::MdPlaneB,
                    tiles
                } ) ),
                "collision" => Ok( Some( Layer::Collision { tiles } ) ),
                _ => {
                    print_warning( &format!( "on layer {}: invalid reskit-layer value {}; ignoring this layer", layer_name, layer_type ) );
                    Ok( None )
                }
            }
        } else {
            print_warning( &format!( "on layer {}: no reskit-layer property defining hardware layer or collision; ignoring this layer", layer_name ) );
            Ok( None )
        }
    } else {
        print_warning( &format!( "on layer {}: no properties defining hardware layer or collision; ignoring this layer", layer_name ) );
        Ok( None )
    }
}

fn get_tiles( tileset: Node, first_gid: usize, working_directory: &str ) -> Result<TiledTileset, Box<dyn Error>> {
    // Get the image for the tileset
    let image = tileset.descendants().find( | node | node.tag_name() == "image".into() ).ok_or( "invalid file: no image object" )?;
    let image_path = format!( "{}/{}", working_directory, image.attribute( "source" ).ok_or( "invalid file: no source attribute on image" )? );
    let image = image::open( image_path )?;

    // Image must be a multiple of 8 (--system md)
    if image.width() % 8 != 0 { return Err( "invalid file: tileset width not multiple of 8" )? }
    if image.height() % 8 != 0 { return Err( "invalid file: tileset height not multiple of 8" )? }

    let tile_count: usize = tileset.attribute( "tilecount" ).ok_or( "invalid file: no tilecount attribute on tileset" )?.parse()?;
    let mut palettes: Vec<Option<u8>> = vec![ None; tile_count ];

    let defined_tiles = tileset.descendants().filter( | node | node.tag_name() == "tile".into() );
    for defined_tile in defined_tiles {
        let tile_id: usize = defined_tile.attribute( "id" ).ok_or( "invalid file: id attribute not defined on a tile" )?.parse()?;
        let properties = defined_tile.descendants().find( | node | node.tag_name() == "properties".into() ).ok_or( "invalid file: no properties descendant in tileset" )?;
        let property = properties.descendants().find( | node | node.tag_name() == "property".into() && node.attribute( "name" ).unwrap_or( "" ) == "reskit-palette" );
        if let Some( property ) = property {
            let property_type = property.attribute( "type" ).unwrap_or( "string" );
            if property_type == "int" {
                let palette_value: u8 = property.attribute( "value" ).ok_or( "invalid file: reskit-palette property has no value" )?.parse()?;
                // --system md
                if palette_value > 3 {
                    print_warning( &format!( "reskit-palette property on tile {} is not valid palette (0 to 3) - leaving palette unset, this is probably not what you want...", tile_id ) );
                } else {
                    palettes[ tile_id ] = Some( palette_value );
                }
            } else {
                print_warning( &format!( "reskit-palette property on tile {} is not int type - leaving palette unset, this is probably not what you want...", tile_id ) )
            }
        }
    }

    // Retrieve the tile order as specified by reskit-tile-order (if none is specified, fallback on Tile)
    let tile_order = if let Some( properties ) = tileset.descendants().find( | node | node.tag_name() == "properties".into() ) {
        if let Some( tile_order_property ) = properties.descendants().find( | node | node.attribute( "name" ) == Some( "reskit-tile-order" ) ) {
            let tile_order_property = tile_order_property.attribute( "value" ).expect( "internal error: no reskit-tile-order value" );
            match tile_order_property.to_lowercase().as_str() {
                "sprite" => TileOrder::Sprite,
                "tile" => TileOrder::Tile,
                invalid => {
                    print_warning( &format!( "invalid setting for property reskit-tile-order: {}. falling back on \"sprite\"", invalid ) );
                    TileOrder::Tile
                }
            }
        } else {
            TileOrder::Tile
        }
    } else {
        TileOrder::Tile
    };

    Ok( TiledTileset { first_gid, image, palettes, tile_order } )
}

pub fn get_tiled_tilemap( path: &str ) -> Result<TiledTilemap, Box<dyn Error>> {
    let file = read_to_string( path )?;
    let document = roxmltree::Document::parse( &file )?;
    let working_directory = {
        let result = Path::new( path ).parent().unwrap_or( Path::new( "." ) ).to_string_lossy();

        if result == "" {
            Cow::from( "." )
        } else {
            result
        }
    };

    let map = document.descendants().find( | node | node.tag_name() == "map".into() );
    if let Some( map ) = map {
        // Validate version
        let version = map.attribute( "version" ).ok_or( "invalid file: no version attribute" )?;
        if version < "1.10" || !version.starts_with( "1." ) {
            return Err( "invalid file: unsupported version" )?
        }

        // Validate orientation and render order
        let orientation = map.attribute( "orientation" ).ok_or( "invalid file: no orientation attribute" )?;
        if orientation != "orthogonal" {
            return Err( "invalid file: only orthogonal orientation is supported" )?
        }
        let render_order = map.attribute( "renderorder" ).ok_or( "invalid file: no renderorder attribute" )?;
        if render_order != "left-down" {
            return Err( "invalid file: only left-down orientation is supported" )?
        }

        let ( width, height ): ( usize, usize ) = (
            map.attribute( "width" ).ok_or( "invalid file: no width attribute" )?.parse()?,
            map.attribute( "height" ).ok_or( "invalid file: no height attribute" )?.parse()?
        );

        let ( tile_width, tile_height ): ( usize, usize ) = (
            map.attribute( "tilewidth" ).ok_or( "invalid file: no tilewidth attribute" )?.parse()?,
            map.attribute( "tileheight" ).ok_or( "invalid file: no tileheight attribute" )?.parse()?
        );

        // --system md is 8x8
        if tile_width != 8 {
            return Err( "invalid file: tile width is not 8 for --system md" )?
        }
        if tile_height != 8 {
            return Err( "invalid file: tile height is not 8 for --system md" )?
        }

        // Build tilesets (current version assumes one tileset per level)
        let mut tilesets: Vec<TiledTileset> = vec![];

        for tileset in map.descendants().filter( | node | node.tag_name() == "tileset".into() ) {
            let tileset_source_path = tileset.attribute( "source" ).ok_or( "invalid file: no tileset source" )?;
            let tileset_file = read_to_string( format!( "{}/{}", working_directory, tileset_source_path ) )?;
            let tileset_document = roxmltree::Document::parse( &tileset_file )?;
            let first_gid = tileset.attribute( "firstgid" ).ok_or( "invalid file: no firstgid attribute" )?.parse()?;
            let tileset = tileset_document.descendants().find( | node | node.tag_name() == "tileset".into() ).ok_or( "invalid file: no tileset origin object" )?;

            // Validate referenced tileset dimensions match map dimensions
            // E.g. if your .tmx specifies 16x16, the acompanying .tsx file should agree
            let ( tsx_tile_width, tsx_tile_height ): ( usize, usize ) = (
                tileset.attribute( "tilewidth" ).ok_or( "invalid file: no tilewidth attribute" )?.parse()?,
                tileset.attribute( "tileheight" ).ok_or( "invalid file: no tileheight attribute" )?.parse()?
            );
            if tsx_tile_width != tile_width || tsx_tile_height != tile_height {
                return Err( "invalid file: referenced tilemap doesn't have same per-tile width and height of parent tilemap" )?
            }

            tilesets.push( get_tiles( tileset, first_gid, &working_directory )? );
        }

        let tileset = tilesets;
        if tileset.is_empty() {
            return Err( "invalid file: at least one tileset must be present in file" )?
        }

        // Get the layers
        let layers: Vec<Layer> = map.descendants()
                                         .filter( | node | node.tag_name() == "layer".into() )
                                         .map( | node | get_layer( node, width, height ) )
                                         .collect::< Result< Vec<Option<Layer>>, Box<dyn Error> > >()?
                                         .into_iter()
                                         .filter_map( | option | option )
                                         .collect();

        // Print warning if there is no collision layer
        if let None = layers.iter().find( | layer | matches!( layer, Layer::Collision { tiles: _ } ) ) {
            print_warning( "tile map has no \"collision\" layer: this probably is not what you want" );
        }

        // Get the entity-component system
        let object_group = map.descendants().find( | node | node.tag_name() == "objectgroup".into() );
        let ecs = if let Some( object_group ) = object_group {
            ecs::get_ecs( object_group )?
        } else {
            Vec::new()
        };

        Ok(
            TiledTilemap {
                tileset,
                layers,
                ecs,
                width,
                height
            }
        )
    } else {
        Err( "invalid file: this does not appear to be valid Tiled .tmx file" )?
    }
}