//! A file-system directory picker

use async_stream::try_stream;
use iced::widget::{button, column, container, row, text, text_input};
use iced::{Element, Font, Length, Padding, Pixels};
use iced_utils::{task, Task};
use normpath::{BasePathBuf, PathExt};
use nucleo_matcher::pattern::{Atom, AtomKind, CaseMatching, Normalization};
use nucleo_matcher::Matcher;
use tokio::fs;

use std::borrow::Cow;
use std::cmp;
use std::collections::{BTreeSet, VecDeque};
use std::path::{self, Path, PathBuf};

use crate::nav_scrollable;

#[derive(Debug)]
pub struct State {
    pub left_nav: nav_scrollable::State,
    pub right_nav: nav_scrollable::State,
    pub input: String,
    pub current_dir: BasePathBuf,
    pub current_kind: Option<RepoKind>,
    /// Absolute paths with the child directories of the `current_dir`
    pub child_dirs: Vec<PathBuf>,
    /// Relative paths with the child directories of the `current_dir` matching
    /// current `input`
    pub matched_child_dirs: Vec<String>,
    pub found_repos_dirs_pijul: Vec<PathBuf>,
    pub found_repos_dirs_git: Vec<PathBuf>,
    pub find_child_dirs_handle: Option<task::Handle>,
    pub find_repos_handle: Option<task::Handle>,
    /// Fuzzy matcher state that avoid allocations during matching
    pub matcher: Matcher,
    pub selection: Selection,
}

#[derive(Debug, Default)]
pub enum Selection {
    #[default]
    Input,
    SubDir(usize),
    ProjectPijul(usize),
    ProjectGit(usize),
}

#[derive(Debug, Clone, Copy)]
pub enum RepoKind {
    Pijul,
    Git,
}

#[derive(Debug, Clone)]
pub enum Msg {
    Input(String),
    SelectChildDir(PathBuf),
    EditSegment { ix: usize },
    FindChildDirsSuccess(Vec<PathBuf>),
    FindChildDirsFailed(String),
    FindReposDirsSuccess((PathBuf, RepoKind)),
    FindReposDirsDone,
    FindReposDirsFailed(String),
    SelectRepo(PathBuf),
}

pub fn init(start_path: impl Into<PathBuf>) -> (State, Task<Msg>) {
    let mut start_path = start_path.into();
    let current_dir = loop {
        match PathExt::normalize(start_path.as_path()) {
            Ok(path) => break path,
            Err(_) => {
                if let Some(path) = start_path.parent() {
                    start_path = path.to_path_buf();
                } else {
                    break BasePathBuf::new(PathBuf::from_iter([
                        path::Component::RootDir,
                    ]))
                    .expect("Must be able to construct root dir");
                }
            }
        }
    };
    let (find_child_dirs_task, find_child_dirs_handle) =
        find_child_dirs(current_dir.as_path());
    let find_child_dirs_handle = Some(find_child_dirs_handle);

    let (find_repos_task, find_repos_handle) =
        find_repos_dirs(current_dir.as_path());
    let find_repos_handle = Some(find_repos_handle);

    let mut state = State {
        left_nav: nav_scrollable::State::default(),
        right_nav: nav_scrollable::State::default(),
        input: String::default(),
        current_dir,
        current_kind: None,
        child_dirs: vec![],
        matched_child_dirs: vec![],
        found_repos_dirs_pijul: vec![],
        found_repos_dirs_git: vec![],
        find_child_dirs_handle,
        find_repos_handle,
        matcher: Matcher::default(),
        selection: Selection::default(),
    };
    update_current_kind(&mut state);
    (
        state,
        Task::batch([
            find_child_dirs_task,
            find_repos_task,
            // Focus the input
            task::widget_focus_next(),
        ]),
    )
}

#[derive(Debug)]
pub enum Action {
    /// Picked an existing Pijul or Git Repo
    Picked(PathBuf),
    /// Create a new project in non-existing directory
    CreateNew(PathBuf),
}

pub fn update(state: &mut State, msg: Msg) -> (Task<Msg>, Option<Action>) {
    let mut action = None;
    let task = match msg {
        Msg::Input(input) => {
            state.selection = Selection::Input;
            nav_scrollable::scroll_up_to_section(&mut state.right_nav, 0);
            nav_scrollable::scroll_up_to_section(&mut state.left_nav, 0);

            if input.is_empty() && state.input.is_empty() {
                // When there's an empty input and nothing is typed in (i.e.
                // indempotent delete), go up a dir
                if let Ok(Some(parent)) = state.current_dir.parent() {
                    change_dir(state, parent.to_owned())
                } else {
                    Task::none()
                }
            } else {
                if input.is_empty() {
                    state.matched_child_dirs = vec![];
                    state.input = input;
                    Task::none()
                } else if input.len() > state.input.len()
                    && (is_pijul(state.current_dir.as_path())
                        || is_git(state.current_dir.as_path()))
                {
                    // Ignore any input if we're already inside a repo path
                    Task::none()
                } else {
                    let ends_with_path_sep = input.ends_with(PATH_SEP);
                    let mut matches = if ends_with_path_sep {
                        // If the path ends with separator accept it only if
                        // there's an exact match
                        let (exact, _) = input.split_at(input.len() - 1);
                        // We're looking for a case-insensitive match, but the
                        // returned `matched` name has the actual file name case
                        if let Some(matched) =
                            state.child_dirs.iter().find_map(|dir| {
                                dir.file_name()
                                    .and_then(|name| name.to_str())
                                    .and_then(|name| {
                                        (name.to_lowercase()
                                            == exact.to_lowercase())
                                        .then_some(name)
                                    })
                            })
                        {
                            vec![matched.to_string()]
                        } else {
                            vec![]
                        }
                    } else {
                        match_child_dirs(state, &input)
                    };
                    // Change dir only if there are some matches
                    if ends_with_path_sep {
                        if !matches.is_empty() {
                            let matched =
                                state.current_dir.join(matches.pop().unwrap());
                            state.input = String::default();
                            change_dir(state, matched)
                        } else {
                            Task::none()
                        }
                    } else {
                        state.matched_child_dirs = matches;
                        state.input = input;
                        Task::none()
                    }
                }
            }
        }
        Msg::SelectChildDir(dir) => {
            state.input = String::default();
            let path = PathExt::normalize(dir.as_path()).unwrap();
            change_dir(state, path)
        }
        Msg::EditSegment { ix } => {
            let ix = cmp::max(ix, 1);
            let path: PathBuf =
                state.current_dir.as_path().iter().take(ix).collect();
            let path = PathExt::normalize(path.as_path()).unwrap();
            change_dir(state, path)
        }
        Msg::FindChildDirsSuccess(dirs) => {
            state.find_child_dirs_handle = None;
            state.child_dirs = dirs;

            // If the last char isn't path separator, update matches.
            // This is needed when `Msg::DeleteChar` deletes a path separator to
            // go up a dir
            if state.input.len() > 1 {
                let (_, last_char) =
                    state.input.split_at(state.input.len() - 1);
                if last_char != PATH_SEP_STR {
                    let input = PathBuf::from(&state.input);
                    let needle = input.file_name().unwrap().to_string_lossy();
                    let matches = match_child_dirs(state, &needle);
                    state.matched_child_dirs = matches;
                }
            }

            Task::none()
        }
        Msg::FindChildDirsFailed(_err) => {
            state.find_child_dirs_handle = None;
            // TODO report error
            Task::none()
        }
        Msg::FindReposDirsSuccess((dir, kind)) => {
            match kind {
                RepoKind::Pijul => state.found_repos_dirs_pijul.push(dir),
                RepoKind::Git => state.found_repos_dirs_git.push(dir),
            }
            Task::none()
        }
        Msg::FindReposDirsFailed(_err) => {
            // TODO report error
            Task::none()
        }
        Msg::FindReposDirsDone => {
            state.find_repos_handle = None;
            Task::none()
        }
        Msg::SelectRepo(dir) => {
            action = Some(Action::Picked(dir));
            Task::none()
        }
    };

    update_current_kind(state);

    (task, action)
}

#[derive(Debug, Clone, Copy)]
pub enum SelectionDir {
    Down,
    Up,
    Left,
    Right,
}

pub fn move_selection(state: &mut State, dir: SelectionDir) {
    let selection = &mut state.selection;
    match dir {
        SelectionDir::Down => match selection {
            Selection::Input => {
                if !state.matched_child_dirs.is_empty()
                    || !state.child_dirs.is_empty()
                {
                    let new_ix = 0;
                    *selection = Selection::SubDir(new_ix);
                    nav_scrollable::scroll_up_to_section(
                        &mut state.left_nav,
                        new_ix,
                    );
                }
            }
            Selection::SubDir(ix) => {
                if (!state.matched_child_dirs.is_empty()
                    && state.matched_child_dirs.len() - 1 > *ix)
                    || (!state.child_dirs.is_empty()
                        && state.child_dirs.len() - 1 > *ix)
                {
                    let new_ix = *ix + 1;
                    *selection = Selection::SubDir(new_ix);
                    nav_scrollable::scroll_down_to_section(
                        &mut state.left_nav,
                        new_ix,
                    );
                } else {
                    *selection = Selection::Input;
                    nav_scrollable::scroll_up_to_section(
                        &mut state.left_nav,
                        0,
                    );
                }
            }
            Selection::ProjectPijul(ix) => {
                if !state.found_repos_dirs_pijul.is_empty() {
                    if state.found_repos_dirs_pijul.len() - 1 > *ix {
                        let new_ix = *ix + 1;
                        *selection = Selection::ProjectPijul(new_ix);
                        nav_scrollable::scroll_down_to_section(
                            &mut state.right_nav,
                            new_ix,
                        );
                    } else if !state.found_repos_dirs_git.is_empty() {
                        *selection = Selection::ProjectGit(0);
                        nav_scrollable::scroll_down_to_section(
                            &mut state.right_nav,
                            state
                                .found_repos_dirs_pijul
                                .len()
                                .saturating_sub(1),
                        );
                    } else {
                        *selection = Selection::ProjectPijul(0);
                        nav_scrollable::scroll_up_to_section(
                            &mut state.right_nav,
                            0,
                        );
                    }
                } else if !state.found_repos_dirs_git.is_empty() {
                    *selection = Selection::ProjectGit(0);
                    nav_scrollable::scroll_up_to_section(
                        &mut state.right_nav,
                        state.found_repos_dirs_pijul.len().saturating_sub(1),
                    );
                } else {
                    *selection = Selection::Input;
                    nav_scrollable::scroll_up_to_section(
                        &mut state.right_nav,
                        0,
                    );
                }
            }
            Selection::ProjectGit(ix) => {
                if !state.found_repos_dirs_git.is_empty() {
                    if state.found_repos_dirs_git.len() - 1 > *ix {
                        let new_ix = *ix + 1;
                        *selection = Selection::ProjectGit(new_ix);
                        nav_scrollable::scroll_down_to_section(
                            &mut state.right_nav,
                            state
                                .found_repos_dirs_pijul
                                .len()
                                .saturating_sub(1)
                                + new_ix,
                        );
                    } else if !state.found_repos_dirs_pijul.is_empty() {
                        *selection = Selection::ProjectPijul(0);
                        nav_scrollable::scroll_up_to_section(
                            &mut state.right_nav,
                            0,
                        );
                    } else {
                        *selection = Selection::ProjectGit(0);
                        nav_scrollable::scroll_down_to_section(
                            &mut state.right_nav,
                            state
                                .found_repos_dirs_pijul
                                .len()
                                .saturating_sub(1),
                        );
                    }
                } else if !state.found_repos_dirs_pijul.is_empty() {
                    *selection = Selection::ProjectPijul(0);
                    nav_scrollable::scroll_up_to_section(
                        &mut state.right_nav,
                        0,
                    );
                } else {
                    *selection = Selection::Input;
                    nav_scrollable::scroll_up_to_section(
                        &mut state.right_nav,
                        0,
                    );
                }
            }
        },
        SelectionDir::Up => match selection {
            Selection::Input => {
                if !state.matched_child_dirs.is_empty() {
                    let new_ix = state.matched_child_dirs.len() - 1;
                    *selection = Selection::SubDir(new_ix);
                    nav_scrollable::scroll_down_to_section(
                        &mut state.left_nav,
                        new_ix,
                    );
                } else if !state.child_dirs.is_empty() {
                    let new_ix = state.child_dirs.len() - 1;
                    *selection = Selection::SubDir(new_ix);
                    nav_scrollable::scroll_down_to_section(
                        &mut state.left_nav,
                        new_ix,
                    );
                }
            }
            Selection::SubDir(ix) => {
                if *ix > 0
                    && (!state.matched_child_dirs.is_empty()
                        || !state.child_dirs.is_empty())
                {
                    let new_ix = *ix - 1;
                    *selection = Selection::SubDir(new_ix);
                    nav_scrollable::scroll_up_to_section(
                        &mut state.left_nav,
                        new_ix,
                    );
                } else {
                    *selection = Selection::Input;
                    nav_scrollable::scroll_up_to_section(
                        &mut state.left_nav,
                        0,
                    );
                }
            }
            Selection::ProjectPijul(ix) => {
                if !state.found_repos_dirs_pijul.is_empty() {
                    if *ix > 0 {
                        let new_ix = *ix - 1;
                        *selection = Selection::ProjectPijul(new_ix);
                        nav_scrollable::scroll_up_to_section(
                            &mut state.right_nav,
                            new_ix,
                        );
                    } else if !state.found_repos_dirs_git.is_empty() {
                        let new_ix = state.found_repos_dirs_git.len() - 1;
                        *selection = Selection::ProjectGit(new_ix);
                        nav_scrollable::scroll_up_to_section(
                            &mut state.right_nav,
                            state
                                .found_repos_dirs_pijul
                                .len()
                                .saturating_sub(1)
                                + new_ix,
                        );
                    } else {
                        let new_ix = state.found_repos_dirs_pijul.len() - 1;
                        *selection = Selection::ProjectPijul(new_ix);
                        nav_scrollable::scroll_up_to_section(
                            &mut state.right_nav,
                            new_ix,
                        );
                    }
                } else if !state.found_repos_dirs_git.is_empty() {
                    if *ix > 0 {
                        let new_ix = *ix - 1;
                        *selection = Selection::ProjectGit(new_ix);
                        nav_scrollable::scroll_up_to_section(
                            &mut state.right_nav,
                            new_ix,
                        );
                    } else {
                        let new_ix = state.found_repos_dirs_git.len() - 1;
                        *selection = Selection::ProjectGit(new_ix);
                        nav_scrollable::scroll_down_to_section(
                            &mut state.right_nav,
                            new_ix,
                        );
                    }
                } else {
                    *selection = Selection::Input;
                    nav_scrollable::scroll_up_to_section(
                        &mut state.right_nav,
                        0,
                    );
                }
            }
            Selection::ProjectGit(ix) => {
                if !state.found_repos_dirs_git.is_empty() {
                    if *ix > 0 {
                        let new_ix = *ix - 1;
                        *selection = Selection::ProjectGit(new_ix);
                        nav_scrollable::scroll_up_to_section(
                            &mut state.right_nav,
                            state
                                .found_repos_dirs_pijul
                                .len()
                                .saturating_sub(1)
                                + new_ix,
                        );
                    } else if !state.found_repos_dirs_pijul.is_empty() {
                        let new_ix = state.found_repos_dirs_pijul.len() - 1;
                        *selection = Selection::ProjectPijul(new_ix);
                        nav_scrollable::scroll_up_to_section(
                            &mut state.right_nav,
                            new_ix,
                        );
                    } else {
                        let new_ix = state.found_repos_dirs_git.len() - 1;
                        *selection = Selection::ProjectGit(new_ix);
                        nav_scrollable::scroll_down_to_section(
                            &mut state.right_nav,
                            state
                                .found_repos_dirs_pijul
                                .len()
                                .saturating_sub(1)
                                + new_ix,
                        );
                    }
                } else if !state.found_repos_dirs_pijul.is_empty() {
                    if *ix > 0 {
                        let new_ix = *ix - 1;
                        *selection = Selection::ProjectPijul(new_ix);
                        nav_scrollable::scroll_up_to_section(
                            &mut state.right_nav,
                            new_ix,
                        );
                    } else {
                        let new_ix = state.found_repos_dirs_pijul.len() - 1;
                        *selection = Selection::ProjectPijul(new_ix);
                        nav_scrollable::scroll_up_to_section(
                            &mut state.right_nav,
                            new_ix,
                        );
                    }
                } else {
                    *selection = Selection::Input;
                    nav_scrollable::scroll_up_to_section(
                        &mut state.right_nav,
                        0,
                    );
                }
            }
        },
        SelectionDir::Right | SelectionDir::Left => match selection {
            Selection::Input | Selection::SubDir(_) => {
                if !state.found_repos_dirs_pijul.is_empty() {
                    *selection = Selection::ProjectPijul(0);
                } else if !state.found_repos_dirs_git.is_empty() {
                    *selection = Selection::ProjectGit(0);
                }
                nav_scrollable::scroll_up_to_section(&mut state.right_nav, 0);
            }
            Selection::ProjectPijul(_) => {
                if !state.matched_child_dirs.is_empty()
                    || !state.child_dirs.is_empty()
                {
                    *selection = Selection::SubDir(0);
                } else {
                    *selection = Selection::Input;
                }
                nav_scrollable::scroll_up_to_section(&mut state.left_nav, 0);
            }
            Selection::ProjectGit(_) => {
                if !state.matched_child_dirs.is_empty()
                    || !state.child_dirs.is_empty()
                {
                    *selection = Selection::SubDir(0);
                } else {
                    *selection = Selection::Input;
                }
                nav_scrollable::scroll_up_to_section(&mut state.left_nav, 0);
            }
        },
    }
    update_current_kind(state);
}

pub fn confirm_input_or_selection(
    state: &mut State,
) -> (Task<Msg>, Option<Action>) {
    let mut action = None;
    let task = match state.selection {
        Selection::Input => {
            if state.input.is_empty() {
                action = Some(Action::Picked(
                    state.current_dir.as_path().to_path_buf(),
                ));
                Task::none()
            } else {
                nav_scrollable::scroll_up_to_section(&mut state.right_nav, 0);
                nav_scrollable::scroll_up_to_section(&mut state.left_nav, 0);

                // Accept input only if there's an exact match
                let mut matches = if state.child_dirs.iter().any(|dir| {
                    dir.file_name().map(|name| name.to_string_lossy())
                        == Some(Cow::Borrowed(&state.input))
                }) {
                    vec![state.input.clone()]
                } else {
                    vec![]
                };
                // Change dir if there is only one match
                if matches.len() == 1 {
                    let matched =
                        state.current_dir.join(matches.pop().unwrap());
                    state.input = String::default();
                    change_dir(state, matched)
                } else {
                    action = Some(Action::CreateNew(
                        state
                            .current_dir
                            .as_path()
                            .to_path_buf()
                            .join(&state.input),
                    ));
                    Task::none()
                }
            }
        }
        Selection::SubDir(ix) => {
            if !state.matched_child_dirs.is_empty() {
                if let Some(dir) = state.matched_child_dirs.get(ix) {
                    let path = state.current_dir.join(dir);
                    state.input = String::default();
                    change_dir(state, path)
                } else {
                    Task::none()
                }
            } else if !state.child_dirs.is_empty() {
                if let Some(dir) = state.child_dirs.get(ix) {
                    let path = state.current_dir.join(dir);
                    state.input = String::default();
                    change_dir(state, path)
                } else {
                    Task::none()
                }
            } else {
                Task::none()
            }
        }
        Selection::ProjectPijul(ix) => {
            if let Some(path) = state.found_repos_dirs_pijul.get(ix) {
                action = Some(Action::Picked(path.clone()));
            }
            Task::none()
        }
        Selection::ProjectGit(ix) => {
            if let Some(path) = state.found_repos_dirs_git.get(ix) {
                action = Some(Action::Picked(path.clone()));
            }
            Task::none()
        }
    };
    state.selection = Selection::Input;
    update_current_kind(state);
    (task, action)
}

#[allow(clippy::too_many_arguments)]
pub fn view<'a, Theme, Renderer>(
    state: &'a State,
    container_class_bordered: <Theme as container::Catalog>::Class<'a>,
    container_class_bordered_selected: <Theme as container::Catalog>::Class<'a>,
    button_class_normal: <Theme as button::Catalog>::Class<'a>,
    button_class_selected: <Theme as button::Catalog>::Class<'a>,
    scrollable_normal: <Theme as nav_scrollable::Catalog>::Class<'a>,
    scrollable_selected: <Theme as nav_scrollable::Catalog>::Class<'a>,
    text_class_decorative: <Theme as text::Catalog>::Class<'a>,
) -> Element<'a, Msg, Theme, Renderer>
where
    Theme: 'a
        + button::Catalog
        + container::Catalog
        + nav_scrollable::Catalog
        + text::Catalog
        + text_input::Catalog,
    <Theme as button::Catalog>::Class<'a>: Copy,
    <Theme as nav_scrollable::Catalog>::Class<'a>: Copy,
    <Theme as text::Catalog>::Class<'a>: Copy,
    Renderer: 'a + iced::advanced::Renderer + iced::advanced::text::Renderer,
    <Renderer as iced_core::text::Renderer>::Font: From<iced::Font>,
{
    let State {
        left_nav,
        right_nav,
        input,
        current_dir,
        child_dirs,
        matched_child_dirs,
        found_repos_dirs_pijul,
        found_repos_dirs_git,
        find_child_dirs_handle: _,
        find_repos_handle,
        matcher: _,
        selection,
        current_kind: _,
    } = state;

    const CORNER: &str = "├─";
    const LAST_CORNER: &str = "└─";
    const CORNER_SIZE: Pixels = Pixels(24.0);

    let mut left_children =
        Vec::with_capacity(if state.find_child_dirs_handle.is_some() {
            1
        } else {
            if !matched_child_dirs.is_empty() {
                matched_child_dirs.len()
            } else {
                child_dirs.len()
            }
        });

    // Display path in segments so that changing level forces element tree
    // updates to avoid the annoying Iced state cache that prevents text_input
    // from updating
    let left_header = Element::new(
        container(row([
            Element::new(row(itertools::intersperse_with(
                current_dir.as_path().iter().enumerate().map(|(ix, comp)| {
                    Element::new(
                        button(text(comp.to_string_lossy()))
                            .on_press(Msg::EditSegment { ix }),
                    )
                }),
                || {
                    Element::new(
                        container(text(PATH_SEP_STR)).padding(
                            text_input::DEFAULT_PADDING.left(0).right(0),
                        ),
                    )
                },
            ))),
            Element::new(
                container(text(PATH_SEP_STR))
                    .padding(text_input::DEFAULT_PADDING.left(0).right(0)),
            ),
            Element::new(text_input("", input).on_input(Msg::Input)),
        ]))
        .class(if matches!(selection, Selection::Input) {
            container_class_bordered_selected
        } else {
            container_class_bordered
        })
        .padding(button::DEFAULT_PADDING),
    );

    const CHILDREN_PADDING: Padding = Padding {
        left: 20.0,
        ..Padding::ZERO
    };
    if state.find_child_dirs_handle.is_some() {
        left_children.push(Element::new(text("Loading child directories...")));
    } else if !matched_child_dirs.is_empty() {
        let last_matched_child_ix = matched_child_dirs.len().saturating_sub(1);
        left_children.extend(matched_child_dirs.iter().enumerate().map(
            |(ix, dir)| {
                Element::new(
                    row([
                        Element::new(
                            text(if ix == last_matched_child_ix {
                                LAST_CORNER
                            } else {
                                CORNER
                            })
                            .size(CORNER_SIZE)
                            .font(Font::MONOSPACE)
                            .shaping(text::Shaping::Advanced)
                            .class(text_class_decorative),
                        ),
                        Element::new(
                            button(text(dir))
                                .on_press_with(move || {
                                    Msg::SelectChildDir(
                                        current_dir.as_path().join(dir),
                                    )
                                })
                                .class(
                                    if let Selection::SubDir(dir_ix) = selection
                                        && *dir_ix == ix
                                    {
                                        button_class_selected
                                    } else {
                                        button_class_normal
                                    },
                                ),
                        ),
                    ])
                    .padding(CHILDREN_PADDING),
                )
            },
        ));
    } else {
        let last_child_ix = child_dirs.len().saturating_sub(1);
        left_children.extend(child_dirs.iter().enumerate().map(|(ix, dir)| {
            let dir_str = dir.file_name().unwrap().to_string_lossy();
            Element::new(
                row([
                    Element::new(
                        text(if ix == last_child_ix {
                            LAST_CORNER
                        } else {
                            CORNER
                        })
                        .size(CORNER_SIZE)
                        .shaping(text::Shaping::Advanced)
                        .class(text_class_decorative),
                    ),
                    Element::new(
                        button(text(dir_str))
                            .on_press_with(|| Msg::SelectChildDir(dir.clone()))
                            .class(
                                if let Selection::SubDir(dir_ix) = selection
                                    && *dir_ix == ix
                                {
                                    button_class_selected
                                } else {
                                    button_class_normal
                                },
                            ),
                    ),
                ])
                .padding(CHILDREN_PADDING),
            )
        }));
    }

    let right_header = Element::new(text(
        if !found_repos_dirs_pijul.is_empty()
            || !found_repos_dirs_git.is_empty()
        {
            "Found repositories:"
        } else if find_repos_handle.is_none() {
            "No repositories found"
        } else if is_pijul(current_dir.as_path()) {
            "This is a Pijul repository"
        } else if is_git(current_dir.as_path()) {
            "This is a Git repository"
        } else {
            "Searching for repositories"
        },
    ));

    let right_children = found_repos_dirs_pijul
        .iter()
        .enumerate()
        .map(|(ix, dir)| {
            Element::new(
                button(text(dir.to_string_lossy()))
                    .on_press_with(|| Msg::SelectRepo(dir.clone()))
                    .class(
                        if let Selection::ProjectPijul(dir_ix) = selection
                            && *dir_ix == ix
                        {
                            button_class_selected
                        } else {
                            button_class_normal
                        },
                    ),
            )
        })
        .chain(found_repos_dirs_git.iter().enumerate().map(|(ix, dir)| {
            Element::new(
                button(text(format!("{} (git)", dir.to_string_lossy())))
                    .on_press_with(|| Msg::SelectRepo(dir.clone()))
                    .class(
                        if let Selection::ProjectGit(dir_ix) = selection
                            && *dir_ix == ix
                        {
                            button_class_selected
                        } else {
                            button_class_normal
                        },
                    ),
            )
        }))
        .collect::<Vec<_>>();

    // TODO make both scrollable
    let left = Element::new(
        container(column([
            left_header,
            Element::new(
                nav_scrollable(left_nav, left_children)
                    .class(if matches!(selection, Selection::SubDir(_)) {
                        scrollable_selected
                    } else {
                        scrollable_normal
                    })
                    .height(Length::Fill)
                    .width(Length::Fill),
            ),
        ]))
        .height(Length::Fill)
        .width(Length::FillPortion(1)),
    );
    let right = Element::new(
        container(column([
            right_header,
            Element::new(
                nav_scrollable(right_nav, right_children)
                    .class(
                        if matches!(
                            selection,
                            Selection::ProjectPijul(_)
                                | Selection::ProjectGit(_)
                        ) {
                            scrollable_selected
                        } else {
                            scrollable_normal
                        },
                    )
                    .height(Length::Fill)
                    .width(Length::Fill),
            ),
        ]))
        .height(Length::Fill)
        .width(Length::FillPortion(1)),
    );

    Element::new(
        row([left, right])
            .spacing(SPACING)
            .width(Length::Fill)
            .height(Length::Fill),
    )
}

const SPACING: u32 = 10;

fn change_dir(state: &mut State, path: BasePathBuf) -> Task<Msg> {
    state.current_dir = path;
    state.found_repos_dirs_pijul = vec![];
    state.found_repos_dirs_git = vec![];
    state.matched_child_dirs = vec![];

    state.child_dirs = vec![];
    let (find_child_dirs_task, find_child_dirs_handle) =
        find_child_dirs(state.current_dir.as_path());
    state.find_child_dirs_handle = Some(find_child_dirs_handle);

    let (find_repos_dirs_task, find_repos_dirs_handle) =
        find_repos_dirs(state.current_dir.as_path());
    state.find_repos_handle = Some(find_repos_dirs_handle);

    Task::batch([find_child_dirs_task, find_repos_dirs_task])
}

fn find_child_dirs(dir: &Path) -> (Task<Msg>, task::Handle) {
    let current_dir = dir.to_owned();
    let (task, handle) = Task::perform(
        async move {
            let mut read_dir = fs::read_dir(&current_dir).await?;
            let mut child_dirs = BTreeSet::new();
            while let Some(entry) = read_dir.next_entry().await? {
                let path = entry.path();
                let metadata = fs::metadata(&path).await?;
                if metadata.is_dir() && path.file_name().is_some() {
                    child_dirs.insert(path);
                }
            }
            Ok(child_dirs.into_iter().collect())
        },
        |result: std::io::Result<_>| match result {
            Ok(dirs) => Msg::FindChildDirsSuccess(dirs),
            Err(error) => Msg::FindChildDirsFailed(error.to_string()),
        },
    )
    .abortable();
    let handle = handle.abort_on_drop();
    (task, handle)
}

fn find_repos_dirs(dir: &Path) -> (Task<Msg>, task::Handle) {
    // Breadth-first search for pijul repos
    let dir = dir.to_path_buf();
    let stream = try_stream! {
        let mut to_try = VecDeque::from([dir.to_path_buf()]);

        while let Some(path) = to_try.pop_front() {
            let mut reader = fs::read_dir(&path).await.map_err(|err| format!("Cannot read dir {} ({err})", path.to_string_lossy()

            ))?;
            // Ignore errors in accessing entries
            while let Ok(Some(entry)) = reader.next_entry().await {
                let metadata = fs::metadata(entry.path()).await;
                if metadata.map(|meta| meta.is_dir()).unwrap_or(false) {
                    let path = entry.path();
                    if is_pijul(&path) {
                        yield Some((path, RepoKind::Pijul));
                    }
                    else if is_git(&path) {
                        yield Some((path, RepoKind::Git));
                    } else {
                        to_try.push_back(path);
                    }
                }
            }
        }
        yield None
    };

    let (task, handle) =
        Task::run(stream, |result: Result<_, String>| match result {
            Ok(Some((dir, kind))) => Msg::FindReposDirsSuccess((dir, kind)),
            Ok(None) => Msg::FindReposDirsDone,
            Err(error) => Msg::FindReposDirsFailed(error.to_string()),
        })
        .abortable();
    let handle = handle.abort_on_drop();
    (task, handle)
}

fn match_child_dirs(state: &mut State, needle: &str) -> Vec<String> {
    let atom = Atom::new(
        needle,
        CaseMatching::Ignore,
        Normalization::Never,
        AtomKind::Fuzzy,
        false,
    );
    itertools::Itertools::sorted_by(
        atom.match_list(
            state.child_dirs.iter().filter_map(|dir| {
                dir.file_name().map(|name| name.to_string_lossy())
            }),
            &mut state.matcher,
        )
        .into_iter(),
        // Sort by score descending
        |(_, left), (_, right)| Ord::cmp(right, left),
    )
    .map(|(dir, _score)| dir.to_string())
    .collect()
}

fn update_current_kind(state: &mut State) {
    let path_to_check = match &state.selection {
        Selection::Input => {
            Some(state.current_dir.join(&state.input).into_path_buf())
        }
        Selection::SubDir(ix) => {
            Some(if !state.matched_child_dirs.is_empty() {
                let subdir = state.matched_child_dirs.get(*ix).unwrap();
                state.current_dir.join(subdir).into_path_buf()
            } else {
                state.child_dirs.get(*ix).unwrap().clone()
            })
        }
        Selection::ProjectPijul(_) => {
            state.current_kind = Some(RepoKind::Pijul);
            None
        }
        Selection::ProjectGit(_) => {
            state.current_kind = Some(RepoKind::Git);
            None
        }
    };
    if let Some(path) = path_to_check {
        if is_pijul(path.as_path()) {
            state.current_kind = Some(RepoKind::Pijul);
        } else if is_git(path.as_path()) {
            state.current_kind = Some(RepoKind::Git);
        } else {
            state.current_kind = None;
        }
    }
}

fn is_pijul(dir: &Path) -> bool {
    dir.join(PIJUL_DIR).exists()
}

fn is_git(dir: &Path) -> bool {
    dir.join(GIT_DIR).exists()
}

const PIJUL_DIR: &str = ".pijul";
const GIT_DIR: &str = ".git";
const PATH_SEP: char = path::MAIN_SEPARATOR;
const PATH_SEP_STR: &str = path::MAIN_SEPARATOR_STR;