use crate::blockchain::coin::Coin;
use crate::blockchain::condition_opcode::ConditionOpcode;
use crate::blockchain::condition_with_args::ConditionWithArgs;
use crate::blockchain::sized_bytes::{Bytes32, SizedBytes};
use crate::blockchain::utils::atom_to_int;
use crate::clvm::program::SerializedProgram;
use crate::clvm::sexp::{IntoSExp, SExp};
use num_traits::ToPrimitive;
use std::collections::HashMap;
use std::io::{Error, ErrorKind};

pub fn parse_sexp_to_condition(sexp: &SExp) -> Result<ConditionWithArgs, Error> {
    let as_atoms = sexp.as_atom_list();
    if as_atoms.is_empty() {
        Err(Error::new(ErrorKind::InvalidData, "Invalid Condition"))
    } else {
        match as_atoms.split_first() {
            Some((first, rest)) => Ok(ConditionWithArgs {
                opcode: ConditionOpcode::from(first[0]),
                vars: Vec::from(rest),
            }),
            None => Err(Error::new(ErrorKind::InvalidData, "Invalid Condition")),
        }
    }
}

pub fn parse_sexp_to_conditions(sexp: SExp) -> Result<Vec<ConditionWithArgs>, Error> {
    let mut results = Vec::new();
    for arg in sexp.iter() {
        match parse_sexp_to_condition(arg) {
            Ok(condition) => {
                results.push(condition);
            }
            Err(error) => return Err(error),
        }
    }
    Ok(results)
}

pub fn conditions_by_opcode(
    conditions: Vec<ConditionWithArgs>,
) -> HashMap<ConditionOpcode, Vec<ConditionWithArgs>> {
    let mut hm: HashMap<ConditionOpcode, Vec<ConditionWithArgs>> = HashMap::new();
    for cvp in conditions {
        match hm.get_mut(&cvp.opcode) {
            Some(list) => {
                list.push(cvp.clone());
            }
            None => {
                hm.insert(cvp.opcode, vec![cvp.clone()]);
            }
        }
    }
    hm
}

pub fn created_outputs_for_conditions_dict(
    conditions_dict: HashMap<ConditionOpcode, Vec<ConditionWithArgs>>,
    input_coin_name: Bytes32,
) -> Result<Vec<Coin>, Error> {
    let mut output_coins = Vec::new();
    if let Some(args) = conditions_dict.get(&ConditionOpcode::CreateCoin) {
        for cvp in args {
            let amount = atom_to_int(&cvp.vars[1]).to_u64().ok_or_else(|| {
                Error::new(ErrorKind::InvalidInput, "Failed to convert atom to int")
            })?;
            let coin = Coin {
                parent_coin_info: input_coin_name,
                puzzle_hash: Bytes32::new(&cvp.vars[0]),
                amount,
            };
            output_coins.push(coin);
        }
    }
    Ok(output_coins)
}

pub fn conditions_dict_for_solution(
    puzzle_reveal: &SerializedProgram,
    solution: &SerializedProgram,
    max_cost: u64,
) -> Result<(HashMap<ConditionOpcode, Vec<ConditionWithArgs>>, u64), Error> {
    match conditions_for_solution(puzzle_reveal, solution, max_cost) {
        Ok((result, cost)) => Ok((conditions_by_opcode(result), cost)),
        Err(error) => Err(error),
    }
}

pub fn conditions_for_solution(
    puzzle_reveal: &SerializedProgram,
    solution: &SerializedProgram,
    max_cost: u64,
) -> Result<(Vec<ConditionWithArgs>, u64), Error> {
    match puzzle_reveal.run_with_cost(max_cost, &solution.to_program()) {
        Ok((cost, r)) => match parse_sexp_to_conditions(r.to_sexp()) {
            Ok(conditions) => Ok((conditions, cost)),
            Err(error) => Err(error),
        },
        Err(error) => Err(error),
    }
}
