use crate::paths;
use crate::encoder;
use crate::git;

use encoder::Encoder;

use std::collections::HashSet;
use std::hash::{Hash, Hasher};

use std::fs;
use std::path::Path;
use std::io::{self, Write, BufRead};
use std::fmt;
use std::cmp::{PartialEq, Ordering};



#[derive(Clone)]
pub struct Item {
    pub id: String,
    pub content: String
}

impl Item {
    pub fn from(s: String, c: String) -> Item {
        Item { id: s, content: c }
    }
    // used only to search in HashSet
    pub fn from_empty(s: String) -> Item {
        Item { id: s, content: String::from("") }
    }
}

impl PartialEq for Item {
    fn eq(&self, other: &Self) -> bool {
        self.id == other.id 
    }
}

impl Eq for Item {}

impl Hash for Item {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.id.hash(state);
    }
}

impl fmt::Display for Item {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        writeln!(f, "{}", self.id)?;
        writeln!(f, "---------")?;
        writeln!(f, "{}", self.content)
    }
}

impl PartialOrd for Item {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        self.id.partial_cmp(&other.id)
    }
}

impl Ord for Item {
    fn cmp(&self, other: &Self) -> Ordering {
        self.id.cmp(&other.id)
    }
}

pub struct Storage {
    items: HashSet::<Item>,
    encoder: Encoder
}

impl Storage {
    pub fn new(passphrase: String) -> Storage {
        Storage {
            items: HashSet::<Item>::new(),
            encoder: Encoder::from(&passphrase)
        }
    }

    pub fn from_db(passphrase: String) -> io::Result<Storage> {
        if !Storage::is_inited()? {
            return Err(io::Error::new(
                io::ErrorKind::Other,
                "Storage is not initialized"
            ));
        }
        let encoder = Encoder::from(&passphrase);
        let file = fs::File::open(paths::get_db_path()?)?;
        let reader = io::BufReader::new(file);
        let mut items = HashSet::<Item>::new();
        let mut id: Option<String> = None;
        let mut lines = reader.lines();
        let passtest = match lines.next() {
            Some(line) => line?,
            None => return Err(
                        io::Error::new(io::ErrorKind::InvalidData,
                        "Bad storage db format: no passphrase in the beginnning"
                    )),
        };
        if !encoder.test_encoded_passphrase(passtest)? {
            return Err(io::Error::new(io::ErrorKind::InvalidData, "Wrong passphrase"));
        }
        for line in lines {
            match line {
                Ok(line) => {
                    if id.is_none() {
                        let line = encoder.decrypt(line)?;
                        id = Some(line);
                    } else {
                        let content = encoder.decrypt(line)?;
                        items.insert(Item::from(id.unwrap(), content));
                        id = None;
                    }
                },
                Err(e) => {
                    eprintln!("Error reading line, {}", e);
                }
            }
        }
        Ok(Storage {
            items,
            encoder
        })
    }

    pub fn init(passphrase: String) -> io::Result<()> {
        Storage::check_installed()?;
        let db_path = paths::get_db_path()?;
        fs::File::create(db_path)?;
        let st = Storage::new(passphrase);
        st.dump()?;
        println!("Storage db created");
        Ok(())
    }

    pub fn check_installed() -> io::Result<()> {
        let sp = paths::get_storage_path()?;
        let storage_path = Path::new(&sp);
        // Check if the folder exists and is a directory
        if !storage_path.exists() || !storage_path.is_dir() {
            return Err(io::Error::new(
                io::ErrorKind::NotFound,
                format!("{} does not exist or not a dir", sp)
            ));
        }
        let git_path = storage_path.join(".git");
        if !git_path.exists() || !git_path.is_dir() {
            return Err(io::Error::new(
                io::ErrorKind::NotFound,
                format!("{} not under git", sp)
            ));
        }
        Ok(())
    }

    pub fn is_inited() -> io::Result<bool> {
        Storage::check_installed()?;
        let db = paths::get_db_path()?;
        let db_path = Path::new(&db);
        Ok(db_path.exists())
    }
    
    pub fn ids(&self) -> Vec<String> {
        let mut result = Vec::new();
        for item in self.items.iter() {
            result.push(item.id.clone());
        }
        result.sort();
        result
    }

    pub fn contains(&self, id: &String) -> bool {
        let item = Item::from_empty(id.clone());
        self.items.contains(&item)
    }

    pub fn get(&self, id: &String) -> &Item {
        let item = Item::from_empty(id.clone());
        self.items.get(&item).unwrap()
    }

    pub fn add(&mut self, item: Item) {
        self.items.insert(item);
    }

    pub fn update(&mut self, item: Item) {
        self.items.remove(&item);
        self.items.insert(item);
    }

    pub fn dump(&self) -> io::Result<()> {
        let mut file = fs::OpenOptions::new()
            .write(true)
            .append(false)
            .open(paths::get_db_path()?)?;
        writeln!(file, "{}", self.encoder.get_encoded_test_passphrase()?)?;

        for item in self.items.iter() {
            writeln!(file, "{}", self.encoder.encrypt(&item.id)?)?;
            let content = self.encoder.encrypt(&item.content)?;
            writeln!(file, "{}", content)?;
        }
        git::Git::sync()?;
        Ok(())
    }
}