use vfile::decompress::decompress;
use vfile::decompress::decompress::DecompressionFormat;
use vfile::decompress::zip::{is_zip, new_reader_for_single_file_zip_archive};
use vfile::transport::seekable::seekable_factory::SeekableTransportFactory;
use vfile::transport::streaming::previewer_factory::PreviewerTransportFactory;
use vfile::transport::streaming::streaming_transport::StreamingTransport;
use vfile::util::buf::previewer::BufPreviewer;
use vfile::util::location::Location;

use crate::chopper::error::{Error, Result};
use crate::chopper::types::TimestampRange;
use crate::input::defaults::{
    create_default_seekable_transports, create_default_streaming_transports,
};
use crate::input::input::InputFormat;
use crate::source::source::Source;
use crate::source::source_factory::SourceFactory;

#[derive(Clone, Debug)]
enum FormatAutodetectResult {
    Detected,
    NotDetected,
}

#[derive(Clone, Debug)]
enum Format {
    UserSpecified(String),
    DetectUsingFileNameThenContents(String),
    DetectUsingFileContents,
}

pub type ChopperLocationInputFactory = LocationInputFactory<Box<dyn Source>, ()>;

/// handles case when there is single reader for the input, i.e. not a bunch of files in a dir;
/// contrast with 'dir' module
pub struct LocationInputFactory<S, EA> {
    seekable_transport_factory: SeekableTransportFactory,
    previewer_transport_factory: PreviewerTransportFactory,
    source_factories: Vec<Box<dyn SourceFactory<S, EA>>>,
}

impl<S, EA> LocationInputFactory<S, EA> {
    pub fn new(
        source_factories: Vec<Box<dyn SourceFactory<S, EA>>>,
        user_streaming_transports: Option<Vec<Box<dyn StreamingTransport>>>,
    ) -> LocationInputFactory<S, EA> {
        // streaming transports and the previewer factory for them
        let mut default_streaming_transports = create_default_streaming_transports();
        let streaming_transports: Vec<Box<dyn StreamingTransport>> = match user_streaming_transports
        {
            Some(mut t) => {
                t.append(&mut default_streaming_transports);
                t
            }
            None => default_streaming_transports,
        };
        let previewer_transport_factory = PreviewerTransportFactory::new(streaming_transports);

        // seekable transports
        let seekable_transports = create_default_seekable_transports();
        let seekable_transport_factory = SeekableTransportFactory::new(seekable_transports);

        LocationInputFactory {
            seekable_transport_factory,
            previewer_transport_factory,
            source_factories,
        }
    }

    pub fn create_previewer(
        &self,
        loc: &Location,
        _range: &TimestampRange,
    ) -> Result<BufPreviewer> {
        let previewer = self.previewer_transport_factory.create_previewer(loc)?;
        Ok(previewer.ok_or_else(|| {
            std::io::Error::new(
                std::io::ErrorKind::NotFound,
                format!("no transport for {} could be found", loc),
            )
        })?)
    }

    pub fn create_source(
        &self,
        loc: &Location,
        _range: &TimestampRange,
        input_format: &InputFormat,
        extra_args: EA,
    ) -> Result<Option<S>> {
        // see if this needs a seekable transport
        let seekable = self.seekable_transport_factory.create_seekable(loc)?;
        if let Some(mut seekable) = seekable {
            // it's rare to need seekable, so we are going to support .zip files as special case here
            // with no user extensibility at least for now
            if is_zip(&mut seekable)? {
                let (reader, file_name) = new_reader_for_single_file_zip_archive(seekable)?;
                let previewer = BufPreviewer::new(reader)?;
                return Ok(Some(self.create_source_from_previewer(
                    previewer,
                    Some(file_name),
                    input_format,
                    extra_args,
                )?));
            }
        }

        // try the non-seekable streaming transports
        let previewer = self.previewer_transport_factory.create_previewer(loc)?;
        if let Some(previewer) = previewer {
            return Ok(Some(self.create_source_from_previewer(
                previewer,
                loc.file_name(),
                input_format,
                extra_args,
            )?));
        }

        Ok(None)
    }

    pub fn create_source_from_previewer(
        &self,
        previewer: BufPreviewer,
        file_name: Option<String>,
        input_format: &InputFormat,
        extra_args: EA,
    ) -> Result<S> {
        let format = match input_format {
            InputFormat::Extension(extension) => {
                let extension = if extension.starts_with('.') {
                    extension.to_owned()
                } else {
                    ".".to_owned() + extension
                };
                Format::UserSpecified(extension)
            }
            InputFormat::Auto => match file_name {
                None => Format::DetectUsingFileContents,
                Some(file_name) => Format::DetectUsingFileNameThenContents(file_name),
            },
        };

        match format {
            Format::UserSpecified(format) => {
                // user told us exactly what they want, don't do any autodetection
                let (_, previewer, format) = Self::decompress_using_format(previewer, format)?;
                self.create_source_from_format(previewer, format, extra_args)
            }
            Format::DetectUsingFileNameThenContents(format) => {
                // first try using the file name alone

                let (decompression_result, previewer, format) =
                    Self::decompress_using_format(previewer, format)?;

                // can theoretically somehow share this code with create_source_from_reader_and_format,
                // but seems hard due to ownership of reader needed later in this match block;
                // maybe revisit one day as learning experience
                for sf in &self.source_factories {
                    if sf.can_create_from_format(&format) {
                        return sf.create_source(previewer, extra_args);
                    }
                }

                // if we got here, we failed to find source factory that can handle the file name
                // try to find one using contents of the file

                // first, check if we were able to decompress above, if so, don't need to
                // decompress again
                let previewer = match decompression_result {
                    FormatAutodetectResult::Detected => previewer,
                    FormatAutodetectResult::NotDetected => {
                        let (_, previewer) = Self::decompress_by_autodetecting_format(previewer)?;
                        previewer
                    }
                };

                self.create_source_by_autodetecting_format(previewer, extra_args)
            }
            Format::DetectUsingFileContents => {
                // we didn't even get a file name as hint, try to figure out using the
                // contents of the file right away
                let (_, previewer) = Self::decompress_by_autodetecting_format(previewer)?;
                self.create_source_by_autodetecting_format(previewer, extra_args)
            }
        }
    }

    fn decompress_using_format(
        previewer: BufPreviewer,
        format: String,
    ) -> Result<(FormatAutodetectResult, BufPreviewer, String)> {
        match decompress::is_compressed_using_format(&format) {
            Some((decompression_format, new_format)) => {
                let new_previewer = Self::decompress(decompression_format, previewer)?;
                Ok((FormatAutodetectResult::Detected, new_previewer, new_format))
            }
            None => Ok((FormatAutodetectResult::NotDetected, previewer, format)),
        }
    }

    fn decompress_by_autodetecting_format(
        previewer: BufPreviewer,
    ) -> Result<(FormatAutodetectResult, BufPreviewer)> {
        match decompress::is_compressed_using_previewer(&previewer) {
            Some(decompression_format) => {
                let new_previewer = Self::decompress(decompression_format, previewer)?;
                Ok((FormatAutodetectResult::Detected, new_previewer))
            }
            None => Ok((FormatAutodetectResult::NotDetected, previewer)),
        }
    }

    fn decompress(
        decompression_format: DecompressionFormat,
        previewer: BufPreviewer,
    ) -> Result<BufPreviewer> {
        let new_reader = decompress::decompress(decompression_format, previewer)?;
        Ok(BufPreviewer::new(new_reader)?)
    }

    fn create_source_from_format(
        &self,
        previewer: BufPreviewer,
        format: String,
        extra_args: EA,
    ) -> Result<S> {
        for sf in &self.source_factories {
            if sf.can_create_from_format(&format) {
                return sf.create_source(previewer, extra_args);
            }
        }

        Err(Error::from(format!(
            "Cannot find source factory for file format {:?}. \
            Note that this might not be the full file name, due to being able to be decompressed.",
            format
        )))
    }

    fn create_source_by_autodetecting_format(
        &self,
        previewer: BufPreviewer,
        extra_args: EA,
    ) -> Result<S> {
        for sf in &self.source_factories {
            if sf.can_create_from_previewer(&previewer) {
                return sf.create_source(previewer, extra_args);
            }
        }

        Err(Error::from(
            "Failed to autodetect file format by peeking at file contents.",
        ))
    }
}

impl<S, EA> Clone for LocationInputFactory<S, EA> {
    fn clone(&self) -> Self {
        LocationInputFactory {
            seekable_transport_factory: self.seekable_transport_factory.clone(),
            previewer_transport_factory: self.previewer_transport_factory.clone(),
            source_factories: self.source_factories.clone(),
        }
    }
}
