/* Copyright (C) 2020-2021 Purism SPC
 * SPDX-License-Identifier: GPL-3.0+
 */

/*! Loading layout files */

use std::env;
use std::fmt;
use std::path::PathBuf;

use super::{ Error, LoadError };
use super::parsing;

use crate::layout;
use crate::layout::ArrangementKind;
use crate::logging;
use crate::xdg;
use crate::imservice::ContentPurpose;


const FALLBACK_LAYOUT_NAME: &str = "us";


#[derive(Debug, Clone, PartialEq)]
enum DataSource {
    File(PathBuf),
    Resource(String),
}

impl fmt::Display for DataSource {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            DataSource::File(path) => write!(f, "Path: {:?}", path.display()),
            DataSource::Resource(name) => write!(f, "Resource: {}", name),
        }
    }
}

/* All functions in this family carry around ArrangementKind,
 * because it's not guaranteed to be preserved,
 * and the resulting layout needs to know which version was loaded.
 * See `squeek_layout_get_kind`.
 * Possible TODO: since this is used only in styling,
 * and makes the below code nastier than needed, maybe it should go.
 */

/// Returns ordered names treating `name` as the base name,
/// ignoring any `+` inside.
fn _get_arrangement_names(name: &str, arrangement: ArrangementKind)
    -> Vec<(ArrangementKind, String)>
{
    let name_with_arrangement = match arrangement {    
        ArrangementKind::Base => name.into(),
        ArrangementKind::Wide => format!("{}_wide", name),
    };
    
    let mut ret = Vec::new();
    if name_with_arrangement != name {
        ret.push((arrangement, name_with_arrangement));
    }
    ret.push((ArrangementKind::Base, name.into()));
    ret
}

/// Returns names accounting for any `+` in the `name`,
/// including the fallback to the default layout.
fn get_preferred_names(name: &str, kind: ArrangementKind)
    -> Vec<(ArrangementKind, String)>
{
    let mut ret = _get_arrangement_names(name, kind);
    
    let base_name_preferences = {
        let mut parts = name.splitn(2, '+');
        match parts.next() {
            Some(base) => {
                // The name is already equal to base, so nothing to add
                if base == name {
                    vec![]
                } else {
                    _get_arrangement_names(base, kind)
                }
            },
            // The layout's base name starts with a "+". Weird but OK.
            None => {
                log_print!(logging::Level::Surprise, "Base layout name is empty: {}", name);
                vec![]
            }
        }
    };
    
    ret.extend(base_name_preferences.into_iter());
    let fallback_names = _get_arrangement_names(FALLBACK_LAYOUT_NAME, kind);
    ret.extend(fallback_names.into_iter());
    ret
}

/// Includes the subdirectory before the forward slash.
type LayoutPath = String;

// This is only used inside iter_fallbacks_with_meta.
// Placed at the top scope
// because `use LayoutPurpose::*;`
// complains about "not in scope" otherwise.
// This seems to be a Rust 2015 edition problem.
/// Helper for determining where to look up the layout.
enum LayoutPurpose<'a> {
    Default,
    Special(&'a str),
}

/// Returns the directory string
/// where the layout should be looked up, including the slash.
fn get_directory_string(
    content_purpose: ContentPurpose,
    overlay: Option<&str>) -> String
{
    use self::LayoutPurpose::*;

    let layout_purpose = match overlay {
        None => match content_purpose {
            ContentPurpose::Email => Special("email"),
            ContentPurpose::Digits => Special("number"),
            ContentPurpose::Number => Special("number"),
            ContentPurpose::Phone => Special("number"),
            ContentPurpose::Pin => Special("pin"),
            ContentPurpose::Terminal => Special("terminal"),
            ContentPurpose::Url => Special("url"),
            _ => Default,
        },
        Some(overlay) => Special(overlay),
    };

    // For intuitiveness,
    // default purpose layouts are stored in the root directory,
    // as they correspond to typical text
    // and are seen the most often.
    match layout_purpose {
        Default => "".into(),
        Special(purpose) => format!("{}/", purpose),
    }
}

/// Returns an iterator over all fallback paths.
fn to_layout_paths(
    name_fallbacks: Vec<(ArrangementKind, String)>,
    content_purpose: ContentPurpose,
    overlay: Option<&str>,
) -> impl Iterator<Item=(ArrangementKind, LayoutPath)> {
    let prepend_directory = get_directory_string(content_purpose, overlay);

    name_fallbacks.into_iter()
        .map(move |(arrangement, name)|
            (arrangement, format!("{}{}", prepend_directory, name))
        )
}

type LayoutSource = (ArrangementKind, DataSource);

fn to_layout_sources(
    layout_paths: impl Iterator<Item=(ArrangementKind, LayoutPath)>,
    filesystem_path: Option<PathBuf>,
) -> impl Iterator<Item=LayoutSource> {
    layout_paths.flat_map(move |(arrangement, layout_path)| {
        let mut sources = Vec::new();
        if let Some(path) = &filesystem_path {
            sources.push((
                arrangement,
                DataSource::File(
                    path.join(&layout_path)
                        .with_extension("yaml")
                )
            ));
        };
        sources.push((arrangement, DataSource::Resource(layout_path.clone())));
        sources.into_iter()
    })
}

/// Returns possible sources, with first as the most preferred one.
/// Trying order: native lang of the right kind, native base,
/// fallback lang of the right kind, fallback base
fn iter_layout_sources(
    name: &str,
    arrangement: ArrangementKind,
    purpose: ContentPurpose,
    ui_overlay: Option<&str>,
    layout_storage: Option<PathBuf>,
) -> impl Iterator<Item=LayoutSource> {
    let names = get_preferred_names(name, arrangement);
    let paths = to_layout_paths(names, purpose, ui_overlay);
    to_layout_sources(paths, layout_storage)
}

fn load_layout_data(source: DataSource)
    -> Result<crate::layout::LayoutParseData, LoadError>
{
    let handler = logging::Print {};
    match source {
        DataSource::File(path) => {
            parsing::Layout::from_file(path.clone())
                .map_err(LoadError::BadData)
                .and_then(|layout|
                    layout.build(handler).0.map_err(LoadError::BadKeyMap)
                )
        },
        DataSource::Resource(name) => {
            parsing::Layout::from_resource(&name)
                .and_then(|layout|
                    layout.build(handler).0.map_err(LoadError::BadKeyMap)
                )
        },
    }
}

fn load_layout_data_with_fallback(
    name: &str,
    kind: ArrangementKind,
    purpose: ContentPurpose,
    overlay: Option<&str>,
) -> (ArrangementKind, layout::LayoutParseData) {

    // Build the path to the right keyboard layout subdirectory
    let path = env::var_os("SQUEEKBOARD_KEYBOARDSDIR")
        .map(PathBuf::from)
        .or_else(|| xdg::data_path("squeekboard/keyboards"));

    for (kind, source) in iter_layout_sources(&name, kind, purpose, overlay, path) {
        let layout = load_layout_data(source.clone());
        match layout {
            Err(e) => match (e, source) {
                (
                    LoadError::BadData(Error::Missing(e)),
                    DataSource::File(file)
                ) => log_print!(
                    logging::Level::Debug,
                    "Tried file {:?}, but it's missing: {}",
                    file, e
                ),
                (e, source) => log_print!(
                    logging::Level::Warning,
                    "Failed to load layout from {}: {}, skipping",
                    source, e
                ),
            },
            Ok(layout) => {
                log_print!(logging::Level::Info, "Loaded layout {}", source);
                return (kind, layout);
            }
        }
    }

    panic!("No useful layout found!");
}

pub fn load_layout(
    name: &String,
    kind: ArrangementKind,
    variant: ContentPurpose,
    overlay: &Option<String>,
) -> layout::Layout {
    let overlay = overlay.as_ref().map(String::as_str);
    let (found_kind, layout)
        = load_layout_data_with_fallback(name, kind, variant, overlay);
    layout::Layout::new(layout, found_kind, variant)
}

#[cfg(test)]
mod tests {
    use super::*;

    use crate::logging::ProblemPanic;

    #[test]
    fn parsing_fallback() {
        assert!(parsing::Layout::from_resource(FALLBACK_LAYOUT_NAME)
            .map(|layout| layout.build(ProblemPanic).0.unwrap())
            .is_ok()
        );
    }
    
    /// First fallback should be to builtin, not to FALLBACK_LAYOUT_NAME
    #[test]
    fn test_fallback_basic_builtin() {
        let sources = iter_layout_sources("nb", ArrangementKind::Base, ContentPurpose::Normal, None, None);
        
        assert_eq!(
            sources.collect::<Vec<_>>(),
            vec!(
                (ArrangementKind::Base, DataSource::Resource("nb".into())),
                (
                    ArrangementKind::Base,
                    DataSource::Resource(FALLBACK_LAYOUT_NAME.into())
                ),
            )
        );
    }
    
    /// Prefer loading from file system before builtin.
    #[test]
    fn test_preferences_order_path() {
        let sources = iter_layout_sources("nb", ArrangementKind::Base, ContentPurpose::Normal, None, Some(".".into()));
        
        assert_eq!(
            sources.collect::<Vec<_>>(),
            vec!(
                (ArrangementKind::Base, DataSource::File("./nb.yaml".into())),
                (ArrangementKind::Base, DataSource::Resource("nb".into())),
                (
                    ArrangementKind::Base,
                    DataSource::File("./us.yaml".into())
                ),
                (
                    ArrangementKind::Base,
                    DataSource::Resource("us".into())
                ),
            )
        );
    }

    /// If layout contains a "+", it should reach for what's in front of it too.
    #[test]
    fn test_preferences_order_base() {
        let sources = iter_layout_sources("nb+aliens", ArrangementKind::Base, ContentPurpose::Normal, None, None);

        assert_eq!(
            sources.collect::<Vec<_>>(),
            vec!(
                (ArrangementKind::Base, DataSource::Resource("nb+aliens".into())),
                (ArrangementKind::Base, DataSource::Resource("nb".into())),
                (
                    ArrangementKind::Base,
                    DataSource::Resource(FALLBACK_LAYOUT_NAME.into())
                ),
            )
        );
    }

    #[test]
    fn test_preferences_order_arrangement() {
        let sources = iter_layout_sources("nb", ArrangementKind::Wide, ContentPurpose::Normal, None, None);

        assert_eq!(
            sources.collect::<Vec<_>>(),
            vec!(
                (ArrangementKind::Wide, DataSource::Resource("nb_wide".into())),
                (ArrangementKind::Base, DataSource::Resource("nb".into())),
                (
                    ArrangementKind::Wide,
                    DataSource::Resource("us_wide".into())
                ),
                (
                    ArrangementKind::Base,
                    DataSource::Resource("us".into())
                ),
            )
        );
    }

    #[test]
    fn test_preferences_order_overlay() {
        let sources = iter_layout_sources("nb", ArrangementKind::Base, ContentPurpose::Normal, Some("terminal"), None);

        assert_eq!(
            sources.collect::<Vec<_>>(),
            vec!(
                (ArrangementKind::Base, DataSource::Resource("terminal/nb".into())),
                (
                    ArrangementKind::Base,
                    DataSource::Resource("terminal/us".into())
                ),
            )
        );
    }

    #[test]
    fn test_preferences_order_hint() {
        let sources = iter_layout_sources("nb", ArrangementKind::Base, ContentPurpose::Terminal, None, None);

        assert_eq!(
            sources.collect::<Vec<_>>(),
            vec!(
                (ArrangementKind::Base, DataSource::Resource("terminal/nb".into())),
                (
                    ArrangementKind::Base,
                    DataSource::Resource("terminal/us".into())
                ),
            )
        );
    }
}
