sonic 0.6.1

client library for Sonicd, a data streaming gateway
use std::fs::{self, File};
use std::io::{self, Read, Write};
use std::process::Command;
use std::fs::OpenOptions;
use std::str::FromStr;
use std::string::ToString;
use std::env;
use std::path::PathBuf;
use std::collections::BTreeMap;

use serde_json::Value;
use regex::Regex;
use ansi_term::Colour::{Red, Yellow};
use sonic::{SonicMessage, Query};

use super::{ChainErr, Error, Result, ErrorKind};

static DEFAULT_EDITOR: &'static str = "vim";

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ClientConfig {
    pub host: String,
    pub tcp_port: u16,
    pub sources: BTreeMap<String, Value>,
    pub auth: Option<String>,
}

impl ClientConfig {
    pub fn empty() -> ClientConfig {
        ClientConfig {
            host: "0.0.0.0".to_string(),
            tcp_port: 10001,
            sources: BTreeMap::new(),
            auth: None,
        }
    }
}

pub fn parse_token(data: Vec<Value>) -> Result<String> {

    let x = try!(data.into_iter()
        .next()
        .ok_or_else(|| "output is empty".to_owned()));

    let s = try!(x.as_str()
        .ok_or_else(|| "token is not a string".to_owned()));

    Ok(s.to_owned())
}


pub fn write_config(config: &ClientConfig, path: &PathBuf) -> Result<()> {
    debug!("overwriting or creating new configuration file with {:?}",
           config);
    let mut f = try!(OpenOptions::new().truncate(true).create(true).write(true).open(path));

    let encoded = try!(::serde_json::to_string_pretty(config));

    try!(f.write_all(encoded.as_bytes()));

    debug!("write success to config file {:?}", path);

    Ok(())
}

pub fn get_config_path() -> PathBuf {
    let mut sonicrc = env::home_dir().expect("can't find your home folder");
    sonicrc.push(".sonicrc");
    sonicrc
}

pub fn edit_file(path: &PathBuf) -> Result<String> {

    let editor = env::var_os("EDITOR")
        .and_then(|e| e.into_string().ok())
        .unwrap_or_else(|| DEFAULT_EDITOR.to_owned());

    let mut cmd = Command::new(editor);

    let path = try!(path.to_str()
        .ok_or_else(|| format!("error checking for utf8 validity {:?}", &path)));

    try!(cmd.arg(path).status());

    let mut f = try!(OpenOptions::new().read(true).open(path));

    let mut body = String::new();

    try!(f.read_to_string(&mut body));

    Ok(body)
}

pub fn read_file_contents(path: &PathBuf) -> Result<String> {

    let mut file = try!(File::open(&path));
    let mut contents = String::new();

    try!(file.read_to_string(&mut contents));

    return Ok(contents);
}

pub fn read_config(path: &PathBuf) -> Result<ClientConfig> {

    let contents = try!(read_file_contents(&path));

    let config = try!(::serde_json::from_str::<ClientConfig>(&contents.to_string())
        .chain_err(|| format!("config {:?} doesn't seem to be a valid JSON", path)));

    Ok(config)
}


/// Sources .sonicrc user config or creates new and prompts user to edit it
pub fn get_default_config() -> Result<ClientConfig> {

    let path = get_config_path();

    debug!("trying to get configuration in path {:?}", path);

    match fs::metadata(&path) {
        Ok(ref cfg_attr) if cfg_attr.is_file() => {
            debug!("found a file in {:?}", path);
            read_config(&path)
        }
        _ => {
            let mut stdout = io::stdout();
            stdout.write(b"It looks like it's the first time you're using the sonic CLI. Let's configure a few things. Press any key to continue").unwrap();
            try!(stdout.flush());
            let mut input = String::new();
            match io::stdin().read_line(&mut input) {
                Ok(_) => {
                    try!(write_config(&ClientConfig::empty(), &path));
                    let contents = try!(edit_file(&path));
                    let c: ClientConfig = try!(::serde_json::from_str(&contents));
                    println!("successfully saved configuration in $HOME/.sonicrc");
                    Ok(c)
                }
                Err(error) => Err(error.into()),
            }
        }
    }
}


/// Splits strings by character '='
///
/// # Examples
/// ```
/// use libsonic::util::split_key_value;
///
/// let vars = vec!["TABLE=account".to_string(), "DATECUT=2015-09-13".to_string()];
/// let result = split_key_value(&vars).unwrap();
///
/// assert_eq!(result[0].0, "TABLE");
/// assert_eq!(result[0].1, "account");
/// assert_eq!(result[1].0, "DATECUT");
/// assert_eq!(result[1].1, "2015-09-13");
/// ```
///
/// It returns an error if string doesn't contain '='.
///
/// # Failures
/// ```
/// use libsonic::util::split_key_value;
///
/// let vars = vec!["key val".to_string()];
/// split_key_value(&vars);
/// ```
pub fn split_key_value(vars: &Vec<String>) -> Result<Vec<(String, String)>> {
    debug!("parsing variables {:?}", vars);
    let mut m: Vec<(String, String)> = Vec::new();
    for var in vars.into_iter() {
        if var.contains("=") {
            let mut split = var.split("=");
            let left = try!(split.next()
                .ok_or_else(|| ErrorKind::InvalidFormat(var.clone(), "<key>=<value>".to_owned())));
            let right = try!(split.next()
                .ok_or_else(|| ErrorKind::InvalidFormat(var.clone(), "<key>=<value>".to_owned())));
            m.push((left.to_owned(), right.to_owned()));
        } else {
            let err = format!("Cannot split. It should follow format 'key=value'");
            return Err(ErrorKind::InvalidFormat(var.clone(), err).into());
        }
    }
    debug!("Successfully parsed parsed variables {:?} into {:?}",
           vars,
           &m);
    return Ok(m);
}


/// Attempts to inject all variables to the given template:
///
/// # Examples
/// ```
/// use libsonic::util::inject_vars;
///
/// let query = "select count(*) from ${TABLE} where dt > '${DATECUT}' and dt <= \
///     date_sub('${DATECUT}', 30);".to_string();
///
/// let vars = vec![("TABLE".to_string(), "accounts".to_string()),
///     ("DATECUT".to_string(), "2015-01-02".to_string())];
///
/// assert_eq!(inject_vars(&query, &vars).unwrap(),
///     "select count(*) from accounts where dt > '2015-01-02' and dt <= \
///     date_sub('2015-01-02', 30);".to_string());
///
/// ```
///
/// It will return an Error if there is a discrepancy between variables and template
///
/// # Failures
/// ```
/// use libsonic::util::inject_vars;
///
/// let query = "select count(*) from hamburgers".to_string();
/// let vars = vec![("TABLE".to_string(), "accounts".to_string())];
/// inject_vars(&query, &vars);
///
/// let query = "select count(*) from ${TABLE} where ${POTATOES}".to_string();
/// let vars = vec![("TABLE".to_string(), "accounts".to_string())];
/// inject_vars(&query, &vars);
///
/// ```
pub fn inject_vars(template: &str, vars: &Vec<(String, String)>) -> Result<String> {
    debug!("injecting variables {:?} into '{:?}'", vars, template);
    let mut q = String::from_str(template).unwrap();
    for var in vars.iter() {
        let k = "${".to_string() + &var.0 + "}";
        if !q.contains(&k) {
            return Err(ErrorKind::InvalidFormat(k, "not found in template".to_owned()).into());
        } else {
            q = q.replace(&k, &var.1);
        }
    }

    debug!("injected all variables: '{:?}'", &q);

    // check if some variables were left un-injected
    let re = Regex::new(r"(\$\{.*\})").unwrap();
    if re.is_match(&q) {
        Err("Some variables remain uninjected".into())
    } else {
        Ok(q)
    }
}

pub fn build(alias: String,
             mut sources: BTreeMap<String, Value>,
             auth: Option<String>,
             raw_query: String)
             -> Result<SonicMessage> {

    let clean = sources.remove(&alias);

    let source_config = match clean {
        Some(o @ Value::Object(_)) => o,
        None => Value::String(alias),
        _ => {
            return Err(format!("source '{}' config is not an object", &alias).into());
        }
    };

    let query = SonicMessage::QueryMsg(Query::new(raw_query, None, auth, source_config));

    Ok(query)
}

pub fn report_error(e: &Error, show_backtrace: bool) {
    let stderr = io::stderr();
    let mut handle = stderr.lock();
    handle.write(&format!("\n{} {}", Red.paint("error:"), e).as_bytes()).unwrap();

    for e in e.iter().skip(1) {
        handle.write(&format!("\n{} {}", Yellow.paint("caused_by:"), e).as_bytes()).unwrap();
    }

    if show_backtrace {
        handle.write(&format!("\nbacktrace:\n{:?}", e.backtrace()).as_bytes()).unwrap();
    }

    handle.flush().unwrap();
}