use std::collections::HashSet;
use rand::prelude::{SliceRandom, StdRng};
use crate::city::{Rectangle, HOUSE_RANGE, House, HouseLayout};
use itertools::iproduct;

pub enum RectangleSearchError {
    Useless,
    Unsatisfiable,
}

pub fn iterate_improvements(mut layout: &mut HouseLayout, mut rng: &mut StdRng, print_progress: bool, merge_first: bool) -> bool {
    #[derive(Eq, PartialEq)]
    enum LastStep {
        None,
        MovingIndividual,
        MergingPairs,
    }

    let mut improved = false;

    let mut first_iteration = true;
    let mut last_improved_step = LastStep::None;
    loop {
        if merge_first && first_iteration {
            first_iteration = false;
        } else {
            if last_improved_step == LastStep::MovingIndividual { break; }
            if print_progress {
                eprintln!("Starting moving individual houses...");
            }
            if improve_move_individual_houses(&mut layout, &mut rng) {
                last_improved_step = LastStep::MovingIndividual;
                improved = true;
            }
            if print_progress {
                eprintln!("Finished moving individual houses...");
            }
        }

        if last_improved_step == LastStep::MergingPairs { break; }
        if print_progress {
            eprintln!("Starting pairwise house merge...");
        }
        if improve_merge_pairwise(&mut layout, print_progress) {
            last_improved_step = LastStep::MergingPairs;
            improved = true;
        }
        if print_progress {
            eprintln!("Finished pairwise house merge");
        }
        if last_improved_step == LastStep::None { break; }
    }

    improved
}

fn get_valid_move_rectangle_multiple(layout: &HouseLayout, houses: &Vec<House>) -> Result<Rectangle, RectangleSearchError> {
    // This is a generalization of get_valid_move_rectangle, it's basically the same thing,
    // just with a dynamic rectangles_containing_count

    // We first establish a bounding box for an that has to be covered if all houses are removed.
    let mut covered_rect: Option<Rectangle> = None;
    for house in houses {
        let range_rect = house.range_rectangle(layout.city);
        for y in range_rect.top..=range_rect.bottom {
            for x in range_rect.left..=range_rect.right {
                // We count how many rectangles of houses contain this xy position.
                let mut rectangles_containing_count = 0;
                for house in houses {
                    let rect = house.range_rectangle(layout.city);
                    if rect.is_inside(x, y) {
                        rectangles_containing_count += 1;
                    }
                }

                // If this house is covered by the exact amount of rectangles,
                // then removing all input houses would uncover this position.
                // It cannot be less than the rectangle count, and more means there
                // is another house covering it as well.
                if layout.cover_count_xy(x, y) == rectangles_containing_count && layout.city.is_house_xy(x, y) {
                    if let Some(cover) = &mut covered_rect {
                        cover.left = cover.left.min(x);
                        cover.right = cover.right.max(x);
                        cover.top = cover.top.min(y);
                        cover.bottom = cover.bottom.max(y);
                    } else {
                        covered_rect = Some(Rectangle { left: x, right: x, top: y, bottom: y });
                    }
                }
            }
        }
    };

    if covered_rect.is_none() {
        // Unnecessary set of houses.
        return Err(RectangleSearchError::Useless);
    }

    let covered_rect = covered_rect.unwrap();

    let height_margin = HOUSE_RANGE as i32 - covered_rect.height() as i32;
    let width_margin = HOUSE_RANGE as i32 - covered_rect.width() as i32;

    let top = (covered_rect.top as i32 - height_margin).max(0) as usize;
    let left = (covered_rect.left as i32 - width_margin).max(0) as usize;
    let bottom = (covered_rect.bottom + height_margin as usize).min(layout.city.height() - 1);
    let right = (covered_rect.right + width_margin as usize).min(layout.city.width() - 1);

    if top > bottom || left > right {
        // Unsatisfiable rectangle by one house
        return Err(RectangleSearchError::Unsatisfiable);
    }

    Ok(Rectangle { left, right, top, bottom })
}

pub fn get_valid_move_rectangle(layout: &HouseLayout, house: House) -> Result<Rectangle, RectangleSearchError> {
    // We first establish a bounding box for an that has to be covered if the house is removed.
    let mut covered_rect: Option<Rectangle> = None;

    let range_rect = house.range_rectangle(layout.city);
    for y in range_rect.top..=range_rect.bottom {
        for x in range_rect.left..=range_rect.right {
            if layout.cover_count_xy(x, y) == 1 && layout.city.is_house_xy(x, y) {
                // This house is only covered by the house, it has to be covered from the new position as well.
                if let Some(cover) = &mut covered_rect {
                    cover.left = cover.left.min(x);
                    cover.right = cover.right.max(x);
                    cover.top = cover.top.min(y);
                    cover.bottom = cover.bottom.max(y);
                } else {
                    covered_rect = Some(Rectangle { left: x, right: x, top: y, bottom: y });
                }
            }
        }
    }

    if covered_rect.is_none() {
        return Err(RectangleSearchError::Useless);
    }

    let covered_rect = covered_rect.unwrap();

    // The distance of the rectangle from the original box tells us how much the house can move.
    let dist_left = covered_rect.left - range_rect.left;
    let dist_right = range_rect.right - covered_rect.right;
    let dist_top = covered_rect.top - range_rect.top;
    let dist_bottom = range_rect.bottom - covered_rect.bottom;

    let left = if house.x <= dist_right { 0 } else { house.x - dist_right };
    let right = if house.x >= layout.city.width() - 1 - dist_left { layout.city.width() - 1 } else { house.x + dist_left };
    let top = if house.y <= dist_bottom { 0 } else { house.y - dist_bottom };
    let bottom = if house.y >= layout.city.height() - 1 - dist_top { layout.city.height() - 1 } else { house.y + dist_top };

    let valid_move_rectangle = Rectangle {
        left,
        right,
        top,
        bottom,
    };

    Ok(valid_move_rectangle)
}

pub fn improve_move_individual_houses(layout: &mut HouseLayout, mut rng: &mut StdRng) -> bool {
    let mut improved = false;
    let mut untried_houses = layout.houses().clone();
    untried_houses.shuffle(&mut rng);

    while untried_houses.len() > 0 {
        let house = untried_houses.pop().unwrap();
        let house_index = layout.houses().iter().position(|x| *x == house).unwrap();

        let move_rectangle = match get_valid_move_rectangle(&layout, house) {
            Ok(move_rectangle) => move_rectangle,
            Err(RectangleSearchError::Useless) => {
                //let old_price = layout.price();
                layout.remove_house(house_index);
                //let new_price = layout.price();
                //let price_diff = new_price as i64 - old_price as i64;
                //eprintln!(" candidate is valid, price diff: {}.", price_diff);
                //eprintln!("Removed a house (useless), diff {}", price_diff);
                //eprintln!("Improved price: {}", new_price);
                improved = true;
                untried_houses = layout.houses().clone();
                untried_houses.shuffle(&mut rng);
                continue;
            }
            _ => unreachable!()
        };

        // TODO: Not needed, can just store best
        let mut new_candidates = Vec::new();
        for new_y in move_rectangle.top..=move_rectangle.bottom {
            for new_x in move_rectangle.left..=move_rectangle.right {
                if layout.city.is_house_xy(new_x, new_y) && layout.city.get_price_xy(new_x, new_y) < layout.city.get_price(house) {
                    new_candidates.push(House::new(new_x, new_y));
                }
            }
        }

        new_candidates.sort_by(|a, b| layout.city.get_price(*a).cmp(&layout.city.get_price(*b)));
        if new_candidates.len() == 0 {
            //eprintln!("Did not find candidate");
        } else {
            for (i, &candidate) in new_candidates.iter().enumerate() {
                //eprint!("Found candidate {}...", i);

                //let old_price = layout.price();
                layout.remove_house(house_index);
                layout.add_house(candidate);

                assert!(layout.is_valid());
                //let new_price = layout.price();
                //let price_diff = new_price as i64 - old_price as i64;
                //eprintln!(" candidate is valid, price diff: {}.", price_diff);
                //eprintln!("Improved price: {}", new_price);
                improved = true;
                untried_houses = layout.houses().clone();
                untried_houses.shuffle(&mut rng);
                break;
            }
        }
    }

    improved
}

pub fn improve_merge_pairwise(layout: &mut HouseLayout, print_progress: bool) -> bool {
    let mut improved = false;

    loop {
        // This here is a hack for being unable to modify the houses while looping through them.
        // We instead go through the houses repeatedly and remember which pairs we have already
        // tried by hashing their values because they can and do move throughout the layout Vec
        // as it's being modified.
        // TODO: This may lead to some pairs still being mergeable thanks to another merge
        //       that happened before.
        let mut checked = HashSet::new();

        let mut loop_improved = false;
        loop {
            let mut merge = None;

            'outer_houses: for i in 0..layout.houses().len() {
                for j in i + 1..layout.houses().len() {
                    let house1 = layout.houses()[i];
                    let house2 = layout.houses()[j];

                    let x_dist = (house1.x as i32 - house2.x as i32).abs() as usize;
                    let y_dist = (house1.y as i32 - house2.y as i32).abs() as usize;
                    if x_dist > 4 * HOUSE_RANGE || y_dist > 4 * HOUSE_RANGE {
                        // Never close enough to merge
                        continue;
                    }

                    if checked.contains(&(house1, house2)) || checked.contains(&(house2, house1)) {
                        continue;
                    } else {
                        checked.insert((house1, house2));
                    }

                    match get_valid_move_rectangle_multiple(&layout, &vec! {house1, house2}) {
                        Ok(rect) => {
                            let mut cheapest = None;
                            for y in rect.top..=rect.bottom {
                                for x in rect.left..=rect.right {
                                    if !layout.city.is_house_xy(x, y) { continue; }
                                    let price = layout.city.get_price_xy(x, y);
                                    match cheapest {
                                        None => cheapest = Some((x, y, price)),
                                        Some((_, _, cheapest_price)) if price < cheapest_price => cheapest = Some((x, y, price)),
                                        _ => {}
                                    };
                                }
                            }
                            if let Some((x, y, price)) = cheapest {
                                if price >= layout.city.get_price(house1) + layout.city.get_price(house2) {
                                    // Merging not worth
                                    //eprintln!("Merging not worth!");
                                } else {
                                    merge = Some((i, j, House::new(x, y)));
                                    break 'outer_houses;
                                }
                            }
                        }
                        Err(RectangleSearchError::Useless) => {
                            //eprintln!("Found useless pair of houses, not solving!")
                        },
                        Err(RectangleSearchError::Unsatisfiable) => {}
                    }
                }
            }

            if let Some((i, j, house)) = merge {
                let old_price = layout.price();
                assert!(i < j);
                layout.remove_house(j);
                layout.remove_house(i);
                layout.add_house(house);

                assert!(layout.is_valid());
                let new_price = layout.price();
                let price_diff = new_price as i32 - old_price as i32;
                if print_progress {
                    eprintln!("Merged two houses, new price {}, diff {}", new_price, price_diff);
                }
                improved = true;
                loop_improved = true;
            } else {
                break;
            }
        }

        if !loop_improved {
            break;
        }
    }

    improved
}