//! A wrapper around an object that ties it to a physical file

extern crate serde;

use anyhow::Context;
use serde::de::DeserializeOwned;
use serde::Serialize;
use std::fs::File;
use std::io::prelude::*;
use std::path::{Path, PathBuf};
use thiserror::Error;

#[derive(Error, Debug)]
pub enum Error {
    #[error(transparent)]
    Serialization(serde_json::Error),
    #[error(transparent)]
    IO(std::io::Error),
    #[error(transparent)]
    Other(#[from] anyhow::Error),
}

/// A wrapper around an object `T` that ties the object to a physical file
#[derive(Debug, PartialEq)]
pub struct FileLinked<T>
where
    T: Serialize,
{
    val: T,
    path: PathBuf,
}

impl<T> FileLinked<T>
where
    T: Serialize,
{
    /// Returns a readonly reference of `T`
    ///
    /// # Examples
    /// ```
    /// # use file_linked::*;
    /// # use serde::{Deserialize, Serialize};
    /// # use std::fmt;
    /// # use std::string::ToString;
    /// # use std::path::PathBuf;
    /// #
    /// # #[derive(Deserialize, Serialize)]
    /// # struct Test {
    /// #     pub a: u32,
    /// #     pub b: String,
    /// #     pub c: f64
    /// # }
    /// #
    /// # fn main() {
    /// let test = Test {
    ///     a: 1,
    ///     b: String::from("two"),
    ///     c: 3.0
    /// };
    ///
    /// let linked_test = FileLinked::new(test, &PathBuf::from("./temp"))
    ///     .expect("Unable to create file linked object");
    ///
    /// assert_eq!(linked_test.readonly().a, 1);
    /// assert_eq!(linked_test.readonly().b, String::from("two"));
    /// assert_eq!(linked_test.readonly().c, 3.0);
    /// #
    /// # std::fs::remove_file("./temp").expect("Unable to remove file");
    /// # }
    /// ```
    pub fn readonly(&self) -> &T {
        &self.val
    }

    /// Creates a new [`FileLinked`] object of type `T` stored to the file given by `path`.
    ///
    /// # Examples
    /// ```
    /// # use file_linked::*;
    /// # use serde::{Deserialize, Serialize};
    /// # use std::fmt;
    /// # use std::string::ToString;
    /// # use std::path::PathBuf;
    /// #
    /// #[derive(Deserialize, Serialize)]
    /// struct Test {
    ///     pub a: u32,
    ///     pub b: String,
    ///     pub c: f64
    /// }
    ///
    /// # fn main() {
    /// let test = Test {
    ///     a: 1,
    ///     b: String::from("two"),
    ///     c: 3.0
    /// };
    ///
    /// let linked_test = FileLinked::new(test, &PathBuf::from("./temp"))
    ///     .expect("Unable to create file linked object");
    ///
    /// assert_eq!(linked_test.readonly().a, 1);
    /// assert_eq!(linked_test.readonly().b, String::from("two"));
    /// assert_eq!(linked_test.readonly().c, 3.0);
    /// #
    /// # std::fs::remove_file("./temp").expect("Unable to remove file");
    /// # }
    /// ```
    pub fn new(val: T, path: &Path) -> Result<FileLinked<T>, Error> {
        let result = FileLinked {
            val,
            path: path.to_path_buf(),
        };
        result.write_data()?;
        Ok(result)
    }

    fn write_data(&self) -> Result<(), Error> {
        let mut file = File::create(&self.path)
            .with_context(|| format!("Unable to open path {}", self.path.display()))?;

        write!(
            file,
            "{}",
            serde_json::to_string(&self.val).map_err(Error::Serialization)?
        )
        .with_context(|| format!("Unable to write to file {}", self.path.display()))?;

        Ok(())
    }

    /// Modifies the data contained in a `FileLinked` object using a callback `op` that has a mutable reference to the
    /// underlying data. After the mutable operation is performed the data is written to a file to synchronize the state.
    ///
    /// # Examples
    /// ```
    /// # use file_linked::*;
    /// # use serde::{Deserialize, Serialize};
    /// # use std::fmt;
    /// # use std::string::ToString;
    /// # use std::path::PathBuf;
    /// #
    /// # #[derive(Deserialize, Serialize)]
    /// # struct Test {
    /// #     pub a: u32,
    /// #     pub b: String,
    /// #     pub c: f64
    /// # }
    /// #
    /// # fn main() -> Result<(), Error> {
    /// let test = Test {
    ///     a: 1,
    ///     b: String::from(""),
    ///     c: 0.0
    /// };
    ///
    /// let mut linked_test = FileLinked::new(test, &PathBuf::from("./temp"))
    ///     .expect("Unable to create file linked object");
    ///
    /// assert_eq!(linked_test.readonly().a, 1);
    ///
    /// linked_test.mutate(|t| t.a = 2)?;
    ///
    /// assert_eq!(linked_test.readonly().a, 2);
    /// #
    /// # std::fs::remove_file("./temp").expect("Unable to remove file");
    /// #
    /// # Ok(())
    /// # }
    /// ```
    pub fn mutate<U, F: FnOnce(&mut T) -> U>(&mut self, op: F) -> Result<U, Error> {
        let result = op(&mut self.val);

        self.write_data()?;

        Ok(result)
    }

    /// Replaces the value held by the `FileLinked` object with `val`. After replacing the object will be written to a file.
    ///
    /// # Examples
    /// ```
    /// # use file_linked::*;
    /// # use serde::{Deserialize, Serialize};
    /// # use std::fmt;
    /// # use std::string::ToString;
    /// # use std::path::PathBuf;
    /// #
    /// # #[derive(Deserialize, Serialize)]
    /// # struct Test {
    /// #     pub a: u32,
    /// #     pub b: String,
    /// #     pub c: f64
    /// # }
    /// #
    /// # fn main() -> Result<(), Error> {
    /// let test = Test {
    ///     a: 1,
    ///     b: String::from(""),
    ///     c: 0.0
    /// };
    ///
    /// let mut linked_test = FileLinked::new(test, &PathBuf::from("./temp"))
    ///     .expect("Unable to create file linked object");
    ///
    /// assert_eq!(linked_test.readonly().a, 1);
    ///
    /// linked_test.replace(Test {
    ///     a: 2,
    ///     b: String::from(""),
    ///     c: 0.0
    /// })?;
    ///
    /// assert_eq!(linked_test.readonly().a, 2);
    /// #
    /// # std::fs::remove_file("./temp").expect("Unable to remove file");
    /// #
    /// # Ok(())
    /// # }
    /// ```
    pub fn replace(&mut self, val: T) -> Result<(), Error> {
        self.val = val;

        self.write_data()
    }
}

impl<T> FileLinked<T>
where
    T: Serialize + DeserializeOwned,
{
    /// Deserializes an object `T` from the file given by `path`
    ///
    /// # Examples
    /// ```
    /// # use file_linked::*;
    /// # use serde::{Deserialize, Serialize};
    /// # use std::fmt;
    /// # use std::string::ToString;
    /// # use std::fs;
    /// # use std::fs::OpenOptions;
    /// # use std::io::Write;
    /// # use std::path::PathBuf;
    /// #
    /// # #[derive(Deserialize, Serialize)]
    /// # struct Test {
    /// #     pub a: u32,
    /// #     pub b: String,
    /// #     pub c: f64
    /// # }
    /// #
    /// # fn main() -> Result<(), Error> {
    /// let test = Test {
    ///     a: 1,
    ///     b: String::from("2"),
    ///     c: 3.0
    /// };
    ///
    /// let path = PathBuf::from("./temp");
    ///
    /// let mut file = OpenOptions::new()
    ///        .write(true)
    ///        .create(true)
    ///        .open(&path)
    ///        .expect("Unable to create file");
    ///
    /// write!(file, "{}", serde_json::to_string(&test)
    ///     .expect("Unable to serialize object"))
    ///     .expect("Unable to write file");
    ///
    /// drop(file);
    ///
    /// let mut linked_test = FileLinked::<Test>::from_file(&path)
    ///     .expect("Unable to create file linked object");
    ///
    /// assert_eq!(linked_test.readonly().a, test.a);
    /// assert_eq!(linked_test.readonly().b, test.b);
    /// assert_eq!(linked_test.readonly().c, test.c);
    /// #
    /// # std::fs::remove_file("./temp").expect("Unable to remove file");
    /// #
    /// # Ok(())
    /// # }
    /// ```
    pub fn from_file(path: &Path) -> Result<FileLinked<T>, Error> {
        let file =
            File::open(path).with_context(|| format!("Unable to open file {}", path.display()))?;

        let val = serde_json::from_reader(file)
            .with_context(|| String::from("Unable to parse value from file."))?;

        Ok(FileLinked {
            val,
            path: path.to_path_buf(),
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs;

    #[test]
    fn test_mutate() -> Result<(), Error> {
        let list = vec![1, 2, 3, 4];
        let mut file_linked_list = FileLinked::new(list, &PathBuf::from("test.txt"))?;

        assert_eq!(format!("{:?}", file_linked_list.readonly()), "[1, 2, 3, 4]");

        file_linked_list.mutate(|v1| v1.push(5))?;

        assert_eq!(
            format!("{:?}", file_linked_list.readonly()),
            "[1, 2, 3, 4, 5]"
        );

        file_linked_list.mutate(|v1| v1[1] = 1)?;

        assert_eq!(
            format!("{:?}", file_linked_list.readonly()),
            "[1, 1, 3, 4, 5]"
        );

        fs::remove_file("test.txt").expect("Unable to remove file");

        Ok(())
    }
}
