use std::fs::File;
use std::io::{BufReader, Read};
use std::path::Path;

use crate::FwReaderSchema;
use crate::fw_error::Result;
use crate::fw_reader::FwReader;

#[cfg(feature = "tokio")]
use crate::async_fw_reader::AsyncFwReader;

/// Configuration for constructing a [`FwReader`] (or [`AsyncFwReader`] with
/// the `tokio` feature) from a [`FwReaderSchema`].
///
/// Created via [`FwReader::options`]. The schema is required at construction;
/// remaining options (such as [`encoding`](Self::encoding)) are configured
/// with chained setters before calling a terminal method to produce the
/// reader.
#[derive(Debug)]
pub struct FwReaderOptions {
    schema: FwReaderSchema,
    #[cfg(feature = "encoding_rs")]
    encoding: &'static encoding_rs::Encoding,
}

impl FwReaderOptions {
    /// Creates new options wrapping the given pre-built [`FwReaderSchema`].
    #[must_use]
    pub(crate) fn new(schema: FwReaderSchema) -> Self {
        Self {
            schema,
            #[cfg(feature = "encoding_rs")]
            encoding: encoding_rs::UTF_8,
        }
    }

    /// Sets the encoding used to decode string fields. Defaults to UTF-8.
    #[cfg(feature = "encoding_rs")]
    #[must_use]
    #[inline]
    pub fn encoding(mut self, encoding: &'static encoding_rs::Encoding) -> Self {
        self.encoding = encoding;
        self
    }

    /// Creates an [`FwReader`] wrapping the given source.
    ///
    /// For the best performance, wrap the source in a [`BufReader`] before
    /// passing it here.
    //noinspection RsSelfConvention
    #[must_use]
    #[inline]
    pub fn from_reader<R: Read>(self, reader: R) -> FwReader<R> {
        FwReader::new(
            reader,
            self.schema,
            #[cfg(feature = "encoding_rs")]
            self.encoding,
        )
    }

    /// Creates an [`FwReader`] wrapping the file in a [`BufReader`].
    //noinspection RsSelfConvention
    #[must_use]
    #[inline]
    pub fn from_file(self, file: File) -> FwReader<BufReader<File>> {
        self.from_reader(BufReader::new(file))
    }

    /// Opens the file at the given path and creates an [`FwReader`] wrapping
    /// it in a [`BufReader`].
    ///
    /// # Errors
    ///
    /// Returns an I/O error if the file cannot be opened.
    //noinspection RsSelfConvention
    pub fn from_path(self, path: impl AsRef<Path>) -> Result<FwReader<BufReader<File>>> {
        let file = File::open(path)?;
        Ok(self.from_file(file))
    }

    /// Creates an [`AsyncFwReader`] wrapping the given async source.
    ///
    /// For the best performance, wrap the source in a
    /// [`tokio::io::BufReader`] before passing it here.
    //noinspection RsSelfConvention
    #[cfg(feature = "tokio")]
    #[must_use]
    #[inline]
    pub fn from_tokio_reader<R: tokio::io::AsyncRead + Unpin>(self, reader: R) -> AsyncFwReader<R> {
        AsyncFwReader::new(
            reader,
            self.schema,
            #[cfg(feature = "encoding_rs")]
            self.encoding,
        )
    }

    /// Creates an [`AsyncFwReader`] wrapping the tokio file in a
    /// [`tokio::io::BufReader`].
    //noinspection RsSelfConvention
    #[cfg(feature = "tokio")]
    #[must_use]
    #[inline]
    pub fn from_tokio_file(
        self,
        file: tokio::fs::File,
    ) -> AsyncFwReader<tokio::io::BufReader<tokio::fs::File>> {
        self.from_tokio_reader(tokio::io::BufReader::new(file))
    }

    /// Opens the file at the given path and creates an [`FwReader`] wrapping
    /// it in a [`BufReader`].
    ///
    /// # Errors
    ///
    /// Returns an I/O error if the file cannot be opened.
    //noinspection RsSelfConvention
    #[cfg(feature = "tokio")]
    pub async fn from_tokio_path(
        self,
        path: impl AsRef<Path>,
    ) -> Result<AsyncFwReader<tokio::io::BufReader<tokio::fs::File>>> {
        let file = tokio::fs::File::open(path).await?;
        Ok(self.from_tokio_file(file))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::Alignment;
    use crate::FwfError;
    use crate::fw_reader_field::FwReaderField;

    fn name_age_schema() -> FwReaderSchema {
        FwReaderSchema::builder(7)
            .field(FwReaderField::builder(0..5).build())
            .field(FwReaderField::builder(5..7).build())
            .build()
            .unwrap()
    }

    #[test]
    fn options_construct_reader() {
        let schema = FwReaderSchema::builder(80).build().unwrap();
        let reader = FwReaderOptions::new(schema).from_reader(b"".as_slice());
        assert_eq!(reader.record_number(), 0);
    }

    #[test]
    fn schema_rejects_field_out_of_bounds() {
        let result = FwReaderSchema::builder(10)
            .field(FwReaderField::builder(0..5).build())
            .field(FwReaderField::builder(5..15).build())
            .build();
        assert!(result.is_err());
        let err = result.unwrap_err();
        assert!(matches!(err, FwfError::FieldOutOfBounds { field_index: 1 }));
    }

    #[test]
    fn schema_allows_overlapping_fields() {
        let schema = FwReaderSchema::builder(20)
            .field(FwReaderField::builder(0..10).build())
            .field(FwReaderField::builder(5..15).build())
            .build()
            .unwrap();
        let reader = FwReaderOptions::new(schema).from_reader(b"".as_slice());
        assert_eq!(reader.record_number(), 0);
    }

    #[test]
    fn schema_allows_non_contiguous_fields() {
        let schema = FwReaderSchema::builder(30)
            .field(FwReaderField::builder(0..5).build())
            .field(FwReaderField::builder(20..25).build())
            .build()
            .unwrap();
        let reader = FwReaderOptions::new(schema).from_reader(b"".as_slice());
        assert_eq!(reader.record_number(), 0);
    }

    #[test]
    fn schema_field_at_exact_boundary() {
        let schema = FwReaderSchema::builder(10)
            .field(FwReaderField::builder(0..10).build())
            .build()
            .unwrap();
        let _reader = FwReaderOptions::new(schema).from_reader(b"".as_slice());
    }

    #[test]
    fn schema_retains_field_properties() {
        let schema = FwReaderSchema::builder(20)
            .field(
                FwReaderField::builder(0..10)
                    .padding(b'0')
                    .alignment(Alignment::Right)
                    .build(),
            )
            .build()
            .unwrap();
        let _reader = FwReaderOptions::new(schema).from_reader(b"".as_slice());
    }

    #[test]
    fn separate_schemas_drive_separate_readers() {
        let _r1 = FwReaderOptions::new(name_age_schema()).from_reader(b"Alice30".as_slice());
        let _r2 = FwReaderOptions::new(name_age_schema()).from_reader(b"Bob  25".as_slice());
    }

    #[cfg(feature = "encoding_rs")]
    #[test]
    fn options_default_encoding_is_utf8() {
        let schema = FwReaderSchema::builder(10).build().unwrap();
        let _reader = FwReaderOptions::new(schema).from_reader(b"".as_slice());
    }

    #[cfg(feature = "encoding_rs")]
    #[test]
    fn options_custom_encoding() {
        let schema = FwReaderSchema::builder(10).build().unwrap();
        let _reader = FwReaderOptions::new(schema)
            .encoding(encoding_rs::SHIFT_JIS)
            .from_reader(b"".as_slice());
    }
}
