extern crate rls_span as span;
#[macro_use]
extern crate log;

use std::collections::HashMap;
use std::fmt;
use std::fs;
use std::io::Read;
use std::marker::PhantomData;
use std::mem;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use std::thread::{self, Thread};

#[cfg(test)]
mod test;

macro_rules! try_opt_loc {
    ($e:expr) => {
        match $e {
            Some(e) => e,
            None => return Err(Error::BadLocation),
        }
    };
}

pub struct Vfs<U = ()>(VfsInternal<RealFileLoader, U>);

/// Span of the text to be replaced defined in col/row terms.
#[derive(Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct SpanData {
    /// Span of the text defined in col/row terms.
    pub span: span::Span<span::ZeroIndexed>,
    /// Length in chars of the text. If present,
    /// used to calculate replacement range instead of
    /// span's row_end/col_end fields. Needed for editors that
    /// can't properly calculate the latter fields.
    /// Span's row_start/col_start are still assumed valid.
    pub len: Option<u64>,
}

/// Span of text that VFS can operate with.
#[derive(Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub enum VfsSpan {
    /// Span with offsets based on unicode scalar values.
    UnicodeScalarValue(SpanData),
    /// Span with offsets based on UTF-16 code units.
    Utf16CodeUnit(SpanData),
}

impl VfsSpan {
    pub fn from_usv(span: span::Span<span::ZeroIndexed>, len: Option<u64>) -> VfsSpan {
        VfsSpan::UnicodeScalarValue(SpanData { span, len })
    }

    pub fn from_utf16(span: span::Span<span::ZeroIndexed>, len: Option<u64>) -> VfsSpan {
        VfsSpan::Utf16CodeUnit(SpanData { span, len })
    }

    /// Return a UTF-8 byte offset in `s` for a given text unit offset.
    pub fn byte_in_str(&self, s: &str, c: span::Column<span::ZeroIndexed>) -> Result<usize, Error> {
        match self {
            VfsSpan::UnicodeScalarValue(..) => byte_in_str(s, c),
            VfsSpan::Utf16CodeUnit(..) => byte_in_str_utf16(s, c),
        }
    }

    fn as_inner(&self) -> &SpanData {
        match self {
            VfsSpan::UnicodeScalarValue(span) => span,
            VfsSpan::Utf16CodeUnit(span) => span,
        }
    }

    pub fn span(&self) -> &span::Span<span::ZeroIndexed> {
        &self.as_inner().span
    }

    pub fn len(&self) -> Option<u64> {
        self.as_inner().len
    }
}

#[derive(Debug)]
pub enum Change {
    /// Create an in-memory image of the file.
    AddFile { file: PathBuf, text: String },
    /// Changes in-memory contents of the previously added file.
    ReplaceText {
        /// Span of the text to be replaced.
        span: VfsSpan,
        /// Text to replace specified text range with.
        text: String,
    },
}

impl Change {
    fn file(&self) -> &Path {
        match *self {
            Change::AddFile { ref file, .. } => file.as_ref(),
            Change::ReplaceText { ref span, .. } => span.span().file.as_ref(),
        }
    }
}

#[derive(Debug, Clone, Eq, PartialEq)]
pub enum Error {
    /// The given file has become out of sync with the filesystem.
    OutOfSync(PathBuf),
    /// IO error reading or writing the given path, 2nd arg is a message.
    Io(Option<PathBuf>, Option<String>),
    /// There are changes to the given file which have not been written to disk.
    UncommittedChanges(PathBuf),
    /// Client specified a location that is not within a file. I.e., a row or
    /// column not in the file.
    BadLocation,
    /// The requested file was not cached in the VFS.
    FileNotCached,
    /// Not really an error, file is cached but there is no user data for it.
    NoUserDataForFile,
    /// Wrong kind of file.
    BadFileKind,
    /// An internal error - a bug in the VFS.
    InternalError(&'static str),
}

impl ::std::error::Error for Error {
    fn description(&self) -> &str {
        match *self {
            Error::OutOfSync(ref _path_buf) => "file out of sync with filesystem",
            Error::Io(ref _path_buf, ref _message) => "io::Error reading or writing path",
            Error::UncommittedChanges(ref _path_buf) => {
                "changes exist which have not been written to disk"
            }
            Error::BadLocation => "client specified location not existing within a file",
            Error::FileNotCached => "requested file was not cached in the VFS",
            Error::NoUserDataForFile => "file is cached but there is no user data for it",
            Error::BadFileKind => {
                "file is not the correct kind for the operation (e.g., text op on binary file)"
            }
            Error::InternalError(_) => "internal error",
        }
    }
}

impl Into<String> for Error {
    fn into(self) -> String {
        ::std::error::Error::description(&self).to_owned()
    }
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match *self {
            Error::OutOfSync(ref path_buf) => {
                write!(f, "file {} out of sync with filesystem", path_buf.display())
            }
            Error::UncommittedChanges(ref path_buf) => {
                write!(f, "{} has uncommitted changes", path_buf.display())
            }
            Error::InternalError(e) => write!(f, "internal error: {}", e),
            Error::BadLocation
            | Error::FileNotCached
            | Error::NoUserDataForFile
            | Error::Io(..)
            | Error::BadFileKind => f.write_str(::std::error::Error::description(self)),
        }
    }
}

impl<U> Vfs<U> {
    /// Creates a new, empty VFS.
    pub fn new() -> Vfs<U> {
        Vfs(VfsInternal::<RealFileLoader, U>::new())
    }

    /// Indicate that the current file as known to the VFS has been written to
    /// disk.
    pub fn file_saved(&self, path: &Path) -> Result<(), Error> {
        self.0.file_saved(path)
    }

    /// Removes a file from the VFS. Does not check if the file is synced with
    /// the disk. Does not check if the file exists.
    pub fn flush_file(&self, path: &Path) -> Result<(), Error> {
        self.0.flush_file(path)
    }

    pub fn file_is_synced(&self, path: &Path) -> Result<bool, Error> {
        self.0.file_is_synced(path)
    }

    /// Record a set of changes to the VFS.
    pub fn on_changes(&self, changes: &[Change]) -> Result<(), Error> {
        self.0.on_changes(changes)
    }

    /// Return all files in the VFS.
    pub fn get_cached_files(&self) -> HashMap<PathBuf, String> {
        self.0.get_cached_files()
    }

    pub fn get_changes(&self) -> HashMap<PathBuf, String> {
        self.0.get_changes()
    }

    /// Returns true if the VFS contains any changed files.
    pub fn has_changes(&self) -> bool {
        self.0.has_changes()
    }

    pub fn set_file(&self, path: &Path, text: &str) {
        self.0.set_file(path, text)
    }

    pub fn load_file(&self, path: &Path) -> Result<FileContents, Error> {
        self.0.load_file(path)
    }

    pub fn load_line(
        &self,
        path: &Path,
        line: span::Row<span::ZeroIndexed>,
    ) -> Result<String, Error> {
        self.0.load_line(path, line)
    }

    pub fn load_lines(
        &self,
        path: &Path,
        line_start: span::Row<span::ZeroIndexed>,
        line_end: span::Row<span::ZeroIndexed>,
    ) -> Result<String, Error> {
        self.0.load_lines(path, line_start, line_end)
    }

    pub fn load_span(&self, span: span::Span<span::ZeroIndexed>) -> Result<String, Error> {
        self.0.load_span(span)
    }

    pub fn for_each_line<F>(&self, path: &Path, f: F) -> Result<(), Error>
    where
        F: FnMut(&str, usize) -> Result<(), Error>,
    {
        self.0.for_each_line(path, f)
    }

    pub fn write_file(&self, path: &Path) -> Result<(), Error> {
        self.0.write_file(path)
    }

    pub fn set_user_data(&self, path: &Path, data: Option<U>) -> Result<(), Error> {
        self.0.set_user_data(path, data)
    }

    // If f returns NoUserDataForFile, then the user data for the given file is erased.
    pub fn with_user_data<F, R>(&self, path: &Path, f: F) -> Result<R, Error>
    where
        F: FnOnce(Result<(Option<&str>, &mut U), Error>) -> Result<R, Error>,
    {
        self.0.with_user_data(path, f)
    }

    // If f returns NoUserDataForFile, then the user data for the given file is erased.
    pub fn ensure_user_data<F>(&self, path: &Path, f: F) -> Result<(), Error>
    where
        F: FnOnce(Option<&str>) -> Result<U, Error>,
    {
        self.0.ensure_user_data(path, f)
    }

    pub fn clear(&self) {
        self.0.clear()
    }
}

// Important invariants! If you are going to lock both files and pending_files,
// you must lock pending_files first.
// You must have both locks to insert or remove files.
struct VfsInternal<T, U> {
    files: Mutex<HashMap<PathBuf, File<U>>>,
    pending_files: Mutex<HashMap<PathBuf, Vec<Thread>>>,
    loader: PhantomData<T>,
}

impl<T: FileLoader, U> VfsInternal<T, U> {
    fn new() -> VfsInternal<T, U> {
        VfsInternal {
            files: Mutex::new(HashMap::new()),
            pending_files: Mutex::new(HashMap::new()),
            loader: PhantomData,
        }
    }

    fn clear(&self) {
        let mut pending_files = self.pending_files.lock().unwrap();
        let mut files = self.files.lock().unwrap();
        *files = HashMap::new();
        let mut new_pending_files = HashMap::new();
        mem::swap(&mut *pending_files, &mut new_pending_files);
        for ts in new_pending_files.values() {
            for t in ts {
                t.unpark();
            }
        }
    }

    fn file_saved(&self, path: &Path) -> Result<(), Error> {
        let mut files = self.files.lock().unwrap();
        if let Some(ref mut f) = files.get_mut(path) {
            match f.kind {
                FileKind::Text(ref mut f) => f.changed = false,
                FileKind::Binary(_) => return Err(Error::BadFileKind),
            }
        }
        Ok(())
    }

    fn flush_file(&self, path: &Path) -> Result<(), Error> {
        loop {
            let mut pending_files = self.pending_files.lock().unwrap();
            let mut files = self.files.lock().unwrap();
            if !pending_files.contains_key(path) {
                files.remove(path);
                return Ok(());
            }

            pending_files.get_mut(path).unwrap().push(thread::current());
            thread::park();
        }
    }

    fn file_is_synced(&self, path: &Path) -> Result<bool, Error> {
        let files = self.files.lock().unwrap();
        match files.get(path) {
            Some(f) => Ok(!f.changed()),
            None => Err(Error::FileNotCached),
        }
    }

    fn on_changes(&self, changes: &[Change]) -> Result<(), Error> {
        trace!("on_changes: {:?}", changes);
        for (file_name, changes) in coalesce_changes(changes) {
            let path = Path::new(file_name);
            {
                let mut files = self.files.lock().unwrap();
                if let Some(file) = files.get_mut(Path::new(path)) {
                    file.make_change(&changes)?;
                    continue;
                }
            }

            // FIXME(#11): if the first change is `Add`, we should avoid
            // loading the file. If the first change is not `Add`, then
            // this is subtly broken, because we can't guarantee that the
            // edits are intended to be applied to the version of the file
            // we read from disk. That is, the on disk contents might have
            // changed after the edit request.
            let mut file = T::read(Path::new(path))?;
            file.make_change(&changes)?;

            let mut files = self.files.lock().unwrap();
            files.insert(path.to_path_buf(), file);
        }

        Ok(())
    }

    fn set_file(&self, path: &Path, text: &str) {
        let file = File {
            kind: FileKind::Text(TextFile {
                text: text.to_owned(),
                line_indices: make_line_indices(text),
                changed: true,
            }),
            user_data: None,
        };

        loop {
            let mut pending_files = self.pending_files.lock().unwrap();
            let mut files = self.files.lock().unwrap();
            if !pending_files.contains_key(path) {
                files.insert(path.to_owned(), file);
                return;
            }

            pending_files.get_mut(path).unwrap().push(thread::current());
            thread::park();
        }
    }

    fn get_cached_files(&self) -> HashMap<PathBuf, String> {
        let files = self.files.lock().unwrap();
        files
            .iter()
            .filter_map(|(p, f)| match f.kind {
                FileKind::Text(ref f) => Some((p.clone(), f.text.clone())),
                FileKind::Binary(_) => None,
            })
            .collect()
    }

    fn get_changes(&self) -> HashMap<PathBuf, String> {
        let files = self.files.lock().unwrap();
        files
            .iter()
            .filter_map(|(p, f)| match f.kind {
                FileKind::Text(ref f) if f.changed => Some((p.clone(), f.text.clone())),
                _ => None,
            })
            .collect()
    }

    fn has_changes(&self) -> bool {
        let files = self.files.lock().unwrap();
        files.values().any(|f| f.changed())
    }

    fn load_line(&self, path: &Path, line: span::Row<span::ZeroIndexed>) -> Result<String, Error> {
        self.ensure_file(path, |f| f.load_line(line).map(|s| s.to_owned()))
    }

    fn load_lines(
        &self,
        path: &Path,
        line_start: span::Row<span::ZeroIndexed>,
        line_end: span::Row<span::ZeroIndexed>,
    ) -> Result<String, Error> {
        self.ensure_file(path, |f| f.load_lines(line_start, line_end).map(|s| s.to_owned()))
    }

    fn load_span(&self, span: span::Span<span::ZeroIndexed>) -> Result<String, Error> {
        self.ensure_file(&span.file, |f| f.load_range(span.range).map(|s| s.to_owned()))
    }

    fn for_each_line<F>(&self, path: &Path, f: F) -> Result<(), Error>
    where
        F: FnMut(&str, usize) -> Result<(), Error>,
    {
        self.ensure_file(path, |file| file.for_each_line(f))
    }

    fn load_file(&self, path: &Path) -> Result<FileContents, Error> {
        self.ensure_file(path, |f| Ok(f.contents()))
    }

    fn ensure_file<F, R>(&self, path: &Path, f: F) -> Result<R, Error>
    where
        F: FnOnce(&File<U>) -> Result<R, Error>,
    {
        loop {
            {
                let mut pending_files = self.pending_files.lock().unwrap();
                let files = self.files.lock().unwrap();
                if files.contains_key(path) {
                    return f(&files[path]);
                }
                if !pending_files.contains_key(path) {
                    pending_files.insert(path.to_owned(), vec![]);
                    break;
                }
                pending_files.get_mut(path).unwrap().push(thread::current());
            }
            thread::park();
        }

        // We should not hold the locks while we read from disk.
        let file = T::read(path);

        // Need to re-get the locks here.
        let mut pending_files = self.pending_files.lock().unwrap();
        let mut files = self.files.lock().unwrap();
        match file {
            Ok(file) => {
                files.insert(path.to_owned(), file);
                let ts = pending_files.remove(path).unwrap();
                for t in ts {
                    t.unpark();
                }
            }
            Err(e) => {
                let ts = pending_files.remove(path).unwrap();
                for t in ts {
                    t.unpark();
                }
                return Err(e);
            }
        }

        f(&files[path])
    }

    fn write_file(&self, path: &Path) -> Result<(), Error> {
        let file = {
            let mut files = self.files.lock().unwrap();
            match files.get_mut(path) {
                Some(f) => {
                    if let FileKind::Text(ref mut f) = f.kind {
                        f.changed = false;
                    }
                    f.kind.clone()
                }
                None => return Err(Error::FileNotCached),
            }
        };

        T::write(path, &file)?;
        Ok(())
    }

    pub fn set_user_data(&self, path: &Path, data: Option<U>) -> Result<(), Error> {
        let mut files = self.files.lock().unwrap();
        match files.get_mut(path) {
            Some(ref mut f) => {
                f.user_data = data;
                Ok(())
            }
            None => Err(Error::FileNotCached),
        }
    }

    // Note that f should not be a long-running operation since we hold the lock
    // to the VFS while it runs.
    pub fn with_user_data<F, R>(&self, path: &Path, f: F) -> Result<R, Error>
    where
        F: FnOnce(Result<(Option<&str>, &mut U), Error>) -> Result<R, Error>,
    {
        let mut files = self.files.lock().unwrap();
        let file = match files.get_mut(path) {
            Some(f) => f,
            None => return f(Err(Error::FileNotCached)),
        };

        let result = f(match file.user_data {
            Some(ref mut u) => {
                let text = match file.kind {
                    FileKind::Text(ref f) => Some(&f.text as &str),
                    FileKind::Binary(_) => None,
                };
                Ok((text, u))
            }
            None => Err(Error::NoUserDataForFile),
        });

        if let Err(Error::NoUserDataForFile) = result {
            file.user_data = None;
        }

        result
    }

    pub fn ensure_user_data<F>(&self, path: &Path, f: F) -> Result<(), Error>
    where
        F: FnOnce(Option<&str>) -> Result<U, Error>,
    {
        let mut files = self.files.lock().unwrap();
        match files.get_mut(path) {
            Some(ref mut file) => {
                if let None = file.user_data {
                    let text = match file.kind {
                        FileKind::Text(ref f) => Some(&f.text as &str),
                        FileKind::Binary(_) => None,
                    };
                    match f(text) {
                        Ok(u) => {
                            file.user_data = Some(u);
                            Ok(())
                        }
                        Err(Error::NoUserDataForFile) => {
                            file.user_data = None;
                            Ok(())
                        }
                        Err(e) => Err(e),
                    }
                } else {
                    Ok(())
                }
            }
            None => Err(Error::FileNotCached),
        }
    }
}

fn coalesce_changes<'a>(changes: &'a [Change]) -> HashMap<&'a Path, Vec<&'a Change>> {
    // Note that for any given file, we preserve the order of the changes.
    let mut result = HashMap::new();
    for c in changes {
        result.entry(&*c.file()).or_insert(vec![]).push(c);
    }
    result
}

fn make_line_indices(text: &str) -> Vec<u32> {
    let mut result = vec![0];
    for (i, b) in text.bytes().enumerate() {
        if b == 0xA {
            result.push((i + 1) as u32);
        }
    }
    result.push(text.len() as u32);
    result
}

#[derive(Clone)]
enum FileKind {
    Text(TextFile),
    Binary(Vec<u8>),
}

impl FileKind {
    fn as_bytes(&self) -> &[u8] {
        match *self {
            FileKind::Text(ref t) => t.text.as_bytes(),
            FileKind::Binary(ref b) => b,
        }
    }
}

#[derive(Debug, PartialEq)]
pub enum FileContents {
    Text(String),
    Binary(Vec<u8>),
}

#[derive(Clone)]
struct TextFile {
    // FIXME(https://github.com/jonathandturner/rustls/issues/21) should use a rope.
    text: String,
    line_indices: Vec<u32>,
    changed: bool,
}

struct File<U> {
    kind: FileKind,
    user_data: Option<U>,
}

impl<U> File<U> {
    fn contents(&self) -> FileContents {
        match self.kind {
            FileKind::Text(ref t) => FileContents::Text(t.text.clone()),
            FileKind::Binary(ref b) => FileContents::Binary(b.clone()),
        }
    }

    fn make_change(&mut self, changes: &[&Change]) -> Result<(), Error> {
        match self.kind {
            FileKind::Text(ref mut t) => {
                self.user_data = None;
                t.make_change(changes)
            }
            FileKind::Binary(_) => Err(Error::BadFileKind),
        }
    }

    fn load_line(&self, line: span::Row<span::ZeroIndexed>) -> Result<&str, Error> {
        match self.kind {
            FileKind::Text(ref t) => t.load_line(line),
            FileKind::Binary(_) => Err(Error::BadFileKind),
        }
    }

    fn load_lines(
        &self,
        line_start: span::Row<span::ZeroIndexed>,
        line_end: span::Row<span::ZeroIndexed>,
    ) -> Result<&str, Error> {
        match self.kind {
            FileKind::Text(ref t) => t.load_lines(line_start, line_end),
            FileKind::Binary(_) => Err(Error::BadFileKind),
        }
    }

    fn load_range(&self, range: span::Range<span::ZeroIndexed>) -> Result<&str, Error> {
        match self.kind {
            FileKind::Text(ref t) => t.load_range(range),
            FileKind::Binary(_) => Err(Error::BadFileKind),
        }
    }

    fn for_each_line<F>(&self, f: F) -> Result<(), Error>
    where
        F: FnMut(&str, usize) -> Result<(), Error>,
    {
        match self.kind {
            FileKind::Text(ref t) => t.for_each_line(f),
            FileKind::Binary(_) => Err(Error::BadFileKind),
        }
    }

    fn changed(&self) -> bool {
        match self.kind {
            FileKind::Text(ref t) => t.changed,
            FileKind::Binary(_) => false,
        }
    }
}

impl TextFile {
    fn make_change(&mut self, changes: &[&Change]) -> Result<(), Error> {
        trace!("TextFile::make_change");
        for c in changes {
            trace!("TextFile::make_change: {:?}", c);
            let new_text = match **c {
                Change::ReplaceText { span: ref vfs_span, ref text } => {
                    let (span, len) = (vfs_span.span(), vfs_span.len());

                    let range = {
                        let first_line = self.load_line(span.range.row_start)?;
                        let byte_start = self.line_indices[span.range.row_start.0 as usize]
                            + vfs_span.byte_in_str(first_line, span.range.col_start)? as u32;

                        let byte_end = if let Some(len) = len {
                            // if `len` exists, the replaced portion of text
                            // is `len` chars starting from row_start/col_start.
                            byte_start
                                + vfs_span.byte_in_str(
                                    &self.text[byte_start as usize..],
                                    span::Column::new_zero_indexed(len as u32),
                                )? as u32
                        } else {
                            // if no `len`, fall back to using row_end/col_end
                            // for determining the tail end of replaced text.
                            let last_line = self.load_line(span.range.row_end)?;
                            self.line_indices[span.range.row_end.0 as usize]
                                + vfs_span.byte_in_str(last_line, span.range.col_end)? as u32
                        };

                        (byte_start, byte_end)
                    };
                    let mut new_text = self.text[..range.0 as usize].to_owned();
                    new_text.push_str(text);
                    new_text.push_str(&self.text[range.1 as usize..]);
                    new_text
                }
                Change::AddFile { file: _, ref text } => text.to_owned(),
            };

            self.text = new_text;
            self.line_indices = make_line_indices(&self.text);
        }

        self.changed = true;
        Ok(())
    }

    fn load_line(&self, line: span::Row<span::ZeroIndexed>) -> Result<&str, Error> {
        let start = *try_opt_loc!(self.line_indices.get(line.0 as usize));
        let end = *try_opt_loc!(self.line_indices.get(line.0 as usize + 1));

        if (end as usize) <= self.text.len() && start <= end {
            Ok(&self.text[start as usize..end as usize])
        } else {
            Err(Error::BadLocation)
        }
    }

    fn load_lines(
        &self,
        line_start: span::Row<span::ZeroIndexed>,
        line_end: span::Row<span::ZeroIndexed>,
    ) -> Result<&str, Error> {
        let line_start = line_start.0 as usize;
        let mut line_end = line_end.0 as usize;
        if line_end >= self.line_indices.len() {
            line_end = self.line_indices.len() - 1;
        }

        let start = (*try_opt_loc!(self.line_indices.get(line_start))) as usize;
        let end = (*try_opt_loc!(self.line_indices.get(line_end))) as usize;

        if (end) <= self.text.len() && start <= end {
            Ok(&self.text[start..end])
        } else {
            Err(Error::BadLocation)
        }
    }

    fn load_range(&self, range: span::Range<span::ZeroIndexed>) -> Result<&str, Error> {
        let line_start = range.row_start.0 as usize;
        let mut line_end = range.row_end.0 as usize;
        if line_end >= self.line_indices.len() {
            line_end = self.line_indices.len() - 1;
        }

        let start = (*try_opt_loc!(self.line_indices.get(line_start))) as usize;
        let start = start + range.col_start.0 as usize;
        let end = (*try_opt_loc!(self.line_indices.get(line_end))) as usize;
        let end = end + range.col_end.0 as usize;

        if (end) <= self.text.len() && start <= end {
            Ok(&self.text[start..end])
        } else {
            Err(Error::BadLocation)
        }
    }

    fn for_each_line<F>(&self, mut f: F) -> Result<(), Error>
    where
        F: FnMut(&str, usize) -> Result<(), Error>,
    {
        let mut line_iter = self.line_indices.iter();
        let mut start = *line_iter.next().unwrap() as usize;
        for (i, idx) in line_iter.enumerate() {
            let idx = *idx as usize;
            f(&self.text[start..idx], i)?;
            start = idx;
        }

        Ok(())
    }
}

/// Return a UTF-8 byte offset in `s` for a given UTF-8 unicode scalar value offset.
fn byte_in_str(s: &str, c: span::Column<span::ZeroIndexed>) -> Result<usize, Error> {
    // We simulate a null-terminated string here because spans are exclusive at
    // the top, and so that index might be outside the length of the string.
    for (i, (b, _)) in s.char_indices().chain(Some((s.len(), '\0')).into_iter()).enumerate() {
        if c.0 as usize == i {
            return Ok(b);
        }
    }

    return Err(Error::InternalError("Out of bounds access in `byte_in_str`"));
}

/// Return a UTF-8 byte offset in `s` for a given UTF-16 code unit offset.
fn byte_in_str_utf16(s: &str, c: span::Column<span::ZeroIndexed>) -> Result<usize, Error> {
    let (mut utf8_offset, mut utf16_offset) = (0, 0);
    let target_utf16_offset = c.0 as usize;

    for chr in s.chars().chain(std::iter::once('\0')) {
        if utf16_offset > target_utf16_offset {
            break;
        } else if utf16_offset == target_utf16_offset {
            return Ok(utf8_offset);
        }

        utf8_offset += chr.len_utf8();
        utf16_offset += chr.len_utf16();
    }

    return Err(Error::InternalError("UTF-16 code unit offset is not at `str` char boundary"));
}

trait FileLoader {
    fn read<U>(file_name: &Path) -> Result<File<U>, Error>;
    fn write(file_name: &Path, file: &FileKind) -> Result<(), Error>;
}

struct RealFileLoader;

impl FileLoader for RealFileLoader {
    fn read<U>(file_name: &Path) -> Result<File<U>, Error> {
        let mut file = match fs::File::open(file_name) {
            Ok(f) => f,
            Err(_) => {
                return Err(Error::Io(
                    Some(file_name.to_owned()),
                    Some(format!("Could not open file: {}", file_name.display())),
                ));
            }
        };
        let mut buf = vec![];
        if let Err(_) = file.read_to_end(&mut buf) {
            return Err(Error::Io(
                Some(file_name.to_owned()),
                Some(format!("Could not read file: {}", file_name.display())),
            ));
        }

        match String::from_utf8(buf) {
            Ok(s) => Ok(File {
                kind: FileKind::Text(TextFile {
                    line_indices: make_line_indices(&s),
                    text: s,
                    changed: false,
                }),
                user_data: None,
            }),
            Err(e) => Ok(File { kind: FileKind::Binary(e.into_bytes()), user_data: None }),
        }
    }

    fn write(file_name: &Path, file: &FileKind) -> Result<(), Error> {
        use std::io::Write;

        macro_rules! try_io {
            ($e:expr) => {
                match $e {
                    Ok(e) => e,
                    Err(e) => {
                        return Err(Error::Io(Some(file_name.to_owned()), Some(e.to_string())));
                    }
                }
            };
        }

        let mut out = try_io!(::std::fs::File::create(file_name));
        try_io!(out.write_all(file.as_bytes()));
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use span::Column;

    #[test]
    fn byte_in_str_utf16() {
        use super::byte_in_str_utf16;

        assert_eq!(
            '😢'.len_utf8(),
            byte_in_str_utf16("😢a", Column::new_zero_indexed('😢'.len_utf16() as u32))
                .unwrap()
        );

        // 😢 is represented by 2 u16s - we can't index in the middle of a character
        assert!(byte_in_str_utf16("😢", Column::new_zero_indexed(1)).is_err());
    }
}
