use std::iter::Fuse;
use std::str::Chars;

mod buffer;
pub mod table;

use buffer::Buffer;

pub fn nfd(s: &str) -> Decompositions<Chars<'_>> {
    Decompositions {
        iter: s.chars().fuse(),
        buffer: Buffer::new(),
    }
}

pub struct Decompositions<I> {
    iter: Fuse<I>,
    buffer: Buffer,
}

impl<I: Iterator<Item = char>> Iterator for Decompositions<I> {
    type Item = char;

    #[inline(always)]
    fn next(&mut self) -> Option<Self::Item> {
        while !self.buffer.is_ready() {
            match self.iter.next() {
                Some(ch) => {
                    decompose(ch, &mut self.buffer);
                }
                None => {
                    if self.buffer.is_empty() {
                        return None;
                    } else {
                        self.buffer.finish();

                        break;
                    }
                }
            }
        }

        Some(self.buffer.pop())
    }

    fn size_hint(&self) -> (usize, Option<usize>) {
        let (lower, _) = self.iter.size_hint();

        (lower, None)
    }
}

// Constants from Unicode 9.0.0 Section 3.12 Conjoining Jamo Behavior
// http://www.unicode.org/versions/Unicode9.0.0/ch03.pdf#M9.32468.Heading.310.Combining.Jamo.Behavior
const S_BASE: u32 = 0xAC00;
const L_BASE: u32 = 0x1100;
const V_BASE: u32 = 0x1161;
const T_BASE: u32 = 0x11A7;
const L_COUNT: u32 = 19;
const V_COUNT: u32 = 21;
const T_COUNT: u32 = 28;
const N_COUNT: u32 = V_COUNT * T_COUNT;
const S_COUNT: u32 = L_COUNT * N_COUNT;

#[inline(always)]
fn decompose(c: char, buffer: &mut Buffer) {
    // 7-bit ASCII never decomposes
    if c <= '\x7f' {
        buffer.push(c, 0);
        return;
    }

    // Perform decomposition for Hangul
    if is_hangul_syllable(c) {
        decompose_hangul(c, buffer);
        return;
    }

    if let Some(decomposed) = table::lookup(c).decomposition() {
        for d in decomposed {
            let c = d.char();

            if d.has_decompositions() {
                decompose(c, buffer);
            } else {
                buffer.push(c, d.combining_class());
            }
        }
        return;
    }

    // Finally bottom out.
    buffer.push_back(c);
}

#[inline(always)]
fn is_hangul_syllable(c: char) -> bool {
    (c as u32) >= S_BASE && (c as u32) < (S_BASE + S_COUNT)
}

#[inline(always)]
#[allow(unsafe_code)]
fn decompose_hangul(s: char, buffer: &mut Buffer) {
    let s_index = s as u32 - S_BASE;
    let l_index = s_index / N_COUNT;

    unsafe {
        buffer.push_back(char::from_u32_unchecked(L_BASE + l_index));

        let v_index = (s_index % N_COUNT) / T_COUNT;
        buffer.push_back(char::from_u32_unchecked(V_BASE + v_index));

        let t_index = s_index % T_COUNT;
        if t_index > 0 {
            buffer.push_back(char::from_u32_unchecked(T_BASE + t_index));
        }
    }
}

#[cfg(test)]
mod tests {
    use std::fs::File;
    use std::io::{BufRead, BufReader};

    use super::*;

    #[test]
    fn test_unicode_normalization() {
        let data = File::open("data/NormalizationTest.txt")
            .map(BufReader::new)
            .expect("unicode normalization test file");

        #[derive(Default)]
        struct Entry {
            source: String,
            nfc: String,
            nfd: String,
            nfkc: String,
            nfkd: String,
            comment: String,
        }

        for (i, line) in data.lines().enumerate() {
            let line = line.expect("line");

            if line.is_empty() {
                continue;
            }

            if line.starts_with(['#', '@']) {
                continue;
            }

            let entry =
                line.splitn(6, ';')
                    .enumerate()
                    .fold(Entry::default(), |mut entry, (i, string)| {
                        match i {
                            0 => {
                                entry.source = string
                                    .split(' ')
                                    .map(|v| {
                                        u32::from_str_radix(v, 16).expect("valid u32 value as hex")
                                    })
                                    .map(|v| char::from_u32(v).expect("valid char"))
                                    .collect::<String>()
                            }
                            1 => {
                                entry.nfc = string
                                    .split(' ')
                                    .map(|v| {
                                        u32::from_str_radix(v, 16).expect("valid u32 value as hex")
                                    })
                                    .map(|v| char::from_u32(v).expect("valid char"))
                                    .collect::<String>()
                            }
                            2 => {
                                entry.nfd = string
                                    .split(' ')
                                    .map(|v| {
                                        u32::from_str_radix(v, 16).expect("valid u32 value as hex")
                                    })
                                    .map(|v| char::from_u32(v).expect("valid char"))
                                    .collect::<String>()
                            }
                            3 => {
                                entry.nfkc = string
                                    .split(' ')
                                    .map(|v| {
                                        u32::from_str_radix(v, 16).expect("valid u32 value as hex")
                                    })
                                    .map(|v| char::from_u32(v).expect("valid char"))
                                    .collect::<String>()
                            }
                            4 => {
                                entry.nfkd = string
                                    .split(' ')
                                    .map(|v| {
                                        u32::from_str_radix(v, 16).expect("valid u32 value as hex")
                                    })
                                    .map(|v| char::from_u32(v).expect("valid char"))
                                    .collect::<String>()
                            }
                            5 => {
                                entry.comment =
                                    string.trim_start_matches(['#', ' ']).trim_end().to_string()
                            }
                            _ => unreachable!(),
                        }

                        entry
                    });

            // c3 ==  toNFD(c1) ==  toNFD(c2) ==  toNFD(c3)
            similar_asserts::assert_str_eq!(
                nfd(&entry.source).collect::<String>(),
                entry.nfd,
                "c3 ==  toNFD(c1) at line {} # {}",
                i + 1,
                entry.comment
            );
            similar_asserts::assert_str_eq!(
                nfd(&entry.nfc).collect::<String>(),
                entry.nfd,
                "c3 ==  toNFD(c2) at line {} # {}",
                i + 1,
                entry.comment
            );
            similar_asserts::assert_str_eq!(
                nfd(&entry.nfd).collect::<String>(),
                entry.nfd,
                "c3 ==  toNFD(c3) at line {} # {}",
                i + 1,
                entry.comment
            );

            // c5 ==  toNFD(c4) ==  toNFD(c5)
            similar_asserts::assert_str_eq!(
                nfd(&entry.nfkc).collect::<String>(),
                entry.nfkd,
                "c5 ==  toNFD(c4) at line {} # {}",
                i + 1,
                entry.comment
            );
            similar_asserts::assert_str_eq!(
                nfd(&entry.nfkd).collect::<String>(),
                entry.nfkd,
                "c5 ==  toNFD(c5) at line {} # {}",
                i + 1,
                entry.comment
            );
        }
    }
}
