// Rshare - a file sharing program written in Rust.
// Copyright (C) 2017 Cooper Paul EdenDay
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
//
// Electronic mail: cedenday@protonmail.com
// Paper mail: 100 Front Street, #31, Bath, Maine, 04530, USA

use std;
use metadata;
use ring;
use untrusted;
use messaging;
use rustyline;
use shlex;
use std::sync::Arc;
use chashmap::CHashMap;
use messaging::itc;

const GPL_INTERACTIVE: &'static str = "\
Rshare Copyright (C) 2017 Cooper Paul EdenDay.\n\
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n\
This is free software, and you are welcome to redistribute it\n\
under certain conditions; type `show c' for details.";

const GPL_W: &'static str = "\
This program is distributed in the hope that it will be useful,\n\
but WITHOUT ANY WARRANTY; without even the implied warranty of\n\
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n\
GNU General Public License for more details.";

const GPL_C: &'static str = "\
This program is free software: you can redistribute it and/or modify\n\
it under the terms of the GNU General Public License as published by\n\
the Free Software Foundation, either version 3 of the License, or\n\
(at your option) any later version.";

pub fn client(rng: &ring::rand::SystemRandom,
              current_keys: Arc<CHashMap<Vec<u8>, itc::Key>>,
              quiet: bool,
              no_servers: bool) {
    if !quiet {
        println!("{}", GPL_INTERACTIVE);
        println!("Type 'help' for assistance.");
        if no_servers {
            println!("Warning: starting client with no servers.");
        }
    }

    let mut rl = rustyline::Editor::<()>::new();

    loop {
        match rl.readline("> ") {
            Ok(line) => {
                rl.add_history_entry(&line);

                let lex_input = line.trim();
                if let Some(lex_output) = shlex::split(lex_input) {
                    if let Some(command_base) = lex_output.get(0) {
                        let command_parts = lex_output[1..].to_vec();

                        match command_base.as_str() {
                            "help" => help(&command_parts),
                            "create" => create(rng, &command_parts, quiet),
                            "download" => download(current_keys.clone(), &command_parts),
                            "stop" => stop(current_keys.clone(), &command_parts),
                            "delete" => delete(&command_parts),
                            "list" => list(),
                            "links" => links(&command_parts),
                            "status" => status(&command_parts, current_keys.clone()),
                            "show" => {
                                if let Some(word) = command_parts.iter().next() {
                                    match word.as_str() {
                                        "w" => println!("{}", GPL_W),
                                        "c" => println!("{}", GPL_C),
                                        _ => println!("Unknown item '{}'.", word),
                                    }
                                } else {
                                    println!("Show what?");
                                }
                            }
                            _ => println!("Unknown command '{}'.", command_base),
                        }
                    } else {
                        println!("Could not parse command base from '{}'.", lex_input);
                    }
                } else {
                    println!("Failed to lex command '{}'.", lex_input);
                }
            }
            Err(rustyline::error::ReadlineError::Interrupted) => {
                break;
            }
            _ => {}
        }
    }
}

fn help(words: &[String]) {
    if let Some(command) = words.iter().next() {
        match command.as_str() {
            "create" => {
                println!("create <FILE PATH>:\n    \
                Create a <LINK> to the data of <FILE_PATH>.")
            }
            "download" => {
                println!("download <TARGET FILE PATH> <LINK>:\n    \
                Download the data associated with <LINK> to <TARGET FILE PATH>.")
            }
            "stop" => {
                println!("stop <LINK>:\n    \
                Stop the download of the data associated with <LINK>.\n    \
                The download can be resumed with the 'download' command.");
            }
            "delete" => {
                println!("delete <LINK>:\n    \
                Stop sharing the data associated with <LINK>.");
            }
            "list" => {
                println!("list:\n   \
                List the shared files.");
            }
            "links" => {
                println!("links <FILE PATH>:\n    \
                View the link(s) to <FILE PATH>.");
            }
            "status" => {
                println!("status <LINK>:\n    \
                View number of parts on disk associated with <LINK>.")
            }
            "show" => {
                println!("show <ITEM>:\n    \
                View the text associated with <ITEM>.")
            }
            _ => println!("Unknown command '{}'.", command),
        }
    } else {
        println!("\
        Type help <COMMAND> for help with a command.\n\
        List of <COMMAND>s:\n\
        create\n\
        download\n\
        stop\n\
        delete\n\
        list\n\
        links\n\
        status\n\
        show\
        ");
    }
}

fn create(rng: &ring::rand::SystemRandom, words: &[String], quiet: bool) {
    if let Some(file_path) = words.iter().next() {
        if let Ok(file_path) = std::path::Path::new(file_path).canonicalize() {
            if file_path.is_file() {
                let file_path = String::from(file_path.to_str().unwrap());

                let num_parts = {
                    let file = std::fs::File::open(&file_path).unwrap();
                    let size = file.metadata().unwrap().len();
                    (size - 1) / metadata::PART_SIZE + 1
                };

                let key = {
                    let bytes = ring::signature::Ed25519KeyPair::generate_pkcs8(rng)
                        .expect("Couldn't generate key pair.");
                    ring::signature::Ed25519KeyPair::from_pkcs8(untrusted::Input::from(&bytes))
                        .unwrap()
                };

                let nonce = {
                    use ring::rand::SecureRandom;
                    let mut nonce = [0u8; 8];
                    rng.fill(&mut nonce).expect("Couldn't generate nonce.");
                    nonce
                };

                let link = metadata::Link {
                    num_parts: num_parts,
                    public_key: key.public_key_bytes().to_vec(),
                    nonce: nonce.to_vec(),
                };
                let encoded = metadata::Link::encode(&link);
                println!("{}", encoded);

                if !quiet {
                    println!("Warning: privacy can't be provided if the adversary has the link.");
                }

                std::thread::spawn(move || {
                    use rayon::prelude::*;
                    let part_numbers: Vec<u64> = (0..num_parts).collect();
                    let parts: Vec<(Vec<u8>, Vec<u8>, u64)> = part_numbers
                        .par_iter()
                        .map(|part_number| {
                            let part_number = *part_number;
                            let part = metadata::read_part(&file_path, part_number).unwrap();

                            let hash = metadata::hash(&part, &nonce, part_number);

                            let signature = key.sign(&hash).as_ref().to_vec();

                            (hash, signature, part_number)
                        })
                        .collect();

                    let connection = metadata::connect();

                    connection
                        .execute("INSERT INTO metadata (num_parts, file_path, key, nonce)\
                        VALUES ($1, $2, $3, $4)",
                                 &[&(num_parts as i64),
                                   &file_path,
                                   &key.public_key_bytes(),
                                   &nonce.to_vec()])
                        .unwrap();

                    let metadata_id = connection.last_insert_rowid();

                    for part in parts {
                        connection
                            .execute("INSERT INTO parts (\
                            hash,\
                            signature,\
                            part_number,\
                            metadata_id)\
                            VALUES ($1, $2, $3, $4)",
                                     &[&part.0, &part.1, &(part.2 as i64), &metadata_id])
                            .unwrap();
                    }
                });
            } else {
                println!("Path needs to point to a regular file.");
            }
        } else {
            println!("Path '{}' is invalid.", file_path);
        }
    } else {
        println!("File path not found in command. Did you forget to enter it?");
    }
}

fn download(current_keys: Arc<CHashMap<Vec<u8>, itc::Key>>, words: &[String]) {
    let mut words_iter = words.iter();
    if let Some(file_path) = words_iter.next() {
        if std::path::Path::new(file_path).parent().unwrap().exists() {
            if let Some(encoded_link) = words_iter.next() {
                let link = metadata::Link::decode(encoded_link);
                match link {
                    Ok(link) => {
                        let key = messaging::itc::Key {
                            nonce: link.nonce,
                            num_parts: link.num_parts,
                            file_path: file_path.to_string(),
                        };

                        current_keys.insert(link.public_key.clone(), key);
                    }
                    Err(e) => println!("{}", e),
                }
            } else {
                println!("Link not found in command. Did you forget to enter the link?");
            }
        } else {
            println!("Directory that contains '{}' doesn't exist.", file_path);
        }
    } else {
        println!("File path not found in command. Did you forget to enter the file path?");
    }
}

fn stop(current_keys: Arc<CHashMap<Vec<u8>, itc::Key>>, words: &[String]) {
    if let Some(encoded_link) = words.iter().next() {
        let link = metadata::Link::decode(encoded_link);
        match link {
            Ok(link) => {
                current_keys.remove(&link.public_key);
            }
            Err(e) => println!("{}", e),
        }
    } else {
        println!("Link not found in command. Did you forget to enter the link?");
    }
}

fn delete(words: &[String]) {
    if let Some(encoded_link) = words.iter().next() {
        let link = metadata::Link::decode(encoded_link);
        match link {
            Ok(link) => {
                let connection = metadata::connect();

                let metadata_id = connection
                    .prepare("SELECT id FROM metadata WHERE key = ?")
                    .unwrap()
                    .query_row(&[&link.public_key], |row| {
                        let id: i64 = row.get(0);
                        id
                    });

                if let Ok(metadata_id) = metadata_id {
                    connection
                        .execute("DELETE FROM parts WHERE \
                        metadata_id = ?",
                                 &[&metadata_id])
                        .unwrap();

                    connection
                        .execute("DELETE FROM metadata \
                        WHERE id = ?",
                                 &[&metadata_id])
                        .unwrap();
                } else {
                    println!("Not sharing link '{}'.", encoded_link);
                }
            }
            Err(e) => println!("{}", e),
        }
    } else {
        println!("Link not found in command. Did you forget to enter the link?");
    }
}

fn list() {
    let connection = metadata::connect();

    let mut query = connection
        .prepare("SELECT DISTINCT file_path FROM metadata")
        .unwrap();

    let file_paths = query
        .query_map(&[], |row| {
            let file_path: String = row.get(0);
            file_path
        })
        .unwrap();

    for file_path in file_paths {
        let file_path = file_path.unwrap();

        println!("{}", file_path);
    }
}

fn links(words: &[String]) {
    if let Some(file_path) = words.iter().next() {
        let connection = metadata::connect();

        let mut query = connection
            .prepare("SELECT num_parts, key, nonce FROM metadata WHERE file_path = ?")
            .unwrap();

        let rows = query
            .query_map(&[&file_path.as_str()], |row| {
                let num_parts: i64 = row.get(0);
                metadata::Link {
                    num_parts: num_parts as u64,
                    public_key: row.get(1),
                    nonce: row.get(2),
                }
            })
            .unwrap();

        let mut peekable = rows.peekable();

        if peekable.peek().is_some() {
            for link in peekable {
                let link = link.unwrap();
                let encoded = metadata::Link::encode(&link);
                println!("{}", encoded);
            }
        } else {
            println!("No links to file '{}' have been created.", file_path);
        }
    } else {
        println!("File path not found in command. Did you forget to enter it?");
    }
}

fn status(words: &[String], current_keys: Arc<CHashMap<Vec<u8>, itc::Key>>) {
    if let Some(encoded_link) = words.iter().next() {
        let link = metadata::Link::decode(encoded_link);
        match link {
            Ok(link) => {
                let connection = metadata::connect();

                let row = connection
                    .prepare("SELECT id, num_parts FROM metadata WHERE key = ?")
                    .unwrap()
                    .query_row(&[&link.public_key], |row| {
                        let output: (i64, i64) = (row.get(0), row.get(1));
                        output
                    });

                let (num_parts_total, num_parts_disk) = if let Ok(row) = row {
                    let (metadata_id, num_parts_total) = row;

                    let num_parts_disk = connection
                        .prepare("SELECT COUNT(*) FROM parts WHERE metadata_id = ?")
                        .unwrap()
                        .query_row(&[&metadata_id], |row| row.get(0))
                        .unwrap();

                    (num_parts_total.to_string(), num_parts_disk)
                } else {
                    let mut num_parts_total = String::from("?");
                    for key in current_keys.as_ref().clone() {
                        let key: itc::Key = key.1;
                        if key.nonce == link.nonce {
                            num_parts_total = key.num_parts.to_string();
                            break;
                        }
                    }
                    (num_parts_total, 0)
                };

                println!("{}/{}", num_parts_disk, num_parts_total);
            }
            Err(e) => println!("{}", e),
        }
    } else {
        println!("Link not found in command. Did you forget to enter the link?");
    }
}
