use super::record::*;
use super::stat::*;
use data::gacha;
use data::gacha::gacha_config::*;
use data::tables::ItemID;

use chrono::{DateTime, Local};
use proto::GachaModelBin;
use rand::{thread_rng, Rng};
use std::collections::{HashMap, HashSet};
use std::hash::{BuildHasher, Hash};

pub struct GachaModel {
    pub gacha_status_map: HashMap<String, GachaStatus>,
    pub gacha_records: Vec<GachaRecord>,
}

impl Default for GachaModel {
    fn default() -> GachaModel {
        let result = GachaModel {
            gacha_status_map: HashMap::new(),
            gacha_records: vec![],
        };
        result.post_deserialize()
    }
}

impl GachaModel {
    pub fn from_bin(gacha_bin: GachaModelBin) -> Self {
        let result = Self {
            gacha_status_map: gacha_bin
                .gacha_status_map
                .into_iter()
                .map(|(k, v)| (k, GachaStatus::from_bin(v)))
                .collect(),
            gacha_records: gacha_bin
                .gacha_records
                .into_iter()
                .map(|x| GachaRecord::from_bin(x))
                .collect(),
        };
        result.post_deserialize()
    }

    pub fn to_bin(&self) -> GachaModelBin {
        GachaModelBin {
            gacha_status_map: self
                .gacha_status_map
                .iter()
                .map(|(k, v)| (k.clone(), v.to_bin()))
                .collect(),
            gacha_records: self.gacha_records.iter().map(|x| x.to_bin()).collect(),
            ..Default::default()
        }
    }

    pub fn post_deserialize(mut self) -> GachaModel {
        let gachaconf = gacha::global_gacha_config();
        for gacha_pool in gachaconf.character_gacha_pool_list.iter() {
            let mut gacha_status_map = &mut self.gacha_status_map;
            let status_bin = get_or_add(
                &mut gacha_status_map,
                &gacha_pool.sharing_guarantee_info_category,
            );
            for rarity_items in gacha_pool.gacha_items.iter() {
                let progress_bin =
                    get_or_add(&mut status_bin.rarity_status_map, &rarity_items.rarity);
                if progress_bin.pity <= 0 {
                    progress_bin.pity = 1;
                }
                for category_guarantee_policy_tag in
                    rarity_items.category_guarantee_policy_tags.iter()
                {
                    get_or_add(
                        &mut progress_bin.categories_progress_map,
                        &category_guarantee_policy_tag,
                    );

                    let guarantee_policy = gachaconf
                        .category_guarantee_policy_map
                        .get(category_guarantee_policy_tag)
                        .unwrap();
                    if !guarantee_policy.chooseable {
                        continue;
                    }
                    get_or_add(
                        &mut progress_bin.categories_chosen_guarantee_progress_map,
                        &category_guarantee_policy_tag,
                    );
                }
            }
            for discount_policy_tag in gacha_pool.discount_policy_tags.iter() {
                if gachaconf
                    .discount_policies
                    .free_select_map
                    .contains_key(discount_policy_tag)
                {
                    let policy = gachaconf
                        .discount_policies
                        .free_select_map
                        .get(discount_policy_tag)
                        .unwrap();
                    get_or_add(
                        &mut status_bin.discount_usage_map,
                        &policy.free_select_progress_record_tag,
                    );
                    get_or_add(
                        &mut status_bin.discount_usage_map,
                        &policy.free_select_usage_record_tag,
                    );
                } else {
                    get_or_add(&mut status_bin.discount_usage_map, &discount_policy_tag);
                }
            }
        }
        self
    }

    pub fn perform_pull_pool<'bin, 'conf>(
        &'bin mut self,
        pull_time: &DateTime<Local>,
        target_pool: &'conf CharacterGachaPool,
    ) -> GachaRecord {
        let (rarity_items, progress_bin, status_bin, probability_model) =
            self.determine_rarity(target_pool);
        let (category_tag, category) =
            self.determine_category(rarity_items, progress_bin, target_pool);
        let result = determine_gacha_result(
            pull_time,
            category,
            target_pool,
            status_bin,
            progress_bin,
            rarity_items,
        );
        self.update_pity(rarity_items, probability_model, target_pool);
        self.update_category_guarantee_info(rarity_items, &category_tag, target_pool);
        self.update_discount(target_pool, &category_tag, rarity_items);
        result
    }

    fn rand_rarity<'bin, 'conf>(
        &'bin self,
        target_pool: &'conf CharacterGachaPool,
        status_bin: &'bin GachaStatus,
    ) -> (
        &'conf GachaAvailableItemsInfo,
        &'bin GachaProgress,
        &'conf ProbabilityModel,
    ) {
        let gachaconf = gacha::global_gacha_config();
        let mut rng = thread_rng();
        let rarity_status_map = &status_bin.rarity_status_map;
        // gacha_items is already sorted by rarity descendingly in its post_configure.
        for rarity_items in target_pool.gacha_items.iter() {
            // Surely any judgement should be made on the current pity.
            let progress_bin = rarity_status_map.get(&rarity_items.rarity).unwrap();
            let pity = progress_bin.pity;
            let probability_model = gachaconf
                .probability_model_map
                .get(&rarity_items.probability_model_tag)
                .unwrap();
            if rng.gen_range(0.0..100.0) <= probability_model.get_chance_percent(&pity) {
                return (rarity_items, progress_bin, probability_model);
            }
        }
        panic!("The user failed to get any items.");
    }

    fn determine_rarity<'bin, 'conf>(
        &'bin self,
        target_pool: &'conf CharacterGachaPool,
    ) -> (
        &'conf GachaAvailableItemsInfo,
        &'bin GachaProgress,
        &'bin GachaStatus,
        &'conf ProbabilityModel,
    ) {
        let gachaconf = gacha::global_gacha_config();
        let status_bin = self
            .gacha_status_map
            .get(&target_pool.sharing_guarantee_info_category)
            .expect(&format!(
                "post_deserialize forgot StatusBin/sharing_guarantee_info_category: {}",
                target_pool.sharing_guarantee_info_category
            ));
        let (mut rarity_items, mut progress_bin, mut probability_model) =
            self.rand_rarity(target_pool, &status_bin);

        // We should take AdvancedGuarantee discount into consideration.
        for discount_tag in target_pool.discount_policy_tags.iter() {
            if let Some(discount) = gachaconf
                .discount_policies
                .advanced_guarantee_map
                .get(discount_tag)
            {
                if discount.rarity <= rarity_items.rarity {
                    continue;
                }
                if status_bin
                    .discount_usage_map
                    .get(discount_tag)
                    .expect(&format!(
                        "post_deserialize forgot StatusBin/discount_usage_map: {}",
                        discount_tag
                    ))
                    >= &discount.use_limit
                {
                    continue;
                }
                let higher_progress_bin = status_bin
                    .rarity_status_map
                    .get(&discount.rarity)
                    .expect(&format!(
                        "post_deserialize forgot StatusBin/rarity_status_map: {}",
                        &discount.rarity
                    ));
                if higher_progress_bin.pity >= discount.guarantee_pity {
                    let mut found_rarity_items = false;
                    for gacha_items in target_pool.gacha_items.iter() {
                        if gacha_items.rarity == discount.rarity {
                            rarity_items = gacha_items;
                            probability_model = gachaconf
                                .probability_model_map
                                .get(&gacha_items.probability_model_tag)
                                .unwrap();
                            found_rarity_items = true;
                            break;
                        }
                    }
                    assert!(found_rarity_items, "Handle AdvancedGuarantee Discount ({discount_tag}) error: The target rarity does not exist in this pool.");
                    progress_bin = higher_progress_bin;
                }
            }
        }

        (rarity_items, progress_bin, status_bin, probability_model)
    }

    fn determine_category<'bin, 'conf>(
        &'bin self,
        rarity_items: &'conf GachaAvailableItemsInfo,
        progress_bin: &'bin GachaProgress,
        target_pool: &'conf CharacterGachaPool,
    ) -> (String, &'conf GachaCategoryInfo) {
        let gachaconf = gacha::global_gacha_config();
        let mut category_tag_inited = false;
        let mut category_tag_result: HashSet<String> = HashSet::new();
        // First of all, if there's a chooseable category and
        // it is SELECTED then we MUST give that category's item.
        for guarantee_policy_tag in rarity_items.category_guarantee_policy_tags.iter() {
            let category_guarantee_policy = gachaconf
                .category_guarantee_policy_map
                .get(guarantee_policy_tag)
                .unwrap();
            if !category_guarantee_policy.chooseable {
                continue;
            }
            // As we found a policy defined chooseable, we
            // should head to look whether the user chose
            // the category he want.
            if let Some(category_tag) = progress_bin
                .categories_chosen_guarantee_category_map
                .get(guarantee_policy_tag)
            {
                // User chose a category; our work are done here.
                category_tag_result.insert(category_tag.clone());
                category_tag_inited = true;
            }
        }
        // Then we should take a look at MustGainItem.
        if !category_tag_inited {
            for discount_policy_tag in target_pool.discount_policy_tags.iter() {
                if let Some(discount) = gachaconf
                    .discount_policies
                    .must_gain_item_map
                    .get(discount_policy_tag)
                {
                    if discount.rarity != rarity_items.rarity {
                        continue;
                    }
                    category_tag_result.insert(discount.category_tag.clone());
                    category_tag_inited = true;
                }
            }
        }
        // Otherwise, just select as normal.
        if !category_tag_inited {
            for tag in rarity_items.categories.keys() {
                category_tag_result.insert(tag.clone());
            }
            for guarantee_policy_tag in rarity_items.category_guarantee_policy_tags.iter() {
                let category_guarantee_policy = gachaconf
                    .category_guarantee_policy_map
                    .get(guarantee_policy_tag)
                    .unwrap();
                let failure_times = progress_bin.categories_progress_map
                    .get(guarantee_policy_tag)
                    .expect(&format!("post_deserialize forgot StatusBin/rarity_status_map[{}]/categories_progress_map: {}", &rarity_items.rarity, guarantee_policy_tag));
                if failure_times >= &category_guarantee_policy.trigger_on_failure_times {
                    category_tag_result = category_tag_result
                        .intersection(&category_guarantee_policy.included_category_tags)
                        .cloned()
                        .collect();
                }
            }
            // category_tag_inited = true;
        }

        let mut categories: Vec<(String, &GachaCategoryInfo)> = vec![];
        let mut weight_sum = 0;
        for result_tag in category_tag_result {
            let category = rarity_items.categories.get(&result_tag).unwrap();
            categories.push((result_tag, category));
            weight_sum += category.category_weight;
        }

        let randomnum = rand::thread_rng().gen_range(0..weight_sum);
        let mut enumerated_ranges_end = 0;
        for category in categories.into_iter() {
            if randomnum <= enumerated_ranges_end + category.1.category_weight {
                return (category.0, category.1);
            }
            enumerated_ranges_end += category.1.category_weight;
        }
        panic!("No category is chosen.");
    }

    fn update_pity<'bin, 'conf>(
        &'bin mut self,
        rarity_items: &'conf GachaAvailableItemsInfo,
        probability_model: &'conf ProbabilityModel,
        target_pool: &'conf CharacterGachaPool,
    ) {
        let status_bin = self
            .gacha_status_map
            .get_mut(&target_pool.sharing_guarantee_info_category)
            .unwrap();
        for (rarity, rarity_status) in status_bin.rarity_status_map.iter_mut() {
            if (rarity == &rarity_items.rarity)
                || (probability_model.clear_status_on_higher_rarity_pulled
                    && rarity < &rarity_items.rarity)
            {
                rarity_status.pity = 1;
            } else {
                rarity_status.pity += 1;
            }
        }
    }

    fn update_category_guarantee_info<'bin, 'conf>(
        &'bin mut self,
        rarity_items: &'conf GachaAvailableItemsInfo,
        category_tag: &String,
        target_pool: &'conf CharacterGachaPool,
    ) {
        let gachaconf = gacha::global_gacha_config();
        let status_bin = self
            .gacha_status_map
            .get_mut(&target_pool.sharing_guarantee_info_category)
            .unwrap();
        let progress_bin = status_bin
            .rarity_status_map
            .get_mut(&rarity_items.rarity)
            .unwrap();
        for policy_tag in rarity_items.category_guarantee_policy_tags.iter() {
            let policy = gachaconf
                .category_guarantee_policy_map
                .get(policy_tag)
                .unwrap();
            // TODO: Chooseable guarantee not implemented
            let prev_failure = progress_bin
                .categories_progress_map
                .get_mut(policy_tag)
                .expect(&format!(
                "post_deserialize forgot StatusBin/rarity_status_map[{}]/categories_progress_map: {}",
                rarity_items.rarity, policy_tag
            ));
            if policy.included_category_tags.contains(category_tag) {
                *prev_failure = 0;
            } else {
                *prev_failure += 1;
            }
        }
    }

    fn update_discount<'bin, 'conf>(
        &'bin mut self,
        target_pool: &'conf CharacterGachaPool,
        category_tag: &String,
        rarity_items: &GachaAvailableItemsInfo,
    ) {
        let gachaconf = gacha::global_gacha_config();
        for (policy_tag, policy) in gachaconf.discount_policies.must_gain_item_map.iter() {
            if *category_tag != policy.category_tag {
                continue;
            }
            if !target_pool.discount_policy_tags.contains(policy_tag) {
                continue;
            }
            let status_bin = self
                .gacha_status_map
                .get_mut(&target_pool.sharing_guarantee_info_category)
                .unwrap();
            let usage = status_bin.discount_usage_map.get_mut(policy_tag).unwrap();
            if *usage < policy.use_limit {
                *usage += 1;
            }
        }
        for (policy_tag, policy) in gachaconf.discount_policies.advanced_guarantee_map.iter() {
            if rarity_items.rarity != policy.rarity {
                continue;
            }
            if !target_pool.discount_policy_tags.contains(policy_tag) {
                continue;
            }
            let status_bin = self
                .gacha_status_map
                .get_mut(&target_pool.sharing_guarantee_info_category)
                .unwrap();
            let usage = status_bin.discount_usage_map.get_mut(policy_tag).unwrap();
            if *usage < policy.use_limit {
                *usage += 1;
            }
        }
        for (policy_tag, policy) in gachaconf.discount_policies.free_select_map.iter() {
            if !target_pool.discount_policy_tags.contains(policy_tag) {
                continue;
            }
            let status_bin = self
                .gacha_status_map
                .get_mut(&target_pool.sharing_guarantee_info_category)
                .unwrap();
            let progress = status_bin
                .discount_usage_map
                .get_mut(&policy.free_select_progress_record_tag)
                .unwrap();
            *progress += 1;
        }
    }
}

fn get_or_add<'a, K: Eq + PartialEq + Hash + Clone, V: Default, S: BuildHasher>(
    map: &'a mut HashMap<K, V, S>,
    key: &K,
) -> &'a mut V {
    if !map.contains_key(key) {
        map.insert(key.clone(), V::default());
    }
    map.get_mut(key).unwrap()
}

fn determine_gacha_result<'bin, 'conf>(
    pull_time: &DateTime<Local>,
    category: &'conf GachaCategoryInfo,
    target_pool: &'conf CharacterGachaPool,
    status_bin: &'bin GachaStatus,
    progress_bin: &'bin GachaProgress,
    rarity_items: &'conf GachaAvailableItemsInfo,
) -> GachaRecord {
    let gachaconf = gacha::global_gacha_config();
    let item_pool_len = category.item_ids.len() as u32;
    let mut item_id: Option<&u32> = None;
    // We should see whether user's search priority exists.
    for guarantee_policy_tag in rarity_items.category_guarantee_policy_tags.iter() {
        let category_guarantee_policy = gachaconf
            .category_guarantee_policy_map
            .get(guarantee_policy_tag)
            .unwrap();
        if !category_guarantee_policy.chooseable {
            continue;
        }
        // Firstly, judge whether the user failed enough times.
        // The user is limited to get only this category's item,
        // so we should record the user's failure to get his
        // selected item elsewhere.
        if progress_bin
            .categories_chosen_guarantee_progress_map
            .get(guarantee_policy_tag)
            .unwrap()
            < &category_guarantee_policy.trigger_on_failure_times
        {
            continue;
        }
        // We directly look whether user chose an UP item.
        if let Some(item) = progress_bin
            .categories_chosen_guarantee_item_map
            .get(guarantee_policy_tag)
        {
            item_id = Some(item);
        }
    }

    let item_id = match item_id {
        Some(val) => val,
        None => category
            .item_ids
            .get(rand::thread_rng().gen_range(0..item_pool_len) as usize)
            .unwrap(),
    };
    let mut extra_item_id: Option<ItemID> = None;
    let mut extra_item_count: u32 = 0;

    for extra_items_policy_tag in rarity_items.extra_items_policy_tags.iter() {
        let extra_items_policy = gachaconf
            .extra_items_policy_map
            .get(extra_items_policy_tag)
            .unwrap();
        // TODO: apply_on_owned_count in a context with bag
        // TODO: That's what RoleModel should do, not me.
        if extra_items_policy.apply_on_owned_count == 0 {
            extra_item_id = ItemID::new(extra_items_policy.id);
            extra_item_count = extra_items_policy.count;
        }
    }
    let extra_resources = match extra_item_id {
        Some(item_id) => Some(GachaExtraResources {
            extra_item_id: item_id,
            extra_item_count,
        }),
        None => None,
    };
    GachaRecord {
        pull_timestamp: pull_time.timestamp(),
        obtained_item_id: ItemID::new_unchecked(item_id.clone()),
        gacha_id: target_pool.gacha_schedule_id.clone(),
        progress_map: status_bin.rarity_status_map.clone(),
        extra_resources,
        item_type: category.item_type.clone(),
    }
}
