use std::{error::Error, convert::TryInto, cmp::max, num::ParseIntError};
use image::GenericImageView;
use linked_hash_set::LinkedHashSet;
use crate::reskit::{tileset::image_to_tiles, utility::{symbol_to_pascal, print_info}, cli::settings::TileOrder};
use super::{converter::{TiledTilemap, Layer, SystemPlane}, ecs::Component};

/**
 * Output the .bin and .pal file (using `tileset` tool to build it) containing each of the tiles
 * and palettes in the Tiled Editor tileset.
 */
pub fn get_tiles( tilemap: &TiledTilemap ) -> Result<(Vec<u8>, Vec<u8>), Box<dyn Error>> {
    let mut system_pals: [[u16; 16]; 4] = [
        [ 0; 16 ],
        [ 0; 16 ],
        [ 0; 16 ],
        [ 0; 16 ]
    ];
    let mut all_tiles: Vec<u8> = vec![0; 32];   // --system md, start with a blank buffer tile

    for tileset in &tilemap.tileset {
        let tiles_height = tileset.image.height() / 8;  // --system md
        let tiles_width = tileset.image.width() / 8;    // --system md

        // all this copy pasted code, it's so over
        if matches!( tileset.tile_order, TileOrder::Sprite ) {
            // Sprite iteration order
            for tile_x in 0..tiles_width {
                for tile_y in 0..tiles_height {
                    let tile_index = ( tile_y * tiles_width ) + tile_x;
                    let tile = tileset.image.clone().crop( tile_x * 8, tile_y * 8, 8, 8 );

                    // Fake palette (see below)
                    let mut fake: [u16; 16] = [ 0; 16 ];
                    let selected_pal = tileset.palettes[ tile_index as usize ];

                    let tile_bin = image_to_tiles(
                        &tile,
                        {
                            // Determine if palette is used here or it is a dummy palette
                            if let Some( selected_pal ) = selected_pal {
                                &mut system_pals[ selected_pal as usize ]
                            } else {
                                // Fake-a-palette
                                // You will get an error in get_tilemap if you try to use this palette-less tile
                                &mut fake
                            }
                        },
                        "tile"  // this is not set to "sprite" because we're only doing one tile at a time
                    );

                    if let Err( _ ) = tile_bin {
                        return Err( format!( "palette {:?} full (try moving tile {} to another palette)", selected_pal, tile_index ) )?
                    }

                    if let Ok( tile_bin ) = tile_bin {
                        all_tiles.extend( tile_bin );
                    }
                }
            }
        } else {
            // Tile iteration order
            for tile_y in 0..tiles_height {
                for tile_x in 0..tiles_width {
                    let tile_index = ( tile_y * tiles_width ) + tile_x;
                    let tile = tileset.image.clone().crop( tile_x * 8, tile_y * 8, 8, 8 );

                    // Fake palette (see below)
                    let mut fake: [u16; 16] = [ 0; 16 ];
                    let selected_pal = tileset.palettes[ tile_index as usize ];

                    let tile_bin = image_to_tiles(
                        &tile,
                        {
                            // Determine if palette is used here or it is a dummy palette
                            if let Some( selected_pal ) = selected_pal {
                                &mut system_pals[ selected_pal as usize ]
                            } else {
                                // Fake-a-palette
                                // You will get an error in get_tilemap if you try to use this palette-less tile
                                &mut fake
                            }
                        },
                        "tile"
                    );

                    if let Err( _ ) = tile_bin {
                        return Err( format!( "palette {:?} full (try moving tile {} to another palette)", selected_pal, tile_index ) )?
                    }

                    if let Ok( tile_bin ) = tile_bin {
                        all_tiles.extend( tile_bin );
                    }
                }
            }
        }
    }

    // Define result and write palettes
    let mut palettes: Vec<u8> = Vec::new();
    for pal in 0..system_pals.len() {
        for i in 0..system_pals[ pal ].len() {
            let bytes = system_pals[ pal ][ i ].to_be_bytes();
            for i in 0..2 {
                palettes.push( bytes[ i ] );
            }
        }
    }

    Ok( ( all_tiles, palettes ) )
}

/**
 * Output the .map file defining the hardware tilemap for the given Tiled Tilemap.
 * In `--system md`, this outputs tilemap B and then tilemap A
 */
pub fn get_tilemap( tilemap: &TiledTilemap ) -> Result<Vec<u8>, Box<dyn Error>> {
    let mut all_palettes: Vec<Option<u8>> = vec![];
    for tileset in &tilemap.tileset {
        all_palettes.extend( tileset.palettes.iter() );
    }

    let layer_b: Option<&Layer> = tilemap.layers.iter().find( | layer | matches!( layer, Layer::Tile { system_plane: SystemPlane::MdPlaneB, tiles: _ } ) );
    let layer_a: Option<&Layer> = tilemap.layers.iter().find( | layer | matches!( layer, Layer::Tile { system_plane: SystemPlane::MdPlaneA, tiles: _ } ) );

    // Each entry in a `--system md` tilemap is 16 bits
    let mut total_nametable: Vec<u16> = Vec::new();

    let mut nametable: Vec<u16> = vec![ 0; tilemap.width * tilemap.height ];
    if let Some( layer_b ) = layer_b {
        let layer_b = match layer_b {
            Layer::Tile { system_plane: _, tiles } => tiles,
            _ => return Err( "internal error: invalid object type" )?
        };

        for y in 0..tilemap.height {
            for x in 0..tilemap.width {
                let target_tile: u32 = *layer_b.get( ( y * tilemap.width ) + x ).ok_or( "internal error: invalid data in tilemap" )?;
                let target_tile: u16 = target_tile.try_into()?;
                if target_tile > 0 {
                    let source_tile = target_tile - 1;

                    // From the starting point of x, y "stamp" the target tile's indices
                    let nametable_entry = target_tile;
                    let selected_pal = all_palettes[ source_tile as usize ];
                    if let Some( selected_pal ) = selected_pal {
                        nametable[ ( y * tilemap.width ) + x ] = ( ( selected_pal as u16 ) << 13 ) | nametable_entry;
                    } else {
                        return Err( format!( "invalid setting: tile {} in tileset has no defined palette", source_tile ) )?
                    }
                }
            }
        }
    }
    total_nametable.extend( nametable.iter() );

    // Just do the same for layer a
    // Copy pasted because i'm lazy and tired
    let mut nametable: Vec<u16> = vec![ 0; tilemap.width * tilemap.height ];
    if let Some( layer_a ) = layer_a {
        let layer_a = match layer_a {
            Layer::Tile { system_plane: _, tiles } => tiles,
            _ => return Err( "internal error: invalid object type" )?
        };

        for y in 0..tilemap.height {
            for x in 0..tilemap.width {
                let target_tile: u32 = *layer_a.get( ( y * tilemap.width ) + x ).ok_or( "internal error: invalid data in tilemap" )?;
                let target_tile: u16 = target_tile.try_into()?;
                if target_tile > 0 {
                    let source_tile = target_tile - 1;

                    // From the starting point of x, y "stamp" the target tile's indices
                    let nametable_entry = target_tile;
                    let selected_pal = all_palettes[ source_tile as usize ];
                    if let Some( selected_pal ) = selected_pal {
                        nametable[ ( y * tilemap.width ) + x ] = ( ( selected_pal as u16 ) << 13 ) | nametable_entry;
                    } else {
                        return Err( format!( "invalid setting: tile {} in tileset has no defined palette", source_tile ) )?
                    }
                }
            }
        }
    }
    total_nametable.extend( nametable.iter() );

    // Convert the u16's to a series of u8 data
    let mut result: Vec<u8> = Vec::new();
    for i in 0..total_nametable.len() {
		let bytes = total_nametable[ i ].to_be_bytes();
		for i in 0..2 {
			result.push( bytes[ i ] );
		}
    }
    Ok( result )
}

/**
 * Get the .lvc collision map (u8 sized for the map dimensions, collision areas are either 0 or 1)
 */
pub fn get_collision_map( tilemap: &TiledTilemap ) -> Result<Vec<u8>, Box<dyn Error>> {
    let mut result: Vec<u8> = Vec::new();

    let collision: Option<&Layer> = tilemap.layers.iter().find( | layer | matches!( layer, Layer::Collision { tiles: _ } ) );
    if let Some( collision ) = collision {
        let collision = match collision {
            Layer::Collision { tiles } => tiles,
            _ => return Err( "internal error: invalid object type" )?
        };

        for collision_data in collision {
            let collision_data: u8 = if *collision_data > 0 { 1 } else { 0 };
            result.push( collision_data );
        }
    }

    Ok( result )
}

/**
 * Get the entity-component system defined. If none is defined return an empty vec.
 */
pub fn get_ecs( tilemap: &TiledTilemap ) -> Result<Vec<u8>, Box<dyn Error>> {
    if tilemap.ecs.is_empty() {
        return Ok( vec![] )
    }

    let mut result: Vec<u8> = Vec::new();

    // Build a complete set of component IDs and types
    let mut component_ids: LinkedHashSet<String> = LinkedHashSet::new();
    let mut types: LinkedHashSet<LinkedHashSet<String>> = LinkedHashSet::new();
    for entity in &tilemap.ecs {
        // Get the list of components attached to this entity
        // Sort alphabetically as order matters in LinkedHashSets
        let mut components: Vec<String> = entity.components.keys().map( | id | id.to_lowercase() ).collect();
        components.sort();
        let components: LinkedHashSet<String> = components.into_iter().collect();

        // Assign an index to each unique component id by inserting it into `component_ids`
        component_ids.extend( components.clone() );

        // Assign this unique combination of components a type id
        // by inserting it into `types`
        types.insert_if_absent( components );
    }

    let largest_type_size: u16 = types.iter()
                                        .map( | components | components.len() as u16 )
                                        .reduce( | prev, current | max( prev, current ) )
                                        .ok_or( "internal error: type is empty" )?;

    // Type Table
    // 2 bytes: Number of Types
    result.extend( ( types.len() as u16 ).to_be_bytes() );

    // 2 bytes: Type Definition Union Size
    result.extend( largest_type_size.to_be_bytes() );

    // For (Number of Types):
    for ecs_type in &types {
        // (Type Definition Union Size) bytes: List of Component IDs
        let mut type_component_ids: Vec<u8> = vec![];

        for component in ecs_type {
            type_component_ids.push(
                component_ids.iter().position( | value | component == value ).ok_or( "internal error: no component" )? as u8
            );
        }

        // Fill the remainder of the union with 0xFF, if needed
        let remainder = largest_type_size as usize - type_component_ids.len();
        for _ in 0..remainder {
            type_component_ids.push( 0xFF );
        }

        result.extend( type_component_ids );
    }

    // Object Table
    // 2 bytes: Object Table Size
    result.extend( ( 512 as u16 ).to_be_bytes() );

    // For (Object Table Size)
    for entity in &tilemap.ecs {
        // Do the thing again
        let mut components: Vec<String> = entity.components.keys().map( | id | id.to_lowercase() ).collect();
        components.sort();
        let components: LinkedHashSet<String> = components.into_iter().collect();

        // What type id is this?
        let type_id: u8 = types.iter().position( | ecs_type | ecs_type == &components ).ok_or( "internal error: no type id" )? as u8;

        // Output type id
        result.push( type_id );
    }

    // Fill the remainder of the sparse Object Array with 0xFF, if needed
    let remainder = 512 - tilemap.ecs.len();
    for _ in 0..remainder {
        result.push( 0xFF );
    }

    // Component Attribute Table
    // Index all attribute values that do not parse to u16
    let mut attribute_value_ids: LinkedHashSet<String> = LinkedHashSet::new();
    for entity in &tilemap.ecs {
        for ( _, component ) in &entity.components {
            for ( _, attribute_value ) in &component.attributes {
                // Handling various types from Tiled Editor
                // * If it is "true" or "false", leave it alone. It will be articulated as 1 or 0.
                // * If it can be parsed as a u16, leave it alone.
                // * In all other cases, add to attribute_value_ids to create an index.
                if attribute_value != "true" && attribute_value != "false" {
                    let try_as_u16: Result<u16, ParseIntError> = attribute_value.parse();
                    if let Err( _ ) = try_as_u16 {
                        attribute_value_ids.insert_if_absent( attribute_value.clone() );
                        print_info( &format!( "Attribute Value ID {}: \"{}\"", attribute_value_ids.len() - 1, attribute_value ) );
                    }
                }
            }
        }
    }

    // Find the largest size of a combination of attributes (the component attribute union size)
    let largest_type_attributes_size = tilemap.ecs.iter()
                                                  .map( | entity | {
                                                    let mut total_attributes_for_entity = 0;

                                                    for ( _, component ) in &entity.components {
                                                        total_attributes_for_entity += component.attributes.len()
                                                    }

                                                    total_attributes_for_entity
                                                } )
                                                .reduce( | prev, current | max( prev, current ) )
                                                .ok_or( "internal error: no largest type attributes size" )? as u16;

    // 2 bytes: Component Attribute Union Size
    result.extend( largest_type_attributes_size.to_be_bytes() );

    // For (Object Table Size):
    for entity in &tilemap.ecs {
        // (Component Attribute Union Size) bytes: Component attribute settings
        // Sort components in this entity alphabetically, ascending
        let mut components: Vec<(&String, &Component)> = entity.components.iter().collect();
        components.sort_by( | a, b | a.0.partial_cmp( b.0 ).expect( "internal error: unsortable" ) );

        let mut written_attributes = 0;
        for ( _, component ) in &components {
            // Sort attributes in each component alphabetically
            let mut attributes: Vec<(&String, &String)> = component.attributes.iter().collect();
            attributes.sort_by( | a, b | a.0.partial_cmp( b.0 ).expect( "internal error: unsortable" ) );

            for ( _, attribute_value ) in attributes {
                written_attributes += 1;
                match attribute_value.as_str() {
                    "true" => result.extend( ( 1 as u16 ).to_be_bytes() ),
                    "false" => result.extend( ( 0 as u16 ).to_be_bytes() ),
                    string => {
                        let try_as_u16: Result<u16, ParseIntError> = string.parse();
                        if let Ok( numeric ) = try_as_u16 {
                            result.extend( numeric.to_be_bytes() );
                        } else {
                            let id: u16 = attribute_value_ids.iter()
                                                            .position( | attribute_value | attribute_value == &string )
                                                            .ok_or( "internal error: no type id" )? as u16;

                            result.extend( id.to_be_bytes() );
                        }
                    }
                }
            }
        }

        // Fill the remainder of the union with 0xFF, if needed
        let remainder = largest_type_attributes_size - written_attributes;
        for _ in 0..remainder {
            result.extend( ( 0xFFFF as u16 ).to_be_bytes() );
        }
    }

    // Fill the remainder of the sparse array with 0xFF, if needed
    let remainder = 512 - tilemap.ecs.len();
    for _ in 0..remainder {
        for _ in 0..largest_type_attributes_size {
            result.extend( ( 0xFFFF as u16 ).to_be_bytes() );
        }
    }

    // Output IDs to terminal
    let component_ids: Vec<String> = component_ids.into_iter().collect();
    for i in 0..component_ids.len() {
        print_info( &format!( "Component ID {}: {}", i, component_ids[ i ] ) );
    }

    // Output type structures to terminal
    let mut visited_types: LinkedHashSet<LinkedHashSet<String>> = LinkedHashSet::new();
    for entity in &tilemap.ecs {
        // Do the thing again
        let mut components: Vec<String> = entity.components.keys().map( | id | id.to_lowercase() ).collect();
        components.sort();
        let components: LinkedHashSet<String> = components.into_iter().collect();

        if visited_types.insert( components.clone() ) {
            // What type id is this?
            let type_id: u8 = types.iter().position( | ecs_type | ecs_type == &components ).ok_or( "internal error: no type id" )? as u8;
            print_info( &format!( "Type ID {} Components and Attributes:", type_id ) );

            let mut components: Vec<(&String, &Component)> = entity.components.iter().collect();
            components.sort_by( | a, b | a.0.partial_cmp( b.0 ).expect( "internal error: unsortable" ) );

            for ( component_name, component ) in components {
                print_info( &format!( "\tComponent {}", component_name ) );
                let mut attributes: Vec<(&String, &String)> = component.attributes.iter().collect();
                attributes.sort_by( | a, b | a.0.partial_cmp( b.0 ).expect( "internal error: unsortable" ) );

                let mut index = 0;
                for ( attribute_name, _ ) in &attributes {
                    print_info( &format!( "\t\tAttribute ID {}: {}", index, attribute_name ) );
                    index += 1;
                }

                if attributes.is_empty() {
                    print_info( "\t\tNo attributes" );
                }
            }
        }
    }

    Ok( result )
}

/**
 * Get a helper .asm or .c file that ties all the level components together
 */
pub fn get_code( tilemap: &TiledTilemap, level_name: &str, path_prefix: &str ) -> Result<String, Box<dyn Error>> {
    let version = env!( "CARGO_PKG_VERSION" );
    let level_label = symbol_to_pascal( level_name );
    let ( width, height ) = ( tilemap.width, tilemap.height );
    let num_tiles = {
        let mut total_tiles = 0;

        for tileset in &tilemap.tileset {
            total_tiles += ( tileset.image.width() / 8 ) * ( tileset.image.height() / 8 );     // --system md
        }

        total_tiles
    };
    let file = format!( r#"; Level definition file
; Generated by reskit v{version}

{level_label}Tiles:
    incbin '{path_prefix}{level_name}/tiles.bin'

{level_label}Palettes:
    incbin '{path_prefix}{level_name}/palettes.pal'

{level_label}Nametables:
    incbin '{path_prefix}{level_name}/nametables.map'

{level_label}Collision:
    incbin '{path_prefix}{level_name}/collision.lvc'

{level_label}Objects:
    incbin '{path_prefix}{level_name}/objects.ecs'

{level_label}:
    dc.w    {width}, {height}, {num_tiles}
    dc.l    {level_label}Tiles
    dc.l    {level_label}Palettes
    dc.l    {level_label}Nametables
    dc.l    {level_label}Collision
    dc.l    {level_label}Objects"# );

    Ok( file )
}