use std::collections::HashMap;
use std::str::FromStr;

use diesel::prelude::*;
use diesel::SqliteConnection;
use uuid::Uuid;

use crate::models::{Block, BlockProperty};
use crate::utils::rank::SaveFileRanking;

pub type SavedBlockProperties = HashMap<String, Vec<u8>>;

pub struct BlockRanking {
    pub previous_id: Option<String>,
    pub next_id: Option<String>,
}

// todo: return Result
pub trait BlockCrud {
    fn create_block(&self, ranking: BlockRanking, block_type: String) -> Block;
    fn retrieve_block(&self, block_id: &str) -> Block;
    fn retrieve_block_with_properties(&self, block_id: &str) -> (Block, SavedBlockProperties);
    fn retrieve_all_blocks(&self) -> Vec<Block>;
    fn retrieve_all_blocks_with_properties(&self) -> HashMap<Block, SavedBlockProperties>;
    fn update_block_rank(&self, block_id: &str, ranking: BlockRanking) -> Block;
    fn delete_block(&self, block_id: &str);
}

impl BlockCrud for SqliteConnection {
    // todo: add properties too
    fn create_block(&self, ranking: BlockRanking, block_type: String) -> Block {
        use crate::schema::blocks::dsl::*;

        let block_uuid = Uuid::new_v4().as_bytes().to_vec();
        let block_rank = self.generate_ranking(ranking.previous_id, ranking.next_id);

        let new_block = Block {
            id: block_uuid.clone(),
            rank: block_rank,
            type_: block_type,
        };

        diesel::insert_into(blocks)
            .values(&new_block)
            .execute(self)
            .and_then(|_| blocks.find(&block_uuid).first(self))
            .expect("Error saving new block")
    }

    // todo: add properties too
    fn retrieve_block(&self, block_id: &str) -> Block {
        use crate::schema::blocks::dsl::*;

        let block_uuid = Uuid::from_str(&block_id).unwrap().as_bytes().to_vec();

        blocks
            .find(block_uuid)
            .first(self)
            .expect(&format!("Could not retrieve block with {}", block_id))
    }

    fn retrieve_block_with_properties(&self, block_id: &str) -> (Block, SavedBlockProperties) {
        use crate::schema::block_properties::dsl as block_properties_dsl;
        use crate::schema::blocks::dsl as blocks_dsl;

        let block_uuid = Uuid::from_str(&block_id).unwrap().as_bytes().to_vec();

        let mut results: Vec<(Block, BlockProperty)> = blocks_dsl::blocks
            .find(block_uuid)
            .inner_join(block_properties_dsl::block_properties)
            .get_results(self)
            .expect(&format!("Could not retrieve block with {}", block_id));

        let mut block: Option<Block> = None;
        let mut properties: SavedBlockProperties = SavedBlockProperties::new();

        while !results.is_empty() {
            let (current_block, property) = results.remove(0);

            if block.is_none() {
                block = Some(current_block);
            }

            properties.insert(property.name, property.data);
        }

        (block.unwrap(), properties)
    }

    fn retrieve_all_blocks(&self) -> Vec<Block> {
        use crate::schema::blocks::dsl::*;

        blocks
            .order(rank)
            .get_results(self)
            .expect("Could not retrieve all blocks from save file")
    }

    fn retrieve_all_blocks_with_properties(&self) -> HashMap<Block, SavedBlockProperties> {
        use crate::schema::block_properties::dsl as block_properties_dsl;
        use crate::schema::blocks::dsl as blocks_dsl;

        let mut results: Vec<(Block, BlockProperty)> = blocks_dsl::blocks
            .order(blocks_dsl::rank)
            .inner_join(block_properties_dsl::block_properties)
            .get_results(self)
            .expect("Could not retrieve blocks with their properties");

        let mut blocks: HashMap<Block, SavedBlockProperties> = HashMap::new();

        while !results.is_empty() {
            let (current_block, property) = results.remove(0);

            if !blocks.contains_key(&current_block) {
                let mut properties = SavedBlockProperties::new();
                properties.insert(property.name, property.data);

                blocks.insert(current_block, properties);
            } else {
                blocks
                    .get_mut(&current_block)
                    .unwrap()
                    .insert(property.name, property.data);
            }
        }

        blocks
    }

    fn update_block_rank(&self, block_id: &str, ranking: BlockRanking) -> Block {
        use crate::schema::blocks::dsl::*;

        let block_uuid = Uuid::from_str(&block_id).unwrap().as_bytes().to_vec();
        let block_rank = self.generate_ranking(ranking.previous_id, ranking.next_id);

        diesel::update(blocks.find(&block_uuid))
            .set(rank.eq(block_rank))
            .execute(self)
            .and_then(|_| blocks.find(&block_uuid).first(self))
            .expect(&format!("Could not find block {}", block_id))
    }

    fn delete_block(&self, block_id: &str) {
        use crate::schema::blocks::dsl::*;

        let block_uuid = Uuid::from_str(&block_id).unwrap().as_bytes().to_vec();

        diesel::delete(blocks.find(block_uuid))
            .execute(self)
            .expect(&format!("Could not delete block {}", block_id));
    }
}
