use rustc_ast as ast;
use rustc_ast::tokenstream::TokenStream;
use rustc_parse::{new_parser_from_source_str, parser::Parser, source_file_to_stream};
use rustc_session::parse::ParseSess;
use rustc_span::source_map::{FilePathMapping, SourceMap};
use rustc_span::with_default_session_globals;
use rustc_span::{BytePos, MultiSpan, Span};

use rustc_data_structures::sync::Lrc;
use rustc_errors::emitter::EmitterWriter;
use rustc_errors::{Handler, PResult};

use std::io;
use std::io::prelude::*;
use std::iter::Peekable;
use std::path::{Path, PathBuf};
use std::str;
use std::sync::{Arc, Mutex};

/// Map string to parser (via tts).
fn string_to_parser(ps: &ParseSess, source_str: String) -> Parser<'_> {
    new_parser_from_source_str(ps, PathBuf::from("bogofile").into(), source_str)
}

crate fn with_error_checking_parse<'a, T, F>(s: String, ps: &'a ParseSess, f: F) -> T
where
    F: FnOnce(&mut Parser<'a>) -> PResult<'a, T>,
{
    let mut p = string_to_parser(&ps, s);
    let x = f(&mut p).unwrap();
    p.sess.span_diagnostic.abort_if_errors();
    x
}

/// Maps a string to tts, using a made-up filename.
crate fn string_to_stream(source_str: String) -> TokenStream {
    let ps = ParseSess::new(FilePathMapping::empty());
    source_file_to_stream(
        &ps,
        ps.source_map().new_source_file(PathBuf::from("bogofile").into(), source_str),
        None,
    )
    .0
}

/// Parses a string, returns a crate.
crate fn string_to_crate(source_str: String) -> ast::Crate {
    let ps = ParseSess::new(FilePathMapping::empty());
    with_error_checking_parse(source_str, &ps, |p| p.parse_crate_mod())
}

/// Does the given string match the pattern? whitespace in the first string
/// may be deleted or replaced with other whitespace to match the pattern.
/// This function is relatively Unicode-ignorant; fortunately, the careful design
/// of UTF-8 mitigates this ignorance. It doesn't do NKF-normalization(?).
crate fn matches_codepattern(a: &str, b: &str) -> bool {
    let mut a_iter = a.chars().peekable();
    let mut b_iter = b.chars().peekable();

    loop {
        let (a, b) = match (a_iter.peek(), b_iter.peek()) {
            (None, None) => return true,
            (None, _) => return false,
            (Some(&a), None) => {
                if rustc_lexer::is_whitespace(a) {
                    break; // Trailing whitespace check is out of loop for borrowck.
                } else {
                    return false;
                }
            }
            (Some(&a), Some(&b)) => (a, b),
        };

        if rustc_lexer::is_whitespace(a) && rustc_lexer::is_whitespace(b) {
            // Skip whitespace for `a` and `b`.
            scan_for_non_ws_or_end(&mut a_iter);
            scan_for_non_ws_or_end(&mut b_iter);
        } else if rustc_lexer::is_whitespace(a) {
            // Skip whitespace for `a`.
            scan_for_non_ws_or_end(&mut a_iter);
        } else if a == b {
            a_iter.next();
            b_iter.next();
        } else {
            return false;
        }
    }

    // Check if a has *only* trailing whitespace.
    a_iter.all(rustc_lexer::is_whitespace)
}

/// Advances the given peekable `Iterator` until it reaches a non-whitespace character.
fn scan_for_non_ws_or_end<I: Iterator<Item = char>>(iter: &mut Peekable<I>) {
    while iter.peek().copied().map(|c| rustc_lexer::is_whitespace(c)) == Some(true) {
        iter.next();
    }
}

/// Identifies a position in the text by the n'th occurrence of a string.
struct Position {
    string: &'static str,
    count: usize,
}

struct SpanLabel {
    start: Position,
    end: Position,
    label: &'static str,
}

crate struct Shared<T: Write> {
    pub data: Arc<Mutex<T>>,
}

impl<T: Write> Write for Shared<T> {
    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
        self.data.lock().unwrap().write(buf)
    }

    fn flush(&mut self) -> io::Result<()> {
        self.data.lock().unwrap().flush()
    }
}

fn test_harness(file_text: &str, span_labels: Vec<SpanLabel>, expected_output: &str) {
    with_default_session_globals(|| {
        let output = Arc::new(Mutex::new(Vec::new()));

        let source_map = Lrc::new(SourceMap::new(FilePathMapping::empty()));
        source_map.new_source_file(Path::new("test.rs").to_owned().into(), file_text.to_owned());

        let primary_span = make_span(&file_text, &span_labels[0].start, &span_labels[0].end);
        let mut msp = MultiSpan::from_span(primary_span);
        for span_label in span_labels {
            let span = make_span(&file_text, &span_label.start, &span_label.end);
            msp.push_span_label(span, span_label.label.to_string());
            println!("span: {:?} label: {:?}", span, span_label.label);
            println!("text: {:?}", source_map.span_to_snippet(span));
        }

        let emitter = EmitterWriter::new(
            Box::new(Shared { data: output.clone() }),
            Some(source_map.clone()),
            false,
            false,
            false,
            None,
            false,
        );
        let handler = Handler::with_emitter(true, None, Box::new(emitter));
        handler.span_err(msp, "foo");

        assert!(
            expected_output.chars().next() == Some('\n'),
            "expected output should begin with newline"
        );
        let expected_output = &expected_output[1..];

        let bytes = output.lock().unwrap();
        let actual_output = str::from_utf8(&bytes).unwrap();
        println!("expected output:\n------\n{}------", expected_output);
        println!("actual output:\n------\n{}------", actual_output);

        assert!(expected_output == actual_output)
    })
}

fn make_span(file_text: &str, start: &Position, end: &Position) -> Span {
    let start = make_pos(file_text, start);
    let end = make_pos(file_text, end) + end.string.len(); // just after matching thing ends
    assert!(start <= end);
    Span::with_root_ctxt(BytePos(start as u32), BytePos(end as u32))
}

fn make_pos(file_text: &str, pos: &Position) -> usize {
    let mut remainder = file_text;
    let mut offset = 0;
    for _ in 0..pos.count {
        if let Some(n) = remainder.find(&pos.string) {
            offset += n;
            remainder = &remainder[n + 1..];
        } else {
            panic!("failed to find {} instances of {:?} in {:?}", pos.count, pos.string, file_text);
        }
    }
    offset
}

#[test]
fn ends_on_col0() {
    test_harness(
        r#"
fn foo() {
}
"#,
        vec![SpanLabel {
            start: Position { string: "{", count: 1 },
            end: Position { string: "}", count: 1 },
            label: "test",
        }],
        r#"
error: foo
 --> test.rs:2:10
  |
2 |   fn foo() {
  |  __________^
3 | | }
  | |_^ test

"#,
    );
}

#[test]
fn ends_on_col2() {
    test_harness(
        r#"
fn foo() {


  }
"#,
        vec![SpanLabel {
            start: Position { string: "{", count: 1 },
            end: Position { string: "}", count: 1 },
            label: "test",
        }],
        r#"
error: foo
 --> test.rs:2:10
  |
2 |   fn foo() {
  |  __________^
3 | |
4 | |
5 | |   }
  | |___^ test

"#,
    );
}
#[test]
fn non_nested() {
    test_harness(
        r#"
fn foo() {
  X0 Y0
  X1 Y1
  X2 Y2
}
"#,
        vec![
            SpanLabel {
                start: Position { string: "X0", count: 1 },
                end: Position { string: "X2", count: 1 },
                label: "`X` is a good letter",
            },
            SpanLabel {
                start: Position { string: "Y0", count: 1 },
                end: Position { string: "Y2", count: 1 },
                label: "`Y` is a good letter too",
            },
        ],
        r#"
error: foo
 --> test.rs:3:3
  |
3 |      X0 Y0
  |  ____^__-
  | | ___|
  | ||
4 | ||   X1 Y1
5 | ||   X2 Y2
  | ||____^__- `Y` is a good letter too
  |  |____|
  |       `X` is a good letter

"#,
    );
}

#[test]
fn nested() {
    test_harness(
        r#"
fn foo() {
  X0 Y0
  Y1 X1
}
"#,
        vec![
            SpanLabel {
                start: Position { string: "X0", count: 1 },
                end: Position { string: "X1", count: 1 },
                label: "`X` is a good letter",
            },
            SpanLabel {
                start: Position { string: "Y0", count: 1 },
                end: Position { string: "Y1", count: 1 },
                label: "`Y` is a good letter too",
            },
        ],
        r#"
error: foo
 --> test.rs:3:3
  |
3 |      X0 Y0
  |  ____^__-
  | | ___|
  | ||
4 | ||   Y1 X1
  | ||____-__^ `X` is a good letter
  | |_____|
  |       `Y` is a good letter too

"#,
    );
}

#[test]
fn different_overlap() {
    test_harness(
        r#"
fn foo() {
  X0 Y0 Z0
  X1 Y1 Z1
  X2 Y2 Z2
  X3 Y3 Z3
}
"#,
        vec![
            SpanLabel {
                start: Position { string: "Y0", count: 1 },
                end: Position { string: "X2", count: 1 },
                label: "`X` is a good letter",
            },
            SpanLabel {
                start: Position { string: "Z1", count: 1 },
                end: Position { string: "X3", count: 1 },
                label: "`Y` is a good letter too",
            },
        ],
        r#"
error: foo
 --> test.rs:3:6
  |
3 |      X0 Y0 Z0
  |   ______^
4 |  |   X1 Y1 Z1
  |  |_________-
5 | ||   X2 Y2 Z2
  | ||____^ `X` is a good letter
6 | |    X3 Y3 Z3
  | |_____- `Y` is a good letter too

"#,
    );
}

#[test]
fn triple_overlap() {
    test_harness(
        r#"
fn foo() {
  X0 Y0 Z0
  X1 Y1 Z1
  X2 Y2 Z2
}
"#,
        vec![
            SpanLabel {
                start: Position { string: "X0", count: 1 },
                end: Position { string: "X2", count: 1 },
                label: "`X` is a good letter",
            },
            SpanLabel {
                start: Position { string: "Y0", count: 1 },
                end: Position { string: "Y2", count: 1 },
                label: "`Y` is a good letter too",
            },
            SpanLabel {
                start: Position { string: "Z0", count: 1 },
                end: Position { string: "Z2", count: 1 },
                label: "`Z` label",
            },
        ],
        r#"
error: foo
 --> test.rs:3:3
  |
3 |       X0 Y0 Z0
  |  _____^__-__-
  | | ____|__|
  | || ___|
  | |||
4 | |||   X1 Y1 Z1
5 | |||   X2 Y2 Z2
  | |||____^__-__- `Z` label
  |  ||____|__|
  |   |____|  `Y` is a good letter too
  |        `X` is a good letter

"#,
    );
}

#[test]
fn triple_exact_overlap() {
    test_harness(
        r#"
fn foo() {
  X0 Y0 Z0
  X1 Y1 Z1
  X2 Y2 Z2
}
"#,
        vec![
            SpanLabel {
                start: Position { string: "X0", count: 1 },
                end: Position { string: "X2", count: 1 },
                label: "`X` is a good letter",
            },
            SpanLabel {
                start: Position { string: "X0", count: 1 },
                end: Position { string: "X2", count: 1 },
                label: "`Y` is a good letter too",
            },
            SpanLabel {
                start: Position { string: "X0", count: 1 },
                end: Position { string: "X2", count: 1 },
                label: "`Z` label",
            },
        ],
        r#"
error: foo
 --> test.rs:3:3
  |
3 | /   X0 Y0 Z0
4 | |   X1 Y1 Z1
5 | |   X2 Y2 Z2
  | |    ^
  | |    |
  | |    `X` is a good letter
  | |____`Y` is a good letter too
  |      `Z` label

"#,
    );
}

#[test]
fn minimum_depth() {
    test_harness(
        r#"
fn foo() {
  X0 Y0 Z0
  X1 Y1 Z1
  X2 Y2 Z2
  X3 Y3 Z3
}
"#,
        vec![
            SpanLabel {
                start: Position { string: "Y0", count: 1 },
                end: Position { string: "X1", count: 1 },
                label: "`X` is a good letter",
            },
            SpanLabel {
                start: Position { string: "Y1", count: 1 },
                end: Position { string: "Z2", count: 1 },
                label: "`Y` is a good letter too",
            },
            SpanLabel {
                start: Position { string: "X2", count: 1 },
                end: Position { string: "Y3", count: 1 },
                label: "`Z`",
            },
        ],
        r#"
error: foo
 --> test.rs:3:6
  |
3 |      X0 Y0 Z0
  |   ______^
4 |  |   X1 Y1 Z1
  |  |____^_-
  | ||____|
  | |     `X` is a good letter
5 | |    X2 Y2 Z2
  | |____-______- `Y` is a good letter too
  |  ____|
  | |
6 | |    X3 Y3 Z3
  | |________- `Z`

"#,
    );
}

#[test]
fn non_overlaping() {
    test_harness(
        r#"
fn foo() {
  X0 Y0 Z0
  X1 Y1 Z1
  X2 Y2 Z2
  X3 Y3 Z3
}
"#,
        vec![
            SpanLabel {
                start: Position { string: "X0", count: 1 },
                end: Position { string: "X1", count: 1 },
                label: "`X` is a good letter",
            },
            SpanLabel {
                start: Position { string: "Y2", count: 1 },
                end: Position { string: "Z3", count: 1 },
                label: "`Y` is a good letter too",
            },
        ],
        r#"
error: foo
 --> test.rs:3:3
  |
3 | /   X0 Y0 Z0
4 | |   X1 Y1 Z1
  | |____^ `X` is a good letter
5 |     X2 Y2 Z2
  |  ______-
6 | |   X3 Y3 Z3
  | |__________- `Y` is a good letter too

"#,
    );
}

#[test]
fn overlaping_start_and_end() {
    test_harness(
        r#"
fn foo() {
  X0 Y0 Z0
  X1 Y1 Z1
  X2 Y2 Z2
  X3 Y3 Z3
}
"#,
        vec![
            SpanLabel {
                start: Position { string: "Y0", count: 1 },
                end: Position { string: "X1", count: 1 },
                label: "`X` is a good letter",
            },
            SpanLabel {
                start: Position { string: "Z1", count: 1 },
                end: Position { string: "Z3", count: 1 },
                label: "`Y` is a good letter too",
            },
        ],
        r#"
error: foo
 --> test.rs:3:6
  |
3 |      X0 Y0 Z0
  |   ______^
4 |  |   X1 Y1 Z1
  |  |____^____-
  | ||____|
  | |     `X` is a good letter
5 | |    X2 Y2 Z2
6 | |    X3 Y3 Z3
  | |___________- `Y` is a good letter too

"#,
    );
}

#[test]
fn multiple_labels_primary_without_message() {
    test_harness(
        r#"
fn foo() {
  a { b { c } d }
}
"#,
        vec![
            SpanLabel {
                start: Position { string: "b", count: 1 },
                end: Position { string: "}", count: 1 },
                label: "",
            },
            SpanLabel {
                start: Position { string: "a", count: 1 },
                end: Position { string: "d", count: 1 },
                label: "`a` is a good letter",
            },
            SpanLabel {
                start: Position { string: "c", count: 1 },
                end: Position { string: "c", count: 1 },
                label: "",
            },
        ],
        r#"
error: foo
 --> test.rs:3:7
  |
3 |   a { b { c } d }
  |   ----^^^^-^^-- `a` is a good letter

"#,
    );
}

#[test]
fn multiple_labels_secondary_without_message() {
    test_harness(
        r#"
fn foo() {
  a { b { c } d }
}
"#,
        vec![
            SpanLabel {
                start: Position { string: "a", count: 1 },
                end: Position { string: "d", count: 1 },
                label: "`a` is a good letter",
            },
            SpanLabel {
                start: Position { string: "b", count: 1 },
                end: Position { string: "}", count: 1 },
                label: "",
            },
        ],
        r#"
error: foo
 --> test.rs:3:3
  |
3 |   a { b { c } d }
  |   ^^^^-------^^ `a` is a good letter

"#,
    );
}

#[test]
fn multiple_labels_primary_without_message_2() {
    test_harness(
        r#"
fn foo() {
  a { b { c } d }
}
"#,
        vec![
            SpanLabel {
                start: Position { string: "b", count: 1 },
                end: Position { string: "}", count: 1 },
                label: "`b` is a good letter",
            },
            SpanLabel {
                start: Position { string: "a", count: 1 },
                end: Position { string: "d", count: 1 },
                label: "",
            },
            SpanLabel {
                start: Position { string: "c", count: 1 },
                end: Position { string: "c", count: 1 },
                label: "",
            },
        ],
        r#"
error: foo
 --> test.rs:3:7
  |
3 |   a { b { c } d }
  |   ----^^^^-^^--
  |       |
  |       `b` is a good letter

"#,
    );
}

#[test]
fn multiple_labels_secondary_without_message_2() {
    test_harness(
        r#"
fn foo() {
  a { b { c } d }
}
"#,
        vec![
            SpanLabel {
                start: Position { string: "a", count: 1 },
                end: Position { string: "d", count: 1 },
                label: "",
            },
            SpanLabel {
                start: Position { string: "b", count: 1 },
                end: Position { string: "}", count: 1 },
                label: "`b` is a good letter",
            },
        ],
        r#"
error: foo
 --> test.rs:3:3
  |
3 |   a { b { c } d }
  |   ^^^^-------^^
  |       |
  |       `b` is a good letter

"#,
    );
}

#[test]
fn multiple_labels_secondary_without_message_3() {
    test_harness(
        r#"
fn foo() {
  a  bc  d
}
"#,
        vec![
            SpanLabel {
                start: Position { string: "a", count: 1 },
                end: Position { string: "b", count: 1 },
                label: "`a` is a good letter",
            },
            SpanLabel {
                start: Position { string: "c", count: 1 },
                end: Position { string: "d", count: 1 },
                label: "",
            },
        ],
        r#"
error: foo
 --> test.rs:3:3
  |
3 |   a  bc  d
  |   ^^^^----
  |   |
  |   `a` is a good letter

"#,
    );
}

#[test]
fn multiple_labels_without_message() {
    test_harness(
        r#"
fn foo() {
  a { b { c } d }
}
"#,
        vec![
            SpanLabel {
                start: Position { string: "a", count: 1 },
                end: Position { string: "d", count: 1 },
                label: "",
            },
            SpanLabel {
                start: Position { string: "b", count: 1 },
                end: Position { string: "}", count: 1 },
                label: "",
            },
        ],
        r#"
error: foo
 --> test.rs:3:3
  |
3 |   a { b { c } d }
  |   ^^^^-------^^

"#,
    );
}

#[test]
fn multiple_labels_without_message_2() {
    test_harness(
        r#"
fn foo() {
  a { b { c } d }
}
"#,
        vec![
            SpanLabel {
                start: Position { string: "b", count: 1 },
                end: Position { string: "}", count: 1 },
                label: "",
            },
            SpanLabel {
                start: Position { string: "a", count: 1 },
                end: Position { string: "d", count: 1 },
                label: "",
            },
            SpanLabel {
                start: Position { string: "c", count: 1 },
                end: Position { string: "c", count: 1 },
                label: "",
            },
        ],
        r#"
error: foo
 --> test.rs:3:7
  |
3 |   a { b { c } d }
  |   ----^^^^-^^--

"#,
    );
}

#[test]
fn multiple_labels_with_message() {
    test_harness(
        r#"
fn foo() {
  a { b { c } d }
}
"#,
        vec![
            SpanLabel {
                start: Position { string: "a", count: 1 },
                end: Position { string: "d", count: 1 },
                label: "`a` is a good letter",
            },
            SpanLabel {
                start: Position { string: "b", count: 1 },
                end: Position { string: "}", count: 1 },
                label: "`b` is a good letter",
            },
        ],
        r#"
error: foo
 --> test.rs:3:3
  |
3 |   a { b { c } d }
  |   ^^^^-------^^
  |   |   |
  |   |   `b` is a good letter
  |   `a` is a good letter

"#,
    );
}

#[test]
fn single_label_with_message() {
    test_harness(
        r#"
fn foo() {
  a { b { c } d }
}
"#,
        vec![SpanLabel {
            start: Position { string: "a", count: 1 },
            end: Position { string: "d", count: 1 },
            label: "`a` is a good letter",
        }],
        r#"
error: foo
 --> test.rs:3:3
  |
3 |   a { b { c } d }
  |   ^^^^^^^^^^^^^ `a` is a good letter

"#,
    );
}

#[test]
fn single_label_without_message() {
    test_harness(
        r#"
fn foo() {
  a { b { c } d }
}
"#,
        vec![SpanLabel {
            start: Position { string: "a", count: 1 },
            end: Position { string: "d", count: 1 },
            label: "",
        }],
        r#"
error: foo
 --> test.rs:3:3
  |
3 |   a { b { c } d }
  |   ^^^^^^^^^^^^^

"#,
    );
}

#[test]
fn long_snippet() {
    test_harness(
        r#"
fn foo() {
  X0 Y0 Z0
  X1 Y1 Z1
1
2
3
4
5
6
7
8
9
10
  X2 Y2 Z2
  X3 Y3 Z3
}
"#,
        vec![
            SpanLabel {
                start: Position { string: "Y0", count: 1 },
                end: Position { string: "X1", count: 1 },
                label: "`X` is a good letter",
            },
            SpanLabel {
                start: Position { string: "Z1", count: 1 },
                end: Position { string: "Z3", count: 1 },
                label: "`Y` is a good letter too",
            },
        ],
        r#"
error: foo
  --> test.rs:3:6
   |
3  |      X0 Y0 Z0
   |   ______^
4  |  |   X1 Y1 Z1
   |  |____^____-
   | ||____|
   | |     `X` is a good letter
5  | |  1
6  | |  2
7  | |  3
...  |
15 | |    X2 Y2 Z2
16 | |    X3 Y3 Z3
   | |___________- `Y` is a good letter too

"#,
    );
}

#[test]
fn long_snippet_multiple_spans() {
    test_harness(
        r#"
fn foo() {
  X0 Y0 Z0
1
2
3
  X1 Y1 Z1
4
5
6
  X2 Y2 Z2
7
8
9
10
  X3 Y3 Z3
}
"#,
        vec![
            SpanLabel {
                start: Position { string: "Y0", count: 1 },
                end: Position { string: "Y3", count: 1 },
                label: "`Y` is a good letter",
            },
            SpanLabel {
                start: Position { string: "Z1", count: 1 },
                end: Position { string: "Z2", count: 1 },
                label: "`Z` is a good letter too",
            },
        ],
        r#"
error: foo
  --> test.rs:3:6
   |
3  |      X0 Y0 Z0
   |   ______^
4  |  | 1
5  |  | 2
6  |  | 3
7  |  |   X1 Y1 Z1
   |  |_________-
8  | || 4
9  | || 5
10 | || 6
11 | ||   X2 Y2 Z2
   | ||__________- `Z` is a good letter too
...   |
15 |  | 10
16 |  |   X3 Y3 Z3
   |  |_______^ `Y` is a good letter

"#,
    );
}
