use quote::{quote, TokenStreamExt};
use std::{
    env,
    fs::File,
    io::{BufWriter, Write},
    str::FromStr,
};
use tiled::{Map, ObjectLayer, TileLayer};

use proc_macro2::TokenStream;

const LEVEL_NAMES: &[&str] = &[
    "level1",
    "level2",
    "level3",
    "level4",
    "level5",
    "level6",
    "level_switch",
    "level_spikes",
    "level_spikes2",
    "level_squid_force_button",
    "level_squid_intro",
    "level_squid2",
    "level_squid1",
    "level_squid_item",
    "level_squid_button",
    "level_squid_drop",
    "level_spikes3",
    "level_around",
    "level_squidprogramming",
    "a_familiar_sight",
    "block_push_1",
    "just_rocks",
    "squid_rock",
    "ice_ice",
    "block_push_2",
    "glove_key",
    "block_push_3",
    "teleporter_1",
    "squid_teleport",
    "teleporter_2",
    "slime_teleporter",
    "another_ice",
    "another_ice_2",
    "another_ice_3",
    "another_ice_4",
    "hole_introduction",
    "rotator_1",
];

fn main() {
    let out_dir = env::var("OUT_DIR").expect("OUT_DIR environment variable must be specified");

    let mut tile_loader = tiled::Loader::new();

    let ui_map = load_tmx(&mut tile_loader, "maps/UI.tmx");
    let ui_tiles = export_ui_tiles(&ui_map, quote!(ui));

    const DPL_LEVELS_ENVIRONMENT_VARIABLE: &str = "DPL_LEVELS";

    println!(
        "cargo:rerun-if-env-changed={}",
        DPL_LEVELS_ENVIRONMENT_VARIABLE
    );

    let levels: Vec<String> = env::var(DPL_LEVELS_ENVIRONMENT_VARIABLE)
        .map(|x| x.split(',').map(|x| x.trim().to_string()).collect())
        .unwrap_or(LEVEL_NAMES.iter().map(|x| x.to_string()).collect());

    let levels = levels
        .iter()
        .map(|level| load_level(&mut tile_loader, &format!("maps/levels/{level}.tmx")))
        .collect::<Vec<_>>();
    let levels_tiles = levels.iter().map(|level| &level.0);
    let levels_data = levels.iter().map(|level| &level.1);

    let tilemaps_output = quote! {
        use agb::display::tiled::TileSetting;

        pub const UI_BACKGROUND_MAP: &[TileSetting] = #ui_tiles;
        pub const LEVELS_MAP: &[&[TileSetting]] = &[#(#levels_tiles),*];
    };

    let levels_output = quote! {
        pub const LEVELS: &[Level] = &[#(#levels_data),*];
    };

    {
        let tilemaps_output_file = File::create(format!("{out_dir}/tilemaps.rs"))
            .expect("Failed to open tilemaps.rs for writing");
        let mut tilemaps_writer = BufWriter::new(tilemaps_output_file);
        write!(&mut tilemaps_writer, "{tilemaps_output}").unwrap();
    }

    {
        let levels_output_file = File::create(format!("{out_dir}/levels.rs"))
            .expect("Failed to open levels.rs for writing");
        let mut levels_output_writer = BufWriter::new(levels_output_file);

        write!(&mut levels_output_writer, "{levels_output}").unwrap();
    }
}

fn load_level(loader: &mut tiled::Loader, filename: &str) -> (TokenStream, Level) {
    let level_map = load_tmx(loader, filename);
    let tiles = export_tiles(&level_map, quote!(level));
    let data = export_level(&level_map);

    (tiles, data)
}

fn load_tmx(loader: &mut tiled::Loader, filename: &str) -> tiled::Map {
    println!("cargo:rerun-if-changed={filename}");
    loader.load_tmx_map(filename).expect("failed to load map")
}

enum Entity {
    Sword,
    Slime,
    Hero,
    Stairs,
    Door,
    Key,
    Switch,
    SwitchPressed,
    SwitchedOpenDoor,
    SwitchedClosedDoor,
    SpikesUp,
    SpikesDown,
    SquidUp,
    SquidDown,
    Ice,
    MovableBlock,
    Glove,
    Teleporter,
    Hole,
    RotatorUp,
    RotatorDown,
    RotatorLeft,
    RotatorRight,
}

impl FromStr for Entity {
    type Err = ();

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        use Entity::*;

        Ok(match s {
            "SWORD" => Sword,
            "SLIME" => Slime,
            "HERO" => Hero,
            "STAIRS" => Stairs,
            "DOOR" => Door,
            "KEY" => Key,
            "SWITCH" => Switch,
            "SWITCH_PRESSED" => SwitchPressed,
            "DOOR_SWITCHED" => SwitchedClosedDoor,
            "DOOR_SWITCHED_OPEN" => SwitchedOpenDoor,
            "SPIKES" => SpikesUp,
            "SPIKES_DOWN" => SpikesDown,
            "SQUID_UP" => SquidUp,
            "SQUID_DOWN" => SquidDown,
            "ICE" => Ice,
            "BLOCK" => MovableBlock,
            "GLOVE" => Glove,
            "TELEPORTER" => Teleporter,
            "HOLE" => Hole,
            "ROTATOR_LEFT" => RotatorLeft,
            "ROTATOR_RIGHT" => RotatorRight,
            "ROTATOR_UP" => RotatorUp,
            "ROTATOR_DOWN" => RotatorDown,
            _ => return Err(()),
        })
    }
}

impl quote::ToTokens for Entity {
    fn to_tokens(&self, tokens: &mut TokenStream) {
        use Entity::*;

        tokens.append_all(match self {
            Sword => quote!(Item::Sword),
            Slime => quote!(Item::Slime),
            Hero => quote!(Item::Hero),
            Stairs => quote!(Item::Stairs),
            Door => quote!(Item::Door),
            Key => quote!(Item::Key),
            Switch => quote!(Item::Switch),
            SwitchPressed => quote!(Item::SwitchPressed),
            SwitchedOpenDoor => quote!(Item::SwitchedOpenDoor),
            SwitchedClosedDoor => quote!(Item::SwitchedClosedDoor),
            SpikesUp => quote!(Item::SpikesUp),
            SpikesDown => quote!(Item::SpikesDown),
            SquidUp => quote!(Item::SquidUp),
            SquidDown => quote!(Item::SquidDown),
            Ice => quote!(Item::Ice),
            MovableBlock => quote!(Item::MovableBlock),
            Glove => quote!(Item::Glove),
            Teleporter => quote!(Item::Teleporter),
            Hole => quote!(Item::Hole),
            RotatorUp => quote!(Item::RotatorUp),
            RotatorDown => quote!(Item::RotatorDown),
            RotatorLeft => quote!(Item::RotatorLeft),
            RotatorRight => quote!(Item::RotatorRight),
        })
    }
}

enum Direction {
    Up,
    Down,
    Left,
    Right,
}

impl TryFrom<char> for Direction {
    type Error = ();

    fn try_from(c: char) -> Result<Self, Self::Error> {
        use Direction::*;

        Ok(match c {
            'U' => Up,
            'D' => Down,
            'L' => Left,
            'R' => Right,
            _ => return Err(()),
        })
    }
}

impl quote::ToTokens for Direction {
    fn to_tokens(&self, tokens: &mut TokenStream) {
        use Direction::*;

        tokens.append_all(match self {
            Up => quote!(Direction::Up),
            Down => quote!(Direction::Down),
            Left => quote!(Direction::Left),
            Right => quote!(Direction::Right),
        });
    }
}

struct EntityWithPosition(Entity, (i32, i32));

impl quote::ToTokens for EntityWithPosition {
    fn to_tokens(&self, tokens: &mut TokenStream) {
        let pos_x = self.1 .0;
        let pos_y = self.1 .1;
        let location = quote!(Vector2D::new(#pos_x, #pos_y));
        let item = &self.0;

        tokens.append_all(quote!(Entity(#item, #location)))
    }
}

struct Level {
    starting_items: Vec<Entity>,
    fixed_positions: Vec<EntityWithPosition>,
    solution_positions: Vec<EntityWithPosition>,
    directions: Vec<Direction>,
    wall_bitmap: Vec<u8>,
    name: String,
}

impl quote::ToTokens for Level {
    fn to_tokens(&self, tokens: &mut TokenStream) {
        let wall_bitmap = &self.wall_bitmap;
        let fixed_positions = &self.fixed_positions;
        let solution_positions = &self.solution_positions;
        let directions = &self.directions;
        let starting_items = &self.starting_items;
        let name = &self.name;

        tokens.append_all(quote! {
            Level::new(
                Map::new(11, 10, &[#(#wall_bitmap),*]),
                &[#(#fixed_positions),*],
                &[#(#solution_positions),*],
                &[#(#directions),*],
                &[#(#starting_items),*],
                #name,
            )
        })
    }
}

fn extract_objects_from_layer(
    objects: ObjectLayer<'_>,
) -> impl Iterator<Item = EntityWithPosition> + '_ {
    objects.objects().map(|obj| {
        let entity: Entity = obj
            .name
            .parse()
            .unwrap_or_else(|_| panic!("unknown object type {}", obj.name));

        let x = (obj.x / 16.0) as i32;
        let y = (obj.y / 16.0) as i32;

        EntityWithPosition(entity, (x, y))
    })
}

fn export_level(map: &tiled::Map) -> Level {
    let objects = map
        .get_object_layer("Puzzle")
        .expect("The puzzle object layer should exist");

    let fixed_positions = extract_objects_from_layer(objects);

    let solution_positions = extract_objects_from_layer(
        map.get_object_layer("Solution")
            .expect("Should have an object layer called 'Solution'"),
    );

    let Some(tiled::PropertyValue::StringValue(starting_items)) = map.properties.get("ITEMS")
    else {
        panic!("Starting items must be a string")
    };

    let Some(tiled::PropertyValue::StringValue(level_name)) = map.properties.get("NAME") else {
        panic!("Level name must be a string")
    };

    let starting_items = starting_items.split(',').map(|starting_item| {
        starting_item
            .parse()
            .unwrap_or_else(|_| panic!("unknown object type {}", starting_item))
    });

    let Some(tiled::PropertyValue::StringValue(directions)) = map.properties.get("DIRECTIONS")
    else {
        panic!("Starting items must be a string")
    };

    let directions = directions.chars().map(|starting_item| {
        starting_item
            .try_into()
            .unwrap_or_else(|_| panic!("unknown object type {}", starting_item))
    });

    let Some(tiled::TileLayer::Finite(tiles)) = map.get_layer(0).unwrap().as_tile_layer() else {
        panic!("Not a finite layer")
    };

    let are_walls = (0..10 * 11).map(|id| {
        let tile_x = id % 11;
        let tile_y = id / 11;

        let is_wall = tiles
            .get_tile(tile_x, tile_y)
            .map(|tile| {
                let tileset = tile.get_tileset();
                let tile_data = &tileset.get_tile(tile.id()).unwrap();
                tile_data
                    .user_type
                    .as_ref()
                    .map(|user_type| user_type == "WALL")
                    .unwrap_or(false)
            })
            .unwrap_or(true);

        is_wall
    });

    Level {
        starting_items: starting_items.collect(),
        fixed_positions: fixed_positions.collect(),
        solution_positions: solution_positions.collect(),
        directions: directions.collect(),
        wall_bitmap: bool_to_bit(&are_walls.collect::<Vec<_>>()),
        name: level_name.clone(),
    }
}

fn export_tiles(map: &tiled::Map, background: TokenStream) -> TokenStream {
    let map_tiles = map
        .get_tile_layer("Ground")
        .expect("The ground layer should exist");

    let width = map_tiles.width().unwrap() * 2;
    let height = map_tiles.height().unwrap() * 2;

    let map_tiles = (0..(height * width)).map(|pos| {
        let x = pos % width;
        let y = pos / width;

        let tile = map_tiles.get_tile(x as i32 / 2, y as i32 / 2);

        match tile {
            Some(tile) => {
                let vflip = tile.flip_v;
                let hflip = tile.flip_h;

                // calculate the actual tile ID based on the properties here
                // since the tiles in tiled are 16x16, but we want to export to 8x8, we have to work this out carefully

                let tile_tileset_x = tile.id() % 9;
                let tile_tileset_y = tile.id() / 9;

                let x_offset = if (x % 2 == 0) ^ hflip { 0 } else { 1 };
                let y_offset = if (y % 2 == 0) ^ vflip { 0 } else { 1 };
                let gba_tile_id =
                    tile_tileset_x * 2 + x_offset + tile_tileset_y * 9 * 4 + y_offset * 9 * 2;
                let gba_tile_id = gba_tile_id as u16;

                quote! { backgrounds::#background.tile_settings[#gba_tile_id as usize].hflip(#hflip).vflip(#vflip) }
            }
            None => {
                quote! { TileSetting::BLANK }
            }
        }
    });

    quote! {&[#(#map_tiles),*]}
}

fn export_ui_tiles(map: &tiled::Map, background: TokenStream) -> TokenStream {
    let map_tiles = map.get_layer(0).unwrap().as_tile_layer().unwrap();

    let width = map_tiles.width().unwrap();
    let height = map_tiles.height().unwrap();

    let map_tiles = (0..(height * width)).map(|pos| {
        let x = pos % width;
        let y = pos / width;

        let tile = map_tiles.get_tile(x as i32, y as i32);

        match tile {
            Some(tile) => {
                let tile_id = tile.id() as u16;
                let vflip = tile.flip_v;
                let hflip = tile.flip_h;

                quote! { backgrounds::#background.tile_settings[#tile_id as usize].hflip(#hflip).vflip(#vflip) }
            }
            None => {
                quote! { TileSetting::BLANK }
            }
        }
    });

    quote! {&[#(#map_tiles),*]}
}

fn bool_to_bit(bools: &[bool]) -> Vec<u8> {
    bools
        .chunks(8)
        .map(|x| {
            x.iter()
                .enumerate()
                .fold(0u8, |bits, (idx, &bit)| bits | ((bit as u8) << idx))
        })
        .collect()
}

#[test]
fn check_bool_to_bit() {
    let bools = [true, false, false, false, true, true, true, true];
    assert_eq!(bool_to_bit(&bools), [0b11110001]);
}

trait TiledMapExtensions {
    fn get_object_layer(&self, name: &str) -> Option<ObjectLayer>;
    fn get_tile_layer(&self, name: &str) -> Option<TileLayer>;
}

impl TiledMapExtensions for Map {
    fn get_object_layer(&self, name: &str) -> Option<ObjectLayer> {
        self.layers()
            .find(|x| x.name == name)
            .and_then(|x| x.as_object_layer())
    }

    fn get_tile_layer(&self, name: &str) -> Option<TileLayer> {
        self.layers()
            .find(|x| x.name == name)
            .and_then(|x| x.as_tile_layer())
    }
}