use failure::{Error, ResultExt};
use json;
use serde::Serialize;
use yaml;

mod types;
pub use self::types::*;
use std::env::vars;
use std::fs::File;
use std::io::Cursor;
use std::io::{self, stdin};
use treediff::{diff, tools};

mod util;

fn validate(cmds: &[Command]) -> Result<(), Error> {
    let num_merge_stdin_cmds = cmds
        .iter()
        .filter(|c| if let Command::MergeStdin = **c { true } else { false })
        .count();
    if num_merge_stdin_cmds > 1 {
        bail!(
            "Cannot read from stdin more than once, found {} invocations",
            num_merge_stdin_cmds
        );
    }
    Ok(())
}

fn to_json(s: String, state: &State) -> json::Value {
    let mut reader = io::Cursor::new(s);
    util::de_json_or_yaml_document_support(&mut reader, state)
        .unwrap_or_else(|_| json::Value::from(reader.into_inner()))
}

pub fn reduce(
    cmds: Vec<Command>,
    initial_state: Option<State>,
    mut output: &mut dyn io::Write,
) -> Result<State, Error> {
    validate(&cmds)?;

    use self::Command::*;
    let mut state = initial_state.unwrap_or_else(State::default);

    for cmd in cmds {
        match cmd {
            SelectToBuffer(pointer) => {
                let json_pointer = into_pointer(&pointer);
                match state.value {
                    Some(ref value) => state.buffer.push(
                        value
                            .pointer(&json_pointer)
                            .ok_or_else(|| format_err!("There is no value at '{}'", pointer))?
                            .clone(),
                    ),
                    None => bail!("There is no value to fetch from yet"),
                }
            }
            SerializeBuffer => show_buffer(state.output_mode.as_ref(), &state.buffer, &mut output)?,
            SelectNextMergeAt(at) => {
                state.select_next_at = Some(at);
            }
            InsertNextMergeAt(at) => {
                state.insert_next_at = Some(at);
            }
            SetMergeMode(mode) => {
                state.merge_mode = mode;
            }
            MergeValue(pointer, value) => {
                let value_to_merge = to_json(value, &state);
                let prev_insert_next_at = state.insert_next_at;
                state.insert_next_at = Some(pointer);

                state = merge(value_to_merge, state)?;

                state.insert_next_at = prev_insert_next_at;
            }
            MergeStdin => {
                if let Some(input) = probe_and_read_from_stdin()? {
                    let value_to_merge = util::de_json_or_yaml_document_support(input, &state)?;
                    state = merge(value_to_merge, state)?;
                }
            }
            MergeEnvironment(pattern) => {
                let map = vars().filter(|&(ref var, _)| pattern.matches(var)).fold(
                    json::Map::new(),
                    |mut m, (var, value)| {
                        m.insert(var, to_json(value, &state));
                        m
                    },
                );
                state = merge(json::Value::from(map), state)?;
            }
            MergePath(path) => {
                let reader =
                    File::open(&path).context(format!("Failed to open file at '{}' for reading", path.display()))?;
                let value_to_merge = util::de_json_or_yaml_document_support(reader, &state)?;
                state = merge(value_to_merge, state)?;
            }
            SetOutputMode(mode) => {
                state.output_mode = Some(mode);
            }
            Serialize => {
                state.value = match state.value {
                    Some(value) => Some(apply_transforms(
                        value,
                        state.insert_next_at.take(),
                        state.select_next_at.take(),
                    )?),
                    None => None,
                };

                show(state.output_mode.as_ref(), &state.value, &mut output)?
            }
        }
    }

    Ok(state)
}

fn probe_and_read_from_stdin() -> Result<Option<Cursor<Vec<u8>>>, Error> {
    use std::io::Read;

    let s = stdin();
    let mut stdin = s.lock();
    let mut buf = Vec::new();
    stdin
        .read_to_end(&mut buf)
        .context("Failed to read everything from standard input")?;
    Ok(if buf.is_empty() { None } else { Some(Cursor::new(buf)) })
}

fn show_buffer<W>(output_mode: Option<&OutputMode>, value: &[json::Value], mut ostream: W) -> Result<(), Error>
where
    W: io::Write,
{
    let has_complex_value = value.iter().any(|v| match *v {
        json::Value::Array(_) | json::Value::Object(_) => true,
        _ => false,
    });

    let output_mode = match output_mode {
        None => {
            if has_complex_value {
                Some(&OutputMode::Json)
            } else {
                None
            }
        }
        Some(mode) => Some(mode),
    };

    match output_mode {
        None => {
            for v in value {
                match *v {
                    json::Value::Bool(ref v) => writeln!(ostream, "{}", v),
                    json::Value::Number(ref v) => writeln!(ostream, "{}", v),
                    json::Value::String(ref v) => writeln!(ostream, "{}", v),
                    json::Value::Null => continue,
                    json::Value::Object(_) | json::Value::Array(_) => {
                        unreachable!("We should never try to print complex values here - this is a bug.")
                    }
                }?;
            }
            Ok(())
        }
        mode @ Some(_) => show(mode, value, ostream),
    }
}

fn show<V, W>(output_mode: Option<&OutputMode>, value: V, ostream: W) -> Result<(), Error>
where
    V: Serialize,
    W: io::Write,
{
    match output_mode {
        Some(&OutputMode::Json) | None => json::to_writer_pretty(ostream, &value).map_err(Into::into),
        Some(&OutputMode::Yaml) => yaml::to_writer(ostream, &value).map_err(Into::into),
    }
}

fn into_pointer(p: &str) -> String {
    let mut p = if p.find('/').is_none() {
        p.replace('.', "/")
    } else {
        p.to_owned()
    };
    if !p.starts_with('/') {
        p.insert(0, '/');
    }
    p
}

fn select_json_at(pointer: Option<String>, v: json::Value) -> Result<json::Value, Error> {
    match pointer {
        Some(pointer) => {
            let json_pointer = into_pointer(&pointer);
            v.pointer(&json_pointer)
                .map(|v| v.to_owned())
                .ok_or_else(|| format_err!("No value at pointer '{}'", pointer))
        }
        None => Ok(v),
    }
}

fn insert_json_at(pointer: Option<String>, v: json::Value) -> Result<json::Value, Error> {
    Ok(match pointer {
        Some(mut pointer) => {
            pointer = into_pointer(&pointer);
            let mut current = v;
            for elm in pointer.rsplit('/').filter(|s| !s.is_empty()) {
                let index: Result<usize, _> = elm.parse();
                match index {
                    Ok(index) => {
                        let mut a = vec![json::Value::Null; index + 1];
                        a[index] = current;
                        current = json::Value::from(a);
                    }
                    Err(_) => {
                        let mut map = json::Map::new();
                        map.insert(elm.to_owned(), current);
                        current = json::Value::from(map)
                    }
                }
            }
            current
        }
        None => v,
    })
}

fn apply_transforms(
    src: json::Value,
    insert_at: Option<String>,
    select_at: Option<String>,
) -> Result<json::Value, Error> {
    select_json_at(select_at, src).and_then(|src| insert_json_at(insert_at, src))
}

fn merge(src: json::Value, mut state: State) -> Result<State, Error> {
    let src = apply_transforms(src, state.insert_next_at.take(), state.select_next_at.take())?;

    match state.value {
        None => {
            state.value = Some(src);
            Ok(state)
        }
        Some(existing_value) => {
            let mut m = tools::Merger::with_filter(existing_value.clone(), NeverDrop::with_mode(&state.merge_mode));
            diff(&existing_value, &src, &mut m);

            if !m.filter().clashed_keys.is_empty() {
                Err(format_err!("{}", m.filter())
                    .context("The merge failed due to conflicts")
                    .into())
            } else {
                state.value = Some(m.into_inner());
                Ok(state)
            }
        }
    }
}
