use crate::error::{Error, Result};
/// Port from https://github.com/getsentry/rust-sourcemap/blob/master/src/encoder.rs
/// It is a helper for encode `SourceMap` to vlq sourcemap string, but here some different.
/// - Quote `source_content` at parallel.
/// - If you using `ConcatSourceMapBuilder`, serialize `tokens` to vlq `mappings` at parallel.
use crate::{token::TokenChunk, SourceMap, Token};
#[cfg(feature = "concurrent")]
use rayon::prelude::*;

// Here using `serde_json::to_string` to serialization `names/source_contents/sources`.
// It will escape the string to avoid invalid JSON string.
pub fn encode(sourcemap: &SourceMap) -> Result<String> {
    let mut buf = String::new();
    buf.push_str("{\"version\":3,");
    if let Some(file) = sourcemap.get_file() {
        buf.push_str("\"file\":\"");
        buf.push_str(file);
        buf.push_str("\",");
    }
    if let Some(source_root) = sourcemap.get_source_root() {
        buf.push_str("\"sourceRoot\":\"");
        buf.push_str(source_root);
        buf.push_str("\",");
    }
    buf.push_str("\"names\":[");
    let names = sourcemap
        .names
        .iter()
        .map(|x| serde_json::to_string(x.as_ref()))
        .collect::<std::result::Result<Vec<_>, serde_json::Error>>()
        .map_err(Error::from)?;
    buf.push_str(&names.join(","));
    buf.push_str("],\"sources\":[");
    let sources = sourcemap
        .sources
        .iter()
        .map(|x| serde_json::to_string(x.as_ref()))
        .collect::<std::result::Result<Vec<_>, serde_json::Error>>()
        .map_err(Error::from)?;
    buf.push_str(&sources.join(","));
    // Quote `source_content` at parallel.
    if let Some(source_contents) = &sourcemap.source_contents {
        buf.push_str("],\"sourcesContent\":[");
        cfg_if::cfg_if! {
            if #[cfg(feature = "concurrent")] {
                let quote_source_contents = source_contents
                    .par_iter()
                    .map(|x| serde_json::to_string(x.as_ref()))
                    .collect::<std::result::Result<Vec<_>, serde_json::Error>>()
                    .map_err(Error::from)?;
            } else {
                let quote_source_contents = source_contents
                    .iter()
                    .map(|x| serde_json::to_string(x.as_ref()))
                    .collect::<std::result::Result<Vec<_>, serde_json::Error>>()
                    .map_err(Error::from)?;
            }
        };
        buf.push_str(&quote_source_contents.join(","));
    }
    if let Some(x_google_ignore_list) = &sourcemap.x_google_ignore_list {
        buf.push_str("],\"x_google_ignoreList\":[");
        let x_google_ignore_list =
            x_google_ignore_list.iter().map(ToString::to_string).collect::<Vec<_>>();
        buf.push_str(&x_google_ignore_list.join(","));
    }
    buf.push_str("],\"mappings\":\"");
    buf.push_str(&serialize_sourcemap_mappings(sourcemap));
    buf.push_str("\"}");
    Ok(buf)
}

#[allow(clippy::cast_possible_truncation)]
fn serialize_sourcemap_mappings(sm: &SourceMap) -> String {
    sm.token_chunks.as_ref().map_or_else(
        || {
            serialize_mappings(
                &sm.tokens,
                &TokenChunk::new(0, sm.tokens.len() as u32, 0, 0, 0, 0, 0, 0),
            )
        },
        |token_chunks| {
            // Serialize `tokens` to vlq `mappings` at parallel.
            cfg_if::cfg_if! {
                if #[cfg(feature = "concurrent")] {
                    token_chunks
                        .par_iter()
                        .map(|token_chunk| serialize_mappings(&sm.tokens, token_chunk))
                        .collect::<String>()
                } else {
                    token_chunks
                        .iter()
                        .map(|token_chunk| serialize_mappings(&sm.tokens, token_chunk))
                        .collect::<String>()
                }
            }
        },
    )
}

fn serialize_mappings(tokens: &[Token], token_chunk: &TokenChunk) -> String {
    let mut rv = String::new();

    let TokenChunk {
        start,
        end,
        mut prev_dst_line,
        mut prev_dst_col,
        mut prev_src_line,
        mut prev_src_col,
        mut prev_name_id,
        mut prev_source_id,
    } = *token_chunk;

    for (idx, token) in tokens[start as usize..end as usize].iter().enumerate() {
        if token.get_dst_line() != prev_dst_line {
            prev_dst_col = 0;
            while token.get_dst_line() != prev_dst_line {
                rv.push(';');
                prev_dst_line += 1;
            }
        } else if idx > 0 {
            if Some(token) == tokens.get(idx - 1) {
                continue;
            }
            rv.push(',');
        }

        encode_vlq_diff(&mut rv, token.get_dst_col(), prev_dst_col);
        prev_dst_col = token.get_dst_col();

        if let Some(source_id) = token.get_source_id() {
            encode_vlq_diff(&mut rv, source_id, prev_source_id);
            prev_source_id = source_id;
            encode_vlq_diff(&mut rv, token.get_src_line(), prev_src_line);
            prev_src_line = token.get_src_line();
            encode_vlq_diff(&mut rv, token.get_src_col(), prev_src_col);
            prev_src_col = token.get_src_col();
            if let Some(name_id) = token.get_name_id() {
                encode_vlq_diff(&mut rv, name_id, prev_name_id);
                prev_name_id = name_id;
            }
        }
    }

    rv
}

#[inline]
fn encode_vlq_diff(out: &mut String, a: u32, b: u32) {
    encode_vlq(out, i64::from(a) - i64::from(b));
}

const B64_CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
fn encode_vlq(out: &mut String, num: i64) {
    let mut num = if num < 0 { ((-num) << 1) + 1 } else { num << 1 };

    loop {
        let mut digit = num & 0b11111;
        num >>= 5;
        if num > 0 {
            digit |= 1 << 5;
        }
        out.push(B64_CHARS[digit as usize] as char);
        if num == 0 {
            break;
        }
    }
}

#[test]
fn test_encode() {
    let input = r#"{
        "version": 3,
        "sources": ["coolstuff.js"],
        "sourceRoot": "x",
        "names": ["x","alert"],
        "mappings": "AAAA,GAAIA,GAAI,EACR,IAAIA,GAAK,EAAG,CACVC,MAAM"
    }"#;
    let sm = SourceMap::from_json_string(input).unwrap();
    let sm2 = SourceMap::from_json_string(&sm.to_json_string().unwrap()).unwrap();

    for (tok1, tok2) in sm.get_tokens().zip(sm2.get_tokens()) {
        assert_eq!(tok1, tok2);
    }
}

#[test]
fn test_encode_escape_string() {
    // '\0' should be escaped.
    let mut sm = SourceMap::new(
        None,
        vec!["\0".into()],
        None,
        vec!["\0".into()],
        Some(vec!["\0".into()]),
        vec![],
        None,
    );
    sm.set_x_google_ignore_list(vec![0]);
    assert_eq!(
        sm.to_json_string().unwrap(),
        r#"{"version":3,"names":["\u0000"],"sources":["\u0000"],"sourcesContent":["\u0000"],"x_google_ignoreList":[0],"mappings":""}"#
    );
}
