use std::fs::File;
use std::io::{BufWriter, Write};
use std::path::Path;

use crate::FwWriterSchema;
use crate::fw_writer::FwWriter;
use crate::fw_writer_state::FwWriterState;

#[cfg(feature = "tokio")]
use crate::async_fw_writer::AsyncFwWriter;

/// Configuration for constructing a [`FwWriter`] (or [`AsyncFwWriter`] with
/// the `tokio` feature) from a [`FwWriterSchema`].
///
/// Created via [`FwWriter::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
/// writer.
#[derive(Debug)]
pub struct FwWriterOptions {
    schema: FwWriterSchema,
    #[cfg(feature = "encoding_rs")]
    encoding: &'static encoding_rs::Encoding,
}

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

    /// Sets the encoding used to encode string values. 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 [`FwWriter`] wrapping the given destination.
    ///
    /// For the best performance, wrap the destination in a [`BufWriter`]
    /// before passing it here.
    //noinspection RsSelfConvention
    #[must_use]
    #[inline]
    pub fn from_writer<W: Write>(self, writer: W) -> FwWriter<W> {
        FwWriter::new(writer, self.build_state())
    }

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

    /// Creates the file at the given path and creates an [`FwWriter`]
    /// wrapping it in a [`BufWriter`].
    ///
    /// # Errors
    ///
    /// Returns an I/O error if the file cannot be created.
    //noinspection RsSelfConvention
    pub fn from_path(self, path: impl AsRef<Path>) -> std::io::Result<FwWriter<BufWriter<File>>> {
        Ok(self.from_file(File::create(path)?))
    }

    /// Creates an [`AsyncFwWriter`] wrapping the given async destination.
    ///
    /// For the best performance, wrap the destination in a
    /// [`tokio::io::BufWriter`] before passing it here.
    //noinspection RsSelfConvention
    #[cfg(feature = "tokio")]
    #[must_use]
    #[inline]
    pub fn from_tokio_writer<W: tokio::io::AsyncWrite + Unpin>(
        self,
        writer: W,
    ) -> AsyncFwWriter<W> {
        AsyncFwWriter::new(writer, self.build_state())
    }

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

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

    fn build_state(self) -> FwWriterState {
        FwWriterState::new(
            self.schema,
            #[cfg(feature = "encoding_rs")]
            self.encoding,
        )
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::Alignment;
    use crate::TruncationPolicy;
    use crate::fw_writer_field::FwWriterField;
    use crate::record_separator::RecordSeparator;

    fn name_age_schema() -> FwWriterSchema {
        FwWriterSchema::builder()
            .field(FwWriterField::builder(5).build())
            .field(FwWriterField::builder(2).build())
            .build()
    }

    #[test]
    fn options_defaults() {
        let writer =
            FwWriterOptions::new(FwWriterSchema::builder().build()).from_writer(Vec::new());
        assert_eq!(writer.record_number(), 0);
    }

    #[test]
    fn options_with_fields() {
        let schema = FwWriterSchema::builder()
            .field(FwWriterField::builder(10).build())
            .field(FwWriterField::builder(2).build())
            .field(FwWriterField::builder(5).build())
            .build();
        let writer = FwWriterOptions::new(schema).from_writer(Vec::new());
        assert_eq!(writer.record_number(), 0);
    }

    #[test]
    fn separate_schemas_drive_separate_writers() {
        let mut w1 = FwWriterOptions::new(name_age_schema()).from_writer(Vec::new());
        w1.write_byte_record(&[b"Alice", b"30"]).unwrap();

        let mut w2 = FwWriterOptions::new(name_age_schema()).from_writer(Vec::new());
        w2.write_byte_record(&[b"Bob", b"25"]).unwrap();
    }

    #[test]
    fn schema_retains_truncation() {
        let schema = FwWriterSchema::builder()
            .field(FwWriterField::builder(3).build())
            .truncation(TruncationPolicy::Right)
            .build();
        let mut writer = FwWriterOptions::new(schema).from_writer(Vec::new());
        writer.write_byte_record(&[b"Hello"]).unwrap();
    }

    #[test]
    fn schema_retains_separator() {
        let schema = FwWriterSchema::builder()
            .field(FwWriterField::builder(5).build())
            .separator(RecordSeparator::Lf)
            .build();
        let mut buf = Vec::new();
        let mut writer = FwWriterOptions::new(schema).from_writer(&mut buf);
        writer.write_byte_record(&[b"Alice"]).unwrap();
        drop(writer);
        assert_eq!(buf, b"Alice\n");
    }

    #[cfg(feature = "encoding_rs")]
    #[test]
    fn options_custom_encoding() {
        let _writer = FwWriterOptions::new(FwWriterSchema::builder().build())
            .encoding(encoding_rs::SHIFT_JIS)
            .from_writer(Vec::new());
    }

    #[test]
    fn schema_retains_field_properties() {
        let schema = FwWriterSchema::builder()
            .field(
                FwWriterField::builder(8)
                    .padding(b'*')
                    .alignment(Alignment::Right)
                    .truncation(TruncationPolicy::Right)
                    .build(),
            )
            .build();
        let mut buf = Vec::new();
        let mut writer = FwWriterOptions::new(schema).from_writer(&mut buf);
        writer.write_byte_record(&[b"Hi"]).unwrap();
        drop(writer);
        assert_eq!(buf, b"******Hi");
    }
}
