use std::{
    any::Any,
    cell::RefCell,
    fmt,
    fs::{self, File},
    io::{Read, Write, stdin, stdout},
    path::Path,
    rc::Rc,
};

use crate::{
    error::{MachineError, ValueConversionError},
    vm::{
        Value,
        env::Environment,
        value::{NativeObject, NativeValue, StringValue, convert::TryFromValue},
    },
};

#[derive(Debug)]
enum Stream {
    Stdin,
    Stdout,
    File(RefCell<Option<File>>),
}

impl NativeObject for Stream {
    fn as_any(&self) -> &dyn Any {
        self
    }
}

impl fmt::Display for Stream {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str("stream")
    }
}

impl Stream {
    pub fn read(&self, amount: usize) -> Option<Value> {
        let mut output = vec![];
        let mut remaining = amount;
        let mut buffer = [0; 4096];

        while remaining != 0 {
            let want = remaining.min(buffer.len());
            let len = match self {
                Self::Stdin => stdin().read(&mut buffer[..want]).ok()?,
                Self::File(cell) => match &mut *cell.borrow_mut() {
                    Some(file) => file.read(&mut buffer[..want]).ok()?,
                    None => return None,
                },
                Self::Stdout => return None,
            };

            if len == 0 {
                break;
            }

            output.extend_from_slice(&buffer[..len]);
            remaining -= len;
        }

        Some(output.into())
    }

    fn write(&self, bytes: &[u8]) -> Option<Value> {
        let len = match self {
            Self::Stdin => return None,
            Self::Stdout => stdout().write(bytes).ok()?,
            Self::File(cell) => match &mut *cell.borrow_mut() {
                Some(file) => file.write(bytes).ok()?,
                None => return None,
            },
        };
        Some(Value::Number(len.into()))
    }
}

pub fn load(env: &Rc<Environment>) {
    let stdin: Rc<dyn NativeObject> = Rc::new(Stream::Stdin);
    let stdout: Rc<dyn NativeObject> = Rc::new(Stream::Stdout);

    // FS API

    env.defun_native(
        "fs/exists?",
        "Returns #t if the path exists",
        |_, _, args| {
            let [path] = args else {
                return Err(MachineError::InvalidArgumentCount);
            };
            let path = StringValue::try_from_value(path)?;
            let path: &Path = (*path).as_ref();
            Ok(Value::Boolean(path.exists().into()))
        },
    );
    env.defun_native(
        "fs/directory?",
        "Returns #t if the path is a directory",
        |_, _, args| {
            let [path] = args else {
                return Err(MachineError::InvalidArgumentCount);
            };
            let path = StringValue::try_from_value(path)?;
            let path: &Path = (*path).as_ref();
            Ok(Value::Boolean(path.is_dir().into()))
        },
    );
    env.defun_native(
        "fs/file?",
        "Returns #t if the path is a file",
        |_, _, args| {
            let [path] = args else {
                return Err(MachineError::InvalidArgumentCount);
            };
            let path = StringValue::try_from_value(path)?;
            let path: &Path = (*path).as_ref();
            Ok(Value::Boolean(path.is_file().into()))
        },
    );
    env.defun_native(
        "fs/read-to-string",
        "Reads the file into a string",
        |_, _, args| {
            let [path] = args else {
                return Err(MachineError::InvalidArgumentCount);
            };
            let path = StringValue::try_from_value(path)?;
            // TODO error handling
            match fs::read_to_string(&*path) {
                Ok(data) => Ok(Value::String(data.into())),
                Err(_error) => Ok(Value::Nil),
            }
        },
    );
    env.defun_native(
        "fs/read-to-vector",
        "Reads the file into a byte vector",
        |_, _, args| {
            let [path] = args else {
                return Err(MachineError::InvalidArgumentCount);
            };
            let path = StringValue::try_from_value(path)?;
            // TODO error handling
            match fs::read(&*path) {
                Ok(data) => Ok(Value::Vector(Rc::new(data.into()))),
                Err(_error) => Ok(Value::Nil),
            }
        },
    );
    env.defun_native(
        "fs/home-directory",
        "Returns the path to user's home directory",
        |_, _, _| {
            if let Some(dir) = cross::path::home_dir() {
                Ok(Value::String(format!("{}", dir.display()).into()))
            } else {
                Ok(Value::Nil)
            }
        },
    );

    // Stream API

    env.set_global_value("stream/stdin", NativeValue::from(stdin))
        .expect("set_global_value failed");
    env.set_global_value("stream/stdout", NativeValue::from(stdout))
        .expect("set_global_value failed");

    env.defun_native("stream/open", "Opens a file", |_, _, args| {
        let [path] = args else {
            return Err(MachineError::InvalidArgumentCount);
        };
        let path = StringValue::try_from_value(path)?;
        match File::open(&*path) {
            Ok(file) => {
                let object: Rc<dyn NativeObject> = Rc::new(Stream::File(RefCell::new(Some(file))));
                Ok(Value::NativeValue(object.into()))
            }
            Err(error) => {
                eprintln!("TODO: not sure whether to use result/exceptions");
                eprintln!("{path}: {error}");
                todo!()
            }
        }
    });
    env.defun_native(
        "stream/read",
        "Reads data as a byte vector from the stream",
        |_, _, args| {
            let (stream, amount) = match args {
                // TODO read to end instead
                [stream] => (stream.as_native::<Stream>()?, 4096),
                [stream, amount] => (
                    stream.as_native::<Stream>()?,
                    usize::try_from_value(amount)?,
                ),
                _ => return Err(MachineError::InvalidArgumentCount),
            };
            match stream.read(amount) {
                Some(value) => Ok(value),
                None => todo!(),
            }
        },
    );
    env.defun_native("stream/write", "Writes data to the stream", |_, _, args| {
        let [stream, data] = args else {
            return Err(MachineError::InvalidArgumentCount);
        };
        let stream = stream.as_native::<Stream>()?;
        let result = match data {
            Value::Vector(value) if let Some(bytes) = value.as_bytes() => stream.write(&bytes[..]),
            Value::String(value) => stream.write(value.as_bytes()),
            _ => {
                return Err(MachineError::ValueConversion(ValueConversionError {
                    expected: "byte vector or string".into(),
                    got: data.clone(),
                }));
            }
        };
        match result {
            Some(value) => Ok(value),
            None => todo!(),
        }
    });
    env.defun_native("stream/close", "Closes the stream", |_, _, args| {
        let [stream] = args else {
            return Err(MachineError::InvalidArgumentCount);
        };
        let stream = stream.as_native::<Stream>()?;
        if let Stream::File(cell) = stream.as_ref() {
            cell.replace(None);
        }
        Ok(Value::Nil)
    });
}
