use uuid::Uuid;
use petgraph::graph::{Graph, UnGraph, NodeIndex};
// use petgraph::dot::{Dot, Config};

// Db Commons
use account::Account;
use serde_cbor::{from_slice, to_vec};
use postgres::transaction::Transaction;
use failure::Error;
use failure::err_msg;

// shapes
use rand::prelude::*;
use rand::{thread_rng};
use rand::distributions::{WeightedIndex};

use game::{Game, GameMode, game_pve_new, game_write};
use rpc::{ZoneJoinParams, ZoneCloseParams};

#[derive(Debug,Clone,Serialize,Deserialize)]
pub struct Zone {
    id: Uuid,
    account: Uuid,
    active: bool,
    graph: UnGraph<Encounter, ()>,
}

#[derive(Debug,Clone,Copy)]
enum Shape {
    Diamond,
    Line,
    Plus,
    Diode,
    Domino,
    Kite,
}

pub type ZoneGraph = UnGraph<Encounter, ()>;

#[derive(Debug,Clone,PartialEq,Eq,Hash,PartialOrd,Ord,Serialize,Deserialize)]
pub struct Encounter {
    tag: String,
    game_id: Option<Uuid>,
    success: bool,
    x: i8,
    y: i8,
}

impl Encounter {
    fn new(tag: &'static str, x: i8, y: i8) -> Encounter {
        return Encounter {
            tag: tag.to_string(),
            success: false,
            game_id: None,
            x,
            y,
        };
    }
    fn start() -> Encounter {
        return Encounter {
            tag: "START".to_string(),
            success: true,
            game_id: None,
            x: 0,
            y: 0,
        };
    }
}

pub fn zone_delete(tx: &mut Transaction, id: Uuid) -> Result<(), Error> {
    let query = "
        DELETE
        FROM zones
        WHERE id = $1;
    ";

    let result = tx
        .execute(query, &[&id])?;

    if result != 1 {
        return Err(format_err!("unable to delete zone {:?}", id));
    }

    println!("zone deleted {:?}", id);

    return Ok(());
}

pub fn zone_get(tx: &mut Transaction, id: Uuid) -> Result<Zone, Error> {
    let query = "
        SELECT *
        FROM zones
        WHERE id = $1
    ";

    let result = tx
        .query(query, &[&id])?;

    let returned = match result.iter().next() {
        Some(row) => row,
        None => return Err(err_msg("zone not found")),
    };

    // tells from_slice to cast into a cryp
    let bytes: Vec<u8> = returned.get("data");
    let zone = match from_slice::<Zone>(&bytes) {
        Ok(z) => z,
        Err(_) => {
            zone_delete(tx, id)?;
            return Err(err_msg("invalid zone removed"))
        },
    };

    return Ok(zone);
}

pub fn zone_create(tx: &mut Transaction, account: &Account) -> Result<Zone, Error> {
    let id = Uuid::new_v4();
    let graph = create_zone_graph();

    let zone = Zone {
        id,
        account: account.id,
        graph,
        active: true,
    };

    let bytes = to_vec(&zone)?;

    let query = "
        INSERT INTO zones (id, data, account)
        VALUES ($1, $2, $3)
        RETURNING id;
    ";

    let result = tx
        .query(query, &[&id, &bytes, &account.id])?;

    result.iter().next().ok_or(format_err!("no zone written"))?;

    return Ok(zone);
}

pub fn zone_update(zone: &Zone, tx: &mut Transaction) -> Result<(), Error> {
    let bytes = to_vec(&zone)?;

    let query = "
        UPDATE zones
        SET data = $1, active = $2
        WHERE id = $3
        RETURNING id, data;
    ";

    let result = tx
        .query(query, &[&bytes, &zone.active, &zone.id])?;

    result.iter().next().ok_or(format_err!("zone {:?} could not be written", zone))?;

    return Ok(());
}

pub fn zone_join(params: ZoneJoinParams, tx: &mut Transaction, account: &Account) -> Result<Game, Error> {
    let mut zone = zone_get(tx, params.zone_id)?;
    let mut game;

    // check node joinable
    node_joinable(&zone.graph, NodeIndex::from(params.node_id))?;

    // borrow zone.graph to make the game
    {
        let node_index = NodeIndex::from(params.node_id);
        let encounter = zone.graph
            .node_weight_mut(node_index)
            .ok_or(err_msg("invalid encounter id"))?;

        let mode = match encounter.tag.as_ref() {
            "NORMAL" => GameMode::Zone3v2Attack,
            "CASTER" => GameMode::Zone2v2Caster,
            "MINIBOSS" => GameMode::Zone3v3MeleeMiniboss,
            "BOSS" => GameMode::Zone3v3HealerBoss,
            _ => return Err(err_msg("unknown zone tag")),
        };
        game = game_pve_new(params.cryp_ids, mode, tx, account)?;
        game.set_zone(zone.id, params.node_id);

        encounter.game_id = Some(game.id);
    }

    // persist
    game_write(&game, tx)?;
    zone_update(&zone, tx)?;

    return Ok(game);
}

pub fn zone_close(params: ZoneCloseParams, tx: &mut Transaction, _account: &Account) -> Result<(), Error> {
    let mut zone = zone_get(tx, params.zone_id)?;
    zone.active = false;
    zone_update(&zone, tx)?;
    return Ok(());
}

// shapes should always add the exit normal node
fn add_shape(shape: Shape, gr: &mut ZoneGraph, start: NodeIndex, x: i8, y: i8) -> (NodeIndex, i8, i8) {
    match shape {
        Shape::Line => {
            let mut next = gr.add_node(Encounter::new("CASTER", x, y));
            gr.add_edge(start, next, ());

            let exit = gr.add_node(Encounter::new("NORMAL", x + 1, y));
            gr.add_edge(next, exit, ());

            return (exit, x + 1, y);
        },
        Shape::Plus => {
            let top = gr.add_node(Encounter::new("MINIBOSS", x, y + 1));
            gr.add_edge(start, top, ());

            let bottom = gr.add_node(Encounter::new("MINIBOSS", x, y - 1));
            gr.add_edge(start, bottom, ());

            let exit = gr.add_node(Encounter::new("NORMAL", x + 1, y));
            gr.add_edge(start, exit, ());

            return (exit, x + 1, y);
        },
        Shape::Diode => {
            let top = gr.add_node(Encounter::new("MINIBOSS", x, y + 1));
            gr.add_edge(start, top, ());

            let bottom = gr.add_node(Encounter::new("MINIBOSS", x, y - 1));
            gr.add_edge(start, bottom, ());

            let exit = gr.add_node(Encounter::new("NORMAL", x + 1, y));
            gr.add_edge(start, exit, ());

            // connect top and exit for an extra chance
            gr.add_edge(top, exit, ());
            gr.add_edge(bottom, exit, ());

            return (exit, x + 1, y);
        },
        Shape::Kite => {
            let top = gr.add_node(Encounter::new("MINIBOSS", x + 1, y + 1));
            gr.add_edge(start, top, ());

            let top_tip = gr.add_node(Encounter::new("BOSS", x + 1, y + 2));
            gr.add_edge(top, top_tip, ());

            let bottom = gr.add_node(Encounter::new("MINIBOSS", x + 1, y - 1));
            gr.add_edge(start, bottom, ());

            let bottom_tip = gr.add_node(Encounter::new("BOSS", x + 1, y - 2));
            gr.add_edge(bottom, bottom_tip, ());

            let side = gr.add_node(Encounter::new("CASTER", x + 2, y));
            gr.add_edge(start, side, ());
            gr.add_edge(top, side, ());
            gr.add_edge(bottom, side, ());

            let exit = gr.add_node(Encounter::new("NORMAL", x + 3, y));
            gr.add_edge(side, exit, ());

            return (exit, x + 3, y);
        },
        Shape::Domino => {
            let top = gr.add_node(Encounter::new("NORMAL", x, y + 1));
            gr.add_edge(start, top, ());

            let top_tip = gr.add_node(Encounter::new("MINIBOSS", x + 1, y + 1));
            gr.add_edge(top, top_tip, ());

            let bottom = gr.add_node(Encounter::new("MINIBOSS", x, y - 1));
            gr.add_edge(start, bottom, ());

            let bottom_tip = gr.add_node(Encounter::new("NORMAL", x + 1, y - 1));
            gr.add_edge(bottom, bottom_tip, ());

            let side = gr.add_node(Encounter::new("CASTER", x + 1, y));
            gr.add_edge(top_tip, side, ());
            gr.add_edge(bottom_tip, side, ());

            let exit = gr.add_node(Encounter::new("NORMAL", x + 2, y));
            gr.add_edge(side, exit, ());

            return (exit, x + 2, y);
        },
        Shape::Diamond => {
            let top = gr.add_node(Encounter::new("NORMAL", x + 1, y + 1));
            gr.add_edge(start, top, ());

            let top_tip = gr.add_node(Encounter::new("MINIBOSS", x + 2, y + 1));
            gr.add_edge(top, top_tip, ());

            let bottom = gr.add_node(Encounter::new("MINIBOSS", x + 1, y - 1));
            gr.add_edge(start, bottom, ());

            let bottom_tip = gr.add_node(Encounter::new("NORMAL", x + 2, y - 1));
            gr.add_edge(bottom, bottom_tip, ());

            let exit = gr.add_node(Encounter::new("CASTER", x + 3, y));
            gr.add_edge(top_tip, exit, ());
            gr.add_edge(bottom_tip, exit, ());

            return (exit, x + 3, y);
        },
        // _ => panic!("nyi shape"),
    }
}

pub fn create_zone_graph() -> ZoneGraph {
    let mut gr = Graph::new_undirected();
    let mut rng = thread_rng();

    let mut last = gr.add_node(Encounter::start());
    let mut x = 0;
    let mut y = 0;

    for _i in 0..4 {
        let shapes = vec![
            (Shape::Line, 1),
            (Shape::Diamond, 1),
            (Shape::Diode, 1),
            (Shape::Kite, 1),
            (Shape::Domino, 1),
            (Shape::Plus, 1),
        ];

        let dist = WeightedIndex::new(shapes.iter().map(|item| item.1)).unwrap();
        let shape = shapes[dist.sample(&mut rng)].0;

        let result = add_shape(shape, &mut gr, last, x, y);
        last = result.0;
        x = result.1;
        y = result.2;
    }

    return gr;
}

pub fn node_joinable(graph: &ZoneGraph, target_index: NodeIndex) -> Result<(), Error> {
    // early return for already attempted
    {
        let target_encounter = match graph.node_weight(target_index) {
            Some(encounter) => encounter,
            None => panic!("{:?} has no weight for {:?}", graph, target_index),
        };

        if target_encounter.game_id.is_some() {
            return Err(err_msg("node already attempted"));
        }
    }

    let success_indices = graph.node_indices().filter(|i| {
        match graph.node_weight(*i) {
           Some(encounter) => encounter.success,
           None => panic!("no weight for {:?}", i),
        }
    });

    // if a node is a neighbour of that graph
    // and hasn't been attempted
    // it is joinable
    for i in success_indices {
        match graph.neighbors(i).find(|n| *n == target_index) {
            Some(_n) => return Ok(()),
            None => continue,
        };
    }

    return Err(err_msg("node requirements not met"));
}

pub fn node_finish(game: &Game, zone_id: Uuid, node_index: u32, tx: &mut Transaction) -> Result<Zone, Error> {
    let mut zone = zone_get(tx, zone_id)?;

    let winner_id = match game.winner() {
        Some(w) => w.id,
        None => return Ok(zone),
    };

    if zone.account == winner_id {
        {
            let encounter = zone.graph
                .node_weight_mut(NodeIndex::from(node_index))
                .ok_or(err_msg("encounter not found for game zone update"))?;

            encounter.success = true;
        }
        zone_update(&zone, tx)?;
    }

    Ok(zone)
}

#[cfg(test)]
mod tests {
    use zone::*;

    #[test]
    fn create_zone_test() {
        let _graph = create_zone_graph();
        // good shit;
        // let nodes = graph.node_indices().collect::<Vec<NodeIndex>>();
        // println!("{:?}", nodes[0]);
        // println!("{:?}", graph.node_weight(nodes[0]));

        // println!("{:?}", Dot::with_config(&graph, &[Config::EdgeNoLabel]));
    }

    #[test]
    fn zone_joinable_test() {
        let graph = create_zone_graph();
        assert!(node_joinable(&graph, NodeIndex::from(1)).is_ok());
        assert!(node_joinable(&graph, NodeIndex::from(graph.node_count() as u32 - 1)).is_err());
    }
}
