#![cfg_attr(feature = "clippy", feature(plugin))]
#![cfg_attr(feature = "clippy", plugin(clippy))]
#![allow(non_snake_case)]

use std::cmp;

const NONE: u8 = 0;
const REMOVED: u8 = 1;
const COMMON: u8 = 2;
const ADDED: u8 = 3;

#[derive(Debug)]
struct Ctx<'a, T: 'a> {
    k: isize,
    base: isize,
    A: &'a [T],
    B: &'a [T],
    ptr: &'a mut usize,
    routes: &'a mut [usize],
    diff_types: &'a mut [u8],
}

#[derive(Debug, PartialEq)]
pub enum DiffResult {
    Removed(DiffElement),
    Common(DiffElement),
    Added(DiffElement),
}

#[derive(Debug, PartialEq)]
pub struct DiffElement {
    pub old_index: Option<usize>,
    pub new_index: Option<usize>,
}

#[derive(Clone)]
struct FarthestPoint {
    y: isize,
    id: usize,
}

fn back_trace<T: PartialEq + Clone>(
    A: &[T],
    B: &[T],
    current: &FarthestPoint,
    swapped: bool,
    routes: &[usize],
    diff_types: &[u8],
    prefix_size: usize,
) -> Vec<DiffResult> {
    let M = A.len();
    let N = B.len();
    let mut result: Vec<DiffResult> = vec![];
    let mut a = M - 1;
    let mut b = N - 1;
    let mut j = routes[current.id];
    let mut diff_type = diff_types[current.id];
    loop {
        if j == 0 && diff_type == NONE {
            break;
        }
        let prev = j;
        match diff_type {
            ADDED => {
                let old_index = if swapped {
                    None
                } else {
                    Some(a.wrapping_add(prefix_size))
                };
                let new_index = if swapped {
                    Some(a.wrapping_add(prefix_size))
                } else {
                    None
                };
                let result_type = if swapped {
                    DiffResult::Added
                } else {
                    DiffResult::Removed
                };
                result.push(result_type(DiffElement {
                    old_index,
                    new_index,
                }));
                a = a.wrapping_sub(1);
            }
            REMOVED => {
                let old_index = if swapped {
                    Some(b.wrapping_add(prefix_size))
                } else {
                    None
                };
                let new_index = if swapped {
                    None
                } else {
                    Some(b.wrapping_add(prefix_size))
                };
                let result_type = if swapped {
                    DiffResult::Removed
                } else {
                    DiffResult::Added
                };
                result.push(result_type(DiffElement {
                    old_index,
                    new_index,
                }));
                b = b.wrapping_sub(1);
            }
            _ => {
                let i = (Some(a.wrapping_add(prefix_size)), Some(b.wrapping_add(prefix_size)));
                let (old_index, new_index) = if swapped {
                    (i.1, i.0)
                } else {
                    i
                };

                result.push(DiffResult::Common(DiffElement { old_index, new_index }));
                a = a.wrapping_sub(1);
                b = b.wrapping_sub(1);
            }
        };
        j = routes[prev];
        diff_type = diff_types[prev];
    }
    result.into_iter().rev().collect()
}

fn create_fp<T: PartialEq>(fp: &[FarthestPoint], ctx: &mut Ctx<T>) -> FarthestPoint {
    if ctx.base < 1 as isize {
        let base = (ctx.base + 1) as usize;
        let prev = fp[base].id;
        let y = fp[base].y + 1;
        *ctx.ptr += 1;
        ctx.routes[*ctx.ptr] = prev;
        ctx.diff_types[*ctx.ptr] = REMOVED;
        return FarthestPoint { y, id: *ctx.ptr };
    } else if ctx.base + 1 >= fp.len() as isize {
        let base = (ctx.base - 1) as usize;
        let prev = fp[base].id;
        let y = fp[base].y;
        *ctx.ptr += 1;
        ctx.routes[*ctx.ptr] = prev;
        ctx.diff_types[*ctx.ptr] = ADDED;
        return FarthestPoint { y, id: *ctx.ptr };
    }

    let slide = &fp[(ctx.base - 1) as usize];
    let down = &fp[(ctx.base + 1) as usize];

    if slide.y == -1 && down.y == -1 {
        return FarthestPoint { y: 0, id: 0 };
    }
    *ctx.ptr += 1;
    if down.y == -1 || ctx.k == ctx.A.len() as isize || slide.y > down.y + 1 {
        let prev = slide.id;
        let y = slide.y;
        ctx.routes[*ctx.ptr] = prev;
        ctx.diff_types[*ctx.ptr] = ADDED;
        return FarthestPoint { y, id: *ctx.ptr };
    }
    let prev = down.id;
    let y = down.y + 1;
    ctx.routes[*ctx.ptr] = prev;
    ctx.diff_types[*ctx.ptr] = REMOVED;
    FarthestPoint { y, id: *ctx.ptr }
}

fn snake<T: PartialEq>(fps: &[FarthestPoint], ctx: &mut Ctx<T>) -> FarthestPoint {
    let M = ctx.A.len() as isize;
    let N = ctx.B.len() as isize;
    if ctx.k + N < 0 || M - ctx.k < 0 {
        return FarthestPoint { y: -1, id: 0 };
    }
    let mut fp = create_fp(fps, ctx);
    while fp.y + ctx.k < M && fp.y < N && ctx.A[(fp.y + ctx.k) as usize] == ctx.B[fp.y as usize] {
        let prev = fp.id;
        *ctx.ptr += 1;
        fp.id = *ctx.ptr;
        fp.y += 1;
        ctx.routes[*ctx.ptr] = prev;
        ctx.diff_types[*ctx.ptr] = COMMON;
    }
    fp
}

pub fn diff<T: PartialEq + Clone>(old: &[T], new: &[T]) -> Vec<DiffResult> {
    let new_len = new.len();
    let old_len = old.len();
    let common_prefix = old.iter().zip(new).take_while(|p| p.0 == p.1);
    let prefix_size = common_prefix.count();
    let common_suffix = old
        .iter()
        .rev()
        .zip(new.iter().rev())
        .take(cmp::min(old_len, new_len) - prefix_size)
        .take_while(|p| p.0 == p.1);
    let suffix_size = common_suffix.count();
    let swapped = old_len < new_len;
    let sliced_old = &old[prefix_size..(old_len - suffix_size)];
    let sliced_new = &new[prefix_size..(new_len - suffix_size)];

    let (A, B) = if swapped {
        (sliced_new, sliced_old)
    } else {
        (sliced_old, sliced_new)
    };

    let mut result: Vec<DiffResult> = Vec::new();
    let M = A.len();
    let N = B.len();

    if M == 0 && N == 0 && prefix_size == 0 && suffix_size == 0 {
        return result;
    }

    if N == 0 {
        let mut p = 0;
        while p < prefix_size {
            result.push(DiffResult::Common(DiffElement {
                old_index: Some(p),
                new_index: Some(p),
            }));
            p += 1;
        }

        let mut o = prefix_size;
        while o < M + prefix_size {
            if swapped {
                result.push(DiffResult::Added(DiffElement {
                    old_index: None,
                    new_index: Some(o),
                }));
            } else {
                result.push(DiffResult::Removed(DiffElement {
                    old_index: Some(o),
                    new_index: None,
                }));
            }
            o += 1;
        }

        let mut s = 0;
        let old_offset = sliced_old.len() + prefix_size;
        let new_offset = sliced_new.len() + prefix_size;
        while s < suffix_size {
            let old_index = s + old_offset;
            result.push(DiffResult::Common(DiffElement {
                old_index: Some(old_index),
                new_index: Some(s + new_offset),
            }));
            s += 1;
        }
        return result;
    }

    let offset = N as isize;
    let D = (M - N) as isize;
    let size = M + N + 1;
    let mut fp: Vec<FarthestPoint> = vec![FarthestPoint { y: -1, id: 0 }; size];
    let mut P = 0;
    let mut ptr = 0;
    let mut routes: Vec<usize> = vec![0; M * N + size + 1];
    let mut diff_types: Vec<u8> = vec![0; M * N + size + 1];

    let mut ctx = Ctx {
        k: 0,
        base: 0,
        A,
        B,
        ptr: &mut ptr,
        routes: &mut routes,
        diff_types: &mut diff_types,
    };

    while fp[(D + offset) as usize].y < N as isize {
        let mut k = -(P as isize);
        while k < D as isize {
            let base = k + offset as isize;
            ctx.k = k;
            ctx.base = base;
            fp[base as usize] = snake(&fp, &mut ctx);
            k += 1;
        }
        let mut k = (D + P) as isize;
        while k > D as isize {
            let base = k + offset as isize;
            ctx.k = k;
            ctx.base = base;
            fp[base as usize] = snake(&fp, &mut ctx);
            k -= 1;
        }
        let base = D + offset;
        ctx.k = D;
        ctx.base = base;
        fp[base as usize] = snake(&fp, &mut ctx);
        P += 1;
    }

    let mut result: Vec<DiffResult> = vec![];
    let mut p = 0;
    while p < prefix_size {
        result.push(DiffResult::Common(DiffElement {
            old_index: Some(p),
            new_index: Some(p),
        }));
        p += 1;
    }
    let base = (D + offset) as usize;
    let back_trace_result = back_trace(
        A,
        B,
        &fp[base],
        swapped,
        &ctx.routes,
        &ctx.diff_types,
        prefix_size,
    );
    result.extend(back_trace_result);
    let mut s = 0;
    let old_offset = sliced_old.len() + prefix_size;
    let new_offset = sliced_new.len() + prefix_size;
    while s < suffix_size {
        let old_index = s + old_offset;
        result.push(DiffResult::Common(DiffElement {
            old_index: Some(old_index),
            new_index: Some(new_offset + s),
        }));
        s += 1;
    }
    result
}

#[test]
fn should_return_one_changed() {
    let result = diff(&vec!["a"], &vec!["b"]);
    let expected = vec![
        DiffResult::Removed(DiffElement {
            old_index: Some(0),
            new_index: None,
        }),
        DiffResult::Added(DiffElement {
            old_index: None,
            new_index: Some(0),
        }),
    ];

    assert_eq!(result, expected);
}

#[test]
fn should_return_empty() {
    let a: Vec<String> = vec![];
    let b: Vec<String> = vec![];
    let result = diff(&a, &b);
    let expected = vec![];
    assert_eq!(result, expected);
}

#[test]
fn should_return_one_common() {
    let result = diff(&vec!["a"], &vec!["a"]);
    let expected = vec![DiffResult::Common(DiffElement {
        old_index: Some(0),
        new_index: Some(0),
    })];
    assert_eq!(result, expected);
}

#[test]
fn should_return_one_removed() {
    let result = diff(&vec!["a"], &vec![]);
    let expected = vec![DiffResult::Removed(DiffElement {
        old_index: Some(0),
        new_index: None,
    })];
    assert_eq!(result, expected);
}

#[test]
fn should_return_one_added() {
    let result = diff(&vec![], &vec!["a"]);
    let expected = vec![DiffResult::Added(DiffElement {
        old_index: None,
        new_index: Some(0),
    })];
    assert_eq!(result, expected);
}

#[test]
fn should_return_two_changed() {
    let result = diff(&vec!["a", "a"], &vec!["b", "b"]);
    let expected = vec![
        DiffResult::Removed(DiffElement {
            old_index: Some(0),
            new_index: None,
        }),
        DiffResult::Removed(DiffElement {
            old_index: Some(1),
            new_index: None,
        }),
        DiffResult::Added(DiffElement {
            old_index: None,
            new_index: Some(0),
        }),
        DiffResult::Added(DiffElement {
            old_index: None,
            new_index: Some(1),
        }),
    ];

    assert_eq!(result, expected);
}

#[test]
fn should_create_diff_result_with_added() {
    let result = diff(&vec!["abc", "c"], &vec!["abc", "bcd", "c"]);
    let expected = vec![
        DiffResult::Common(DiffElement {
            old_index: Some(0),
            new_index: Some(0),
        }),
        DiffResult::Added(DiffElement {
            old_index: None,
            new_index: Some(1),
        }),
        DiffResult::Common(DiffElement {
            old_index: Some(1),
            new_index: Some(2),
        }),
    ];

    assert_eq!(result, expected);
}

#[test]
fn should_create_diff_result_with_added_common_no_suffix() {
    let result = diff(&vec![
        "common",
    ], &vec![
        "added",
        "common",
        "added",
    ]);
    let expected = vec![
        DiffResult::Added(DiffElement {
            old_index: None,
            new_index: Some(0),
        }),
        DiffResult::Common(DiffElement {
            old_index: Some(0),
            new_index: Some(1),
        }),
        DiffResult::Added(DiffElement {
            old_index: None,
            new_index: Some(2),
        }),
    ];

    assert_eq!(result, expected);
}

#[test]
fn should_create_diff_result_with_added_common_no_suffix_swapped() {
    let result = diff(&vec![
        "added",
        "common",
        "added",
    ], &vec![
        "common",
    ]);
    let expected = vec![
        DiffResult::Removed(DiffElement {
            old_index: Some(0),
            new_index: None,
        }),
        DiffResult::Common(DiffElement {
            old_index: Some(1),
            new_index: Some(0),
        }),
        DiffResult::Removed(DiffElement {
            old_index: Some(2),
            new_index: None,
        }),
    ];

    assert_eq!(result, expected);
}

#[test]
fn should_create_diff_result_with_added_swapped() {
    let result = diff(&vec!["abc", "bcd", "c"], &vec!["abc", "c"]);
    let expected = vec![
        DiffResult::Common(DiffElement {
            old_index: Some(0),
            new_index: Some(0),
        }),
        DiffResult::Removed(DiffElement {
            old_index: Some(1),
            new_index: None,
        }),
        DiffResult::Common(DiffElement {
            old_index: Some(2),
            new_index: Some(1),
        }),
    ];

    assert_eq!(result, expected);
}

#[test]
fn should_create_diff_result_with_removed() {
    let result = diff(&vec!["abc", "bcd", "c"], &vec!["abc", "c"]);
    let expected = vec![
        DiffResult::Common(DiffElement {
            old_index: Some(0),
            new_index: Some(0),
        }),
        DiffResult::Removed(DiffElement {
            old_index: Some(1),
            new_index: None,
        }),
        DiffResult::Common(DiffElement {
            old_index: Some(2),
            new_index: Some(1),
        }),
    ];
    assert_eq!(result, expected);
}

#[test]
fn should_create_diff_result_without_new() {
    let result = diff(&vec!["abc", "bcd", "c"], &vec![]);
    let expected = vec![
        DiffResult::Removed(DiffElement {
            old_index: Some(0),
            new_index: None,
        }),
        DiffResult::Removed(DiffElement {
            old_index: Some(1),
            new_index: None,
        }),
        DiffResult::Removed(DiffElement {
            old_index: Some(2),
            new_index: None,
        }),
    ];
    assert_eq!(result, expected);
}

#[test]
fn should_create_diff_result_without_old() {
    let result = diff(&vec![], &vec!["abc", "bcd", "c"]);
    let expected = vec![
        DiffResult::Added(DiffElement {
            old_index: None,
            new_index: Some(0),
        }),
        DiffResult::Added(DiffElement {
            old_index: None,
            new_index: Some(1),
        }),
        DiffResult::Added(DiffElement {
            old_index: None,
            new_index: Some(2),
        }),
    ];
    assert_eq!(result, expected);
}

#[test]
fn should_create_empty_result_with_empty_input() {
    let result = diff(&vec![0u8; 0], &vec![0u8; 0]);
    let expected = vec![];
    assert_eq!(result, expected);
}

#[test]
fn should_create_one_removed_diff_result() {
    let result = diff(
        &vec!["abc", "bcd", "c", "aaa", "bbb", "ccc"],
        &vec!["abc", "c", "aaa", "bbb", "ccc"],
    );
    let expected = vec![
        DiffResult::Common(DiffElement {
            old_index: Some(0),
            new_index: Some(0),
        }),
        DiffResult::Removed(DiffElement {
            old_index: Some(1),
            new_index: None,
        }),
        DiffResult::Common(DiffElement {
            old_index: Some(2),
            new_index: Some(1),
        }),
        DiffResult::Common(DiffElement {
            old_index: Some(3),
            new_index: Some(2),
        }),
        DiffResult::Common(DiffElement {
            old_index: Some(4),
            new_index: Some(3),
        }),
        DiffResult::Common(DiffElement {
            old_index: Some(5),
            new_index: Some(4),
        }),
    ];
    assert_eq!(result, expected);
}

#[test]
fn should_create_string_and_strength_diff_result() {
    let result = diff(
        &vec!["s", "t", "r", "e", "n", "g", "t", "h"],
        &vec!["s", "t", "r", "i", "n", "g"],
    );
    let expected = vec![
        DiffResult::Common(DiffElement {
            old_index: Some(0),
            new_index: Some(0),
        }),
        DiffResult::Common(DiffElement {
            old_index: Some(1),
            new_index: Some(1),
        }),
        DiffResult::Common(DiffElement {
            old_index: Some(2),
            new_index: Some(2),
        }),
        DiffResult::Removed(DiffElement {
            old_index: Some(3),
            new_index: None,
        }),
        DiffResult::Added(DiffElement {
            old_index: None,
            new_index: Some(3),
        }),
        DiffResult::Common(DiffElement {
            old_index: Some(4),
            new_index: Some(4),
        }),
        DiffResult::Common(DiffElement {
            old_index: Some(5),
            new_index: Some(5),
        }),
        DiffResult::Removed(DiffElement {
            old_index: Some(6),
            new_index: None,
        }),
        DiffResult::Removed(DiffElement {
            old_index: Some(7),
            new_index: None,
        }),
    ];
    assert_eq!(result, expected);
}
