use lzma::LzmaWriter as XzEncoder;
use bzip2::write::BzEncoder;
use flate2::write::GzEncoder;
use tar::Builder;
use std::io::{Write, Result};
use std::path::Path;
use std::convert::AsRef;
use std::fs::File;
use std::marker::PhantomData;
use archiver::FileArchiver;
use tarcompressor::HasTarCompressor;
use tarcompressor::TarCompressor;

pub enum TarCompressionLevel {
    None,
    Fast,
    Best,
    Default,
}

struct TarArchiver<W: Write, C: TarCompressor<W>> {
    tar_builder: Builder<C>,
    phantom: PhantomData<W>,
}

impl<C: TarCompressor<W>, W: Write> TarArchiver<W, C> {
    pub fn new(compressor: C) -> Self {
        TarArchiver {
            tar_builder: Builder::new_with_compressor(compressor),
            phantom: PhantomData,
        }
    }
}

impl<T: TarCompressor<File>> FileArchiver for TarArchiver<File, T> {
    fn add_file<P: AsRef<Path>>(&mut self, file_path: P, archive_path: P) -> Result<()> {
        let mut f_in = File::open(file_path)?;
        self.tar_builder.append_file(archive_path, &mut f_in)?;
        Ok(())
    }

    fn 
<P: AsRef<Path>>(&mut self, dir_path: P, archive_dir_path: P) -> Result<()> {
        self.tar_builder.append_dir(archive_dir_path, dir_path)?;
        Ok(())
    }

    fn finish(&mut self) -> Result<()> {
        self.tar_builder.finish()?;
        Ok(())
    }
}

macro_rules! make_archiver {
    ($name:ident, $encoder:ident, #[$doc:meta]) => {
        #[$doc]
        pub struct $name<W: Write> {
            inner: TarArchiver<W, $encoder<W>>,
        }

        impl FileArchiver for $name<File> {
            type CompressionEnum = TarCompressionLevel;

            fn create<P: AsRef<Path>>(output_path: P) -> Result<Self> {
                Ok(
                    $name {
                        inner: TarArchiver::new($encoder::new_with_compression_level(TarCompressionLevel::Default, output_path)?),
                    }
                  )
            }

            fn create_with_compression_level<P: AsRef<Path>>(output_path: P, compression_level: TarCompressionLevel) -> Result<Self> {
                Ok(
                    $name {
                        inner: TarArchiver::new($encoder::new_with_compression_level(compression_level, output_path)?),
                    }
                  )
            }

            fn add_file<P: AsRef<Path>>(&mut self, file: P, archive_path: P) -> Result<()> {
                self.inner.add_file(file, archive_path)?;
                Ok(())
            }

            fn add_directory<P: AsRef<Path>>(&mut self, dir_path: P, archive_dir_path: P) -> Result<()> {
                self.inner.add_directory(dir_path, archive_dir_path)?;
                Ok(())
            }

            fn finish(&mut self) -> Result<()> {
                self.inner.finish()?;
                Ok(())
            }
        }
    }
}

make_archiver!(BzArchiver, BzEncoder,
               /// Tar archiver using bzip2
              );
make_archiver!(GzArchiver, GzEncoder,
               /// Tar archiver using gzip
              );
make_archiver!(XzArchiver, XzEncoder,
               /// Tar archiver using xz (lzma)
              );

add_directory