//! Provides a way to load files from disk and to cache them.

use libflorescence::encoding;
use libflorescence::encoding::Encoding;
#[doc(inline)]
#[allow(unused_imports)]
pub use libflorescence::file::{
    id_hash, id_parts_hash, log_id_parts_hash, Diff, Id, IdHash, Kind, Path,
};

use crate::diff;
use iced_utils::Task;
use libflorescence::prelude::*;
use libflorescence::repo;

use std::borrow::Cow;
use std::collections::HashSet;
use std::mem;
use std::num::NonZero;
use std::path::PathBuf;

use clru::{CLruCache, WeightScale};
use tokio::sync::mpsc;
use tokio_stream::wrappers::UnboundedReceiverStream;
use tokio_stream::StreamExt;

// Invariant: Must be non-zero
const SRC_FILES_CACHE_CAPACITY: usize = 1024 * 1024 * 1024;

#[derive(Debug)]
pub struct State {
    pub file_load_tx: mpsc::UnboundedSender<(Id, usize)>,
    pub diffs_cache: DiffsCache,
}

#[derive(Debug, Clone)]
pub enum Msg {
    LoadedSrcFile {
        /// The cache counter value when the file was requested
        cache_counter: usize,
        id: Id,
        data: Vec<u8>,
        /// Some for a file, None for a dir
        encoding: Option<Encoding>,
    },
    /// File cannot be loaded because it's either been moved or deleted
    SrcFileDoesntExist {
        id: Id,
    },
    NoOp,
}

#[derive(Debug)]
pub struct DiffsCache {
    /// Used to prevent race-conditions between file loading and clearing cache
    pub counter: usize,
    pub inner: DiffsCacheInner,
}

pub type DiffsCacheInner =
    CLruCache<IdHash, Diff, std::hash::RandomState, DiffsCacheWeight>;

#[derive(Debug)]
pub struct DiffsCacheWeight;

pub fn init(repo_path: PathBuf) -> (State, Task<Msg>) {
    let (src_file_load_tx, src_file_load_rx) = mpsc::unbounded_channel();
    let src_file_load_rx = UnboundedReceiverStream::from(src_file_load_rx);
    let src_file_load_task = Task::run(
        src_file_load_rx
            .map(move |(id, cache_counter): (Id, usize)| {
                (repo_path.clone(), id, cache_counter)
            })
            .then(|(repo_path, id, cache_counter)| async move {
                load_src_file(repo_path, id, cache_counter).await
            }),
        |msg| msg,
    );
    let src_files_cache_capacity =
        NonZero::new(SRC_FILES_CACHE_CAPACITY).unwrap();

    let diffs_cache = DiffsCache {
        counter: 0,
        inner: DiffsCacheInner::with_scale(
            src_files_cache_capacity,
            DiffsCacheWeight,
        ),
    };

    let state = State {
        file_load_tx: src_file_load_tx,
        diffs_cache,
    };
    (state, src_file_load_task)
}

#[derive(Debug)]
pub struct Loaded {
    pub file_id: IdHash,
    pub unchanged_sections: HashSet<usize>,
}

pub fn update(
    state: &mut State,
    repo: &repo::State,
    msg: Msg,
) -> Option<Loaded> {
    match msg {
        Msg::LoadedSrcFile {
            cache_counter,
            id,
            data,
            encoding,
        } => {
            if state.diffs_cache.counter == cache_counter {
                let file_content = match encoding {
                    Some(Encoding::Text(encoding)) => {
                        let decoded = encoding.decode(&data);
                        diff::FileContent::Decoded(decoded)
                    }
                    Some(Encoding::Image) => diff::FileContent::Image(data),
                    Some(
                        Encoding::Other
                        | Encoding::Audio
                        | Encoding::Video
                        | Encoding::Font,
                    )
                    | None => diff::FileContent::UnknownEncoding,
                };
                let id_hash = id_hash(&id);
                match id.file_kind {
                    Kind::Untracked => {
                        let file_diff: diff::File =
                            diff::init_file(file_content, None);
                        let unchanged_sections =
                            diff::unchanged_sections(&file_diff);

                        diffs_cache_put(
                            &mut state.diffs_cache,
                            id_hash,
                            Diff::Loaded(file_diff),
                        );

                        return Some(Loaded {
                            file_id: id_hash,
                            unchanged_sections,
                        });
                    }
                    Kind::Changed => {
                        let changed_file = repo.changed_files.get(&id.path);

                        let file_diff: diff::File =
                            diff::init_file(file_content, changed_file);
                        let skip_sections =
                            diff::unchanged_sections(&file_diff);

                        diffs_cache_put(
                            &mut state.diffs_cache,
                            id_hash,
                            Diff::Loaded(file_diff),
                        );

                        return Some(Loaded {
                            file_id: id_hash,
                            unchanged_sections: skip_sections,
                        });
                    }
                }
            } else {
                // Reload for a new cache counter
                state
                    .file_load_tx
                    .send((id, state.diffs_cache.counter))
                    .unwrap();
            }
        }
        Msg::NoOp => {}
        Msg::SrcFileDoesntExist { id } => {
            let id_hash = id_hash(&id);
            return Some(Loaded {
                file_id: id_hash,
                unchanged_sections: HashSet::new(),
            });
        }
    }
    None
}

pub async fn load_src_file(
    repo_path: PathBuf,
    id: Id,
    cache_counter: usize,
) -> Msg {
    let mut path = repo_path;
    path.push(&id.path.raw);
    // TODO handle symlink
    if let Ok(metadata) = tokio::fs::metadata(&path).await
        && metadata.is_dir()
    {
        Msg::LoadedSrcFile {
            id,
            data: vec![],
            encoding: None,
            cache_counter,
        }
    } else if let Ok(data) = tokio::fs::read(&path).await {
        let encoding = encoding::detect(&data);
        Msg::LoadedSrcFile {
            id,
            data,
            encoding: Some(encoding),
            cache_counter,
        }
    } else {
        Msg::NoOp
    }
}

pub fn try_get_src_file(state: &State, id_hash: IdHash) -> Option<&diff::File> {
    state
        .diffs_cache
        .inner
        .peek(&id_hash)
        .and_then(|diff| match diff {
            Diff::Loading => None,
            Diff::Loaded(file) => Some(file),
        })
}

pub fn load_src_file_if_not_cached(state: &mut State, id: Id) {
    let id_hash = id_hash(&id);
    if !state.diffs_cache.inner.contains(&id_hash) {
        diffs_cache_put(&mut state.diffs_cache, id_hash, Diff::Loading);
        state
            .file_load_tx
            .send((id, state.diffs_cache.counter))
            .unwrap();
    }
}

pub fn src_file_doesnt_exist(
    state: &mut State,
    id: Id,
    changed_file: &repo::ChangedFile,
) -> Task<Msg> {
    let id_hash = id_hash(&id);
    if !state.diffs_cache.inner.contains(&id_hash) {
        let content = diff::FileContent::Decoded(Cow::from(""));
        let file = diff::init_file(content, Some(changed_file));
        diffs_cache_put(&mut state.diffs_cache, id_hash, Diff::Loaded(file));
        return Task::done(Msg::SrcFileDoesntExist { id });
    }
    Task::none()
}

pub fn diffs_cache_clear(cache: &mut DiffsCache) {
    cache.inner.clear();
    cache.counter = cache.counter.wrapping_add(1);
}

fn diffs_cache_put(cache: &mut DiffsCache, key: IdHash, value: Diff) {
    if let Err((key, value)) = cache.inner.put_with_weight(key, value) {
        let kv_weight = DiffsCacheWeight.weight(&key, &value);
        info!("Source file cache is too small to fit new key. Resizing cache to {kv_weight} fit it.");
        cache.inner.resize(NonZero::new(kv_weight).unwrap());
        let res = cache.inner.put_with_weight(key, value);
        assert!(res.is_ok());
    }
}

impl WeightScale<IdHash, Diff> for DiffsCacheWeight {
    fn weight(&self, _key: &IdHash, value: &Diff) -> usize {
        const KEY_WEIGHT: usize = mem::size_of::<IdHash>();
        let val_weight = match value {
            Diff::Loading => 0,
            Diff::Loaded(file) => match file {
                diff::File::Decoded(diff::DecodedFile {
                    combined:
                        diff::Combined {
                            sections,
                            max_line_num: _,
                        },
                    diffs_without_contents,
                }) => {
                    mem::size_of_val(sections)
                        + mem::size_of::<usize>()
                        + mem::size_of_val(diffs_without_contents)
                }
                diff::File::Undecodable(diff::UndecodableFile {
                    diffs_with_contents,
                    diffs_without_contents,
                }) => {
                    mem::size_of_val(diffs_with_contents)
                        + mem::size_of_val(diffs_without_contents)
                }
                diff::File::Image(bytes) => bytes.len(),
            },
        };
        KEY_WEIGHT + val_weight
    }
}