use std::iter;

use crate::element::Element;
use crate::table;
use crate::weights::IMPLICIT_WEIGHTS;

#[derive(Default)]
pub struct Collator;

impl Collator {
    fn collation_elements(&self, normalized: &[char]) -> Vec<Element> {
        let debug = true;

        let mut all_elements = Vec::new();

        let mut code_points = normalized.to_vec();
        let mut code_points_len = code_points.len();
        let mut cp_index = 0;

        if debug {
            eprintln!(
                "nfd: {}",
                normalized
                    .iter()
                    .map(|c| format!("{:04X}", *c as u32))
                    .collect::<Vec<_>>()
                    .join(" ")
            );
        }

        while cp_index < code_points_len {
            let (elements, idx) = table::lookup(&code_points[cp_index..]).unwrap_or_default();
            let mut elements = elements.collect::<Vec<_>>();

            if debug {
                eprintln!(
                    "found at 1: [{}], idx: {}, start: {:04X}",
                    elements
                        .iter()
                        .map(ToString::to_string)
                        .collect::<Vec<_>>()
                        .join("]["),
                    idx,
                    code_points[cp_index] as u32
                );
            }

            let s = &code_points[0..cp_index + idx + 1];

            // handle non-starters
            let mut last_class = None;
            let tail_start = cp_index + idx + 1;
            let mut tail_index = tail_start;

            // advance to last combining C
            while tail_index < code_points_len {
                let combining_class =
                    u_norm::table::lookup(code_points[tail_index]).combining_class();

                if debug {
                    eprintln!(
                        "combining class: {}, start: {:04X}",
                        combining_class, code_points[tail_index] as u32
                    );
                }

                if combining_class == 0 {
                    if tail_index != tail_start {
                        tail_index -= 1;
                    }

                    break;
                }

                if let Some(last_class) = last_class {
                    if last_class >= combining_class {
                        if tail_index != tail_start {
                            tail_index -= 1;
                        }

                        break;
                    }
                }

                last_class = Some(combining_class);
                tail_index += 1;
            }

            if tail_index == code_points_len {
                tail_index -= 1;
            }

            if tail_index > tail_start {
                let c = code_points[tail_index];

                let mut new_key = Vec::with_capacity(s.len() + 1);
                new_key.extend_from_slice(s);
                new_key.push(c);

                if debug {
                    eprintln!(
                        "new key: {}, s: {}, c: {:04X}",
                        new_key
                            .iter()
                            .map(|c| format!("{:04X}", *c as u32))
                            .collect::<Vec<_>>()
                            .join(" "),
                        s.iter()
                            .map(|c| format!("{:04X}", *c as u32))
                            .collect::<Vec<_>>()
                            .join(" "),
                        c as u32
                    );
                }

                let (new_elements, new_idx) = table::lookup(&new_key).expect("lookup");
                let new_elements = new_elements.collect::<Vec<_>>();

                if debug {
                    eprintln!(
                        "found at 2: [{}], idx: {}, start: {:04X}",
                        new_elements
                            .iter()
                            .map(ToString::to_string)
                            .collect::<Vec<_>>()
                            .join("]["),
                        new_idx,
                        new_key[0] as u32
                    );
                }

                if new_idx == (new_key.len() - 1) && !new_elements.is_empty() {
                    cp_index = tail_start;

                    // splice
                    let mut tmp = Vec::with_capacity(code_points_len - 1);
                    tmp.extend_from_slice(&code_points[0..tail_index]);

                    if tail_index + 1 < code_points_len {
                        tmp.extend_from_slice(&code_points[tail_index + 1..]);
                    }

                    code_points = tmp;
                    code_points_len = code_points.len();

                    if debug {
                        eprintln!("add part 2 elements to all");
                    }

                    // add elements to final collection
                    all_elements.extend(new_elements);

                    continue;
                }
            }

            if elements.is_empty() {
                if debug {
                    eprintln!("no part 1 elements, use implicit weight");
                }

                elements = self.implicit_weight(code_points[0] as u32);

                if debug {
                    eprintln!(
                        "found at 3: [{}], start: {:04X}",
                        elements
                            .iter()
                            .map(ToString::to_string)
                            .collect::<Vec<_>>()
                            .join("]["),
                        code_points[0] as u32
                    );
                }
            }

            if debug {
                eprintln!("add part 1 elements to all");
            }

            // add elements to final collection
            all_elements.extend(elements);

            cp_index += idx + 1;
        }

        if debug {
            eprintln!(
                "all: [{}]",
                all_elements
                    .iter()
                    .map(ToString::to_string)
                    .collect::<Vec<_>>()
                    .join("][")
            );
        }

        all_elements
    }

    fn implicit_weight(&self, cp: u32) -> Vec<Element> {
        let base;
        let mut aaaa = None;
        let mut bbbb = 0;

        if is_unified_ideograph(cp)
            && ((cp >= 0x4E00 && cp <= 0x9FFF) || (cp >= 0xF900 && cp <= 0xFAFF))
        {
            base = 0xFB40;
            aaaa = Some(base + (cp >> 15));
            bbbb = (cp & 0x7FFF) | 0x8000;
        } else if is_unified_ideograph(cp)
            && !((cp >= 0x4E00 && cp <= 0x9FFF) || (cp >= 0xF900 && cp <= 0xFAFF))
        {
            base = 0xFB80;
            aaaa = Some(base + (cp >> 15));
            bbbb = (cp & 0x7FFF) | 0x8000;
        } else {
            if let Some((start, _, base)) = IMPLICIT_WEIGHTS
                .iter()
                .find(|(start, end, _)| cp >= *start && cp <= *end)
            {
                aaaa = Some(*base);
                bbbb = (cp - *start) | 0x8000;

                if cp >= 0x18D00 && cp <= 0x18D8F {
                    bbbb = (cp - 0x17000) | 0x8000;
                } else {
                    bbbb = (cp - *start) | 0x8000;
                }
            }

            if aaaa.is_none() {
                base = 0xFBC0;
                aaaa = Some(base + (cp >> 15));
                bbbb = (cp & 0x7FFF) | 0x8000;
            }
        }

        vec![
            Element::new(
                aaaa.map(|x| (x & 0xFFFF) as u16).unwrap_or(0),
                0x0020,
                0x0002,
                0x0000,
                false,
            ),
            Element::new((bbbb & 0xFFFF) as u16, 0x0000, 0x0000, 0x0000, false),
        ]
    }

    fn sort_key_from_collation_elements(&self, collation_elements: &[Element]) -> Vec<u16> {
        let l1 = collation_elements
            .iter()
            .map(|element| element.l1())
            .filter(|x| *x > 0);

        let l2 = collation_elements
            .iter()
            .map(|element| element.l2())
            .filter(|x| *x > 0);

        let l3 = collation_elements
            .iter()
            .map(|element| element.l3() as u16)
            .filter(|x| *x > 0);

        let l4 = collation_elements
            .iter()
            .map(|element| element.l4())
            .filter(|x| *x > 0);

        l1.chain(iter::once(0))
            .chain(l2)
            .chain(iter::once(0))
            .chain(l3)
            .chain(iter::once(0))
            .chain(l4)
            .collect()
    }

    pub fn sort_key<S: AsRef<str>>(&self, input: S) -> Vec<u16> {
        let normalized = u_norm::nfd(input.as_ref()).collect::<Vec<_>>();
        let collation_elements = self.collation_elements(&normalized);

        self.sort_key_from_collation_elements(&collation_elements)
    }
}

pub fn is_unified_ideograph(cp: u32) -> bool {
    if cp < 0x3400 || cp > 0x3134a {
        return false;
    }

    match cp {
        0x3400..=0x4dbf => true,
        0x4e00..=0x9fff => true,
        0xfa0e..=0xfa0f => true,
        0xfa11 => true,
        0xfa13..=0xfa14 => true,
        0xfa1f => true,
        0xfa21 => true,
        0xfa23..=0xfa24 => true,
        0xfa27..=0xfa29 => true,
        0x20000..=0x2a6df => true,
        0x2a700..=0x2b738 => true,
        0x2b740..=0x2b81d => true,
        0x2b820..=0x2cea1 => true,
        0x2ceb0..=0x2ebe0 => true,
        0x30000..=0x3134a => true,
        _ => false,
    }
}

#[cfg(test)]
mod tests {
    use crate::fmt;

    use super::*;

    #[test]
    fn test_bug_001() {
        let collator = Collator::default();

        let fixture = "\u{1abc}\u{334}";
        let sort_key = collator.sort_key(fixture);

        similar_asserts::assert_eq!("| 004A 0033 | 0002 0002 |", fmt(&sort_key));

        let fixture = "\u{1ac1}\u{334}";
        let sort_key = collator.sort_key(fixture);

        similar_asserts::assert_eq!("| 004A 0033 | 0002 0002 |", fmt(&sort_key));
    }

    #[test]
    fn test_bug_002() {
        let collator = Collator::default();

        let fixture = "\u{a8}\u{301}\u{334}";
        let sort_key = collator.sort_key(fixture);

        similar_asserts::assert_eq!(
            "04D0 | 0020 004A 0024 | 0002 0002 0002 |",
            fmt(&sort_key),
            "nfd: {:?}",
            u_norm::nfd(fixture)
                .map(|ch| format!("{:04X}", ch as u32))
                .collect::<Vec<_>>()
                .join(" ")
        );
    }

    #[test]
    fn test_bug_003() {
        let collator = Collator::default();

        let fixture = "\u{18d00}\u{21}";
        let sort_key = collator.sort_key(fixture);

        similar_asserts::assert_eq!(
            "FB00 9D00 0268 | 0020 0020 | 0002 0002 |",
            fmt(&sort_key),
            "nfd: {:?}",
            u_norm::nfd(fixture)
                .map(|ch| format!("{:04X}", ch as u32))
                .collect::<Vec<_>>()
                .join(" ")
        );
    }

    #[test]
    fn test_bug_004() {
        // 31B3 0062; # (ㆳ) BOPOMOFO LETTER INNN     [4593 208F | 0020 0119 0020 | 0016 0016 0002 |]

        let collator = Collator::default();

        let fixture = "\u{31b3}\u{62}";
        let sort_key = collator.sort_key(fixture);

        similar_asserts::assert_eq!(
            "4593 208F | 0020 0119 0020 | 0016 0016 0002 |",
            fmt(&sort_key),
            "nfd: {:?}",
            u_norm::nfd(fixture)
                .map(|ch| format!("{:04X}", ch as u32))
                .collect::<Vec<_>>()
                .join(" ")
        );
    }
}
