// Copyright 2020 Lucjan Suski
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//`

use crate::{ffi, ffi_util::to_cpath, Error, Options};

use libc::{self, c_char, size_t};
use std::{ffi::CString, marker::PhantomData, path::Path};

/// SstFileWriter is used to create sst files that can be added to database later
/// All keys in files generated by SstFileWriter will have sequence number = 0.
pub struct SstFileWriter<'a> {
    pub(crate) inner: *mut ffi::rocksdb_sstfilewriter_t,
    // Options are needed to be alive when calling open(),
    // so let's make sure it doesn't get, dropped for the lifetime of SstFileWriter
    phantom: PhantomData<&'a Options>,
}

unsafe impl Send for SstFileWriter<'_> {}
unsafe impl Sync for SstFileWriter<'_> {}

struct EnvOptions {
    inner: *mut ffi::rocksdb_envoptions_t,
}

impl Drop for EnvOptions {
    fn drop(&mut self) {
        unsafe {
            ffi::rocksdb_envoptions_destroy(self.inner);
        }
    }
}

impl Default for EnvOptions {
    fn default() -> Self {
        let opts = unsafe { ffi::rocksdb_envoptions_create() };
        Self { inner: opts }
    }
}

impl<'a> SstFileWriter<'a> {
    /// Initializes SstFileWriter with given DB options.
    pub fn create(opts: &'a Options) -> Self {
        let env_options = EnvOptions::default();

        let writer = Self::create_raw(opts, &env_options);

        Self {
            inner: writer,
            phantom: PhantomData,
        }
    }

    fn create_raw(opts: &Options, env_opts: &EnvOptions) -> *mut ffi::rocksdb_sstfilewriter_t {
        unsafe { ffi::rocksdb_sstfilewriter_create(env_opts.inner, opts.inner) }
    }

    /// Prepare SstFileWriter to write into file located at "file_path".
    pub fn open<P: AsRef<Path>>(&'a self, path: P) -> Result<(), Error> {
        let cpath = to_cpath(&path)?;
        self.open_raw(&cpath)
    }

    fn open_raw(&'a self, cpath: &CString) -> Result<(), Error> {
        unsafe {
            ffi_try!(ffi::rocksdb_sstfilewriter_open(
                self.inner,
                cpath.as_ptr() as *const _
            ));

            Ok(())
        }
    }

    /// Finalize writing to sst file and close file.
    pub fn finish(&mut self) -> Result<(), Error> {
        unsafe {
            ffi_try!(ffi::rocksdb_sstfilewriter_finish(self.inner,));
            Ok(())
        }
    }

    /// returns the current file size
    pub fn file_size(&self) -> u64 {
        let mut file_size: u64 = 0;
        unsafe {
            ffi::rocksdb_sstfilewriter_file_size(self.inner, &mut file_size);
        }
        file_size
    }

    /// Adds a Put key with value to currently opened file
    /// REQUIRES: key is after any previously added key according to comparator.
    pub fn put<K, V>(&mut self, key: K, value: V) -> Result<(), Error>
    where
        K: AsRef<[u8]>,
        V: AsRef<[u8]>,
    {
        let key = key.as_ref();
        let value = value.as_ref();
        unsafe {
            ffi_try!(ffi::rocksdb_sstfilewriter_put(
                self.inner,
                key.as_ptr() as *const c_char,
                key.len() as size_t,
                value.as_ptr() as *const c_char,
                value.len() as size_t,
            ));
            Ok(())
        }
    }

    /// Adds a Put key with value to currently opened file
    /// REQUIRES: key is after any previously added key according to comparator.
    pub fn put_with_ts<K, V, S>(&mut self, key: K, ts: S, value: V) -> Result<(), Error>
    where
        K: AsRef<[u8]>,
        V: AsRef<[u8]>,
        S: AsRef<[u8]>,
    {
        let key = key.as_ref();
        let value = value.as_ref();
        let ts = ts.as_ref();
        unsafe {
            ffi_try!(ffi::rocksdb_sstfilewriter_put_with_ts(
                self.inner,
                key.as_ptr() as *const c_char,
                key.len() as size_t,
                ts.as_ptr() as *const c_char,
                ts.len() as size_t,
                value.as_ptr() as *const c_char,
                value.len() as size_t,
            ));
            Ok(())
        }
    }

    /// Adds a Merge key with value to currently opened file
    /// REQUIRES: key is after any previously added key according to comparator.
    pub fn merge<K, V>(&mut self, key: K, value: V) -> Result<(), Error>
    where
        K: AsRef<[u8]>,
        V: AsRef<[u8]>,
    {
        let key = key.as_ref();
        let value = value.as_ref();

        unsafe {
            ffi_try!(ffi::rocksdb_sstfilewriter_merge(
                self.inner,
                key.as_ptr() as *const c_char,
                key.len() as size_t,
                value.as_ptr() as *const c_char,
                value.len() as size_t,
            ));
            Ok(())
        }
    }

    /// Adds a deletion key to currently opened file
    /// REQUIRES: key is after any previously added key according to comparator.
    pub fn delete<K: AsRef<[u8]>>(&mut self, key: K) -> Result<(), Error> {
        let key = key.as_ref();

        unsafe {
            ffi_try!(ffi::rocksdb_sstfilewriter_delete(
                self.inner,
                key.as_ptr() as *const c_char,
                key.len() as size_t,
            ));
            Ok(())
        }
    }

    /// Adds a deletion key to currently opened file
    /// REQUIRES: key is after any previously added key according to comparator.
    pub fn delete_with_ts<K: AsRef<[u8]>, S: AsRef<[u8]>>(
        &mut self,
        key: K,
        ts: S,
    ) -> Result<(), Error> {
        let key = key.as_ref();
        let ts = ts.as_ref();
        unsafe {
            ffi_try!(ffi::rocksdb_sstfilewriter_delete_with_ts(
                self.inner,
                key.as_ptr() as *const c_char,
                key.len() as size_t,
                ts.as_ptr() as *const c_char,
                ts.len() as size_t,
            ));
            Ok(())
        }
    }
}

impl Drop for SstFileWriter<'_> {
    fn drop(&mut self) {
        unsafe {
            ffi::rocksdb_sstfilewriter_destroy(self.inner);
        }
    }
}
