// TODO: simplify/rewrite

use std::fmt::Write;

use ttf_parser::{cff, GlyphId, CFFError, Rect};

struct Builder(String);
impl ttf_parser::OutlineBuilder for Builder {
    fn move_to(&mut self, x: f32, y: f32) {
        write!(&mut self.0, "M {} {} ", x, y).unwrap();
    }

    fn line_to(&mut self, x: f32, y: f32) {
        write!(&mut self.0, "L {} {} ", x, y).unwrap();
    }

    fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
        write!(&mut self.0, "Q {} {} {} {} ", x1, y1, x, y).unwrap();
    }

    fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
        write!(&mut self.0, "C {} {} {} {} {} {} ", x1, y1, x2, y2, x, y).unwrap();
    }

    fn close(&mut self) {
        write!(&mut self.0, "Z ").unwrap();
    }
}

#[allow(dead_code)]
mod operator {
    pub const HORIZONTAL_STEM: u8           = 1;
    pub const VERTICAL_STEM: u8             = 3;
    pub const VERTICAL_MOVE_TO: u8          = 4;
    pub const LINE_TO: u8                   = 5;
    pub const HORIZONTAL_LINE_TO: u8        = 6;
    pub const VERTICAL_LINE_TO: u8          = 7;
    pub const CURVE_TO: u8                  = 8;
    pub const CALL_LOCAL_SUBROUTINE: u8     = 10;
    pub const RETURN: u8                    = 11;
    pub const ENDCHAR: u8                   = 14;
    pub const HORIZONTAL_STEM_HINT_MASK: u8 = 18;
    pub const HINT_MASK: u8                 = 19;
    pub const COUNTER_MASK: u8              = 20;
    pub const MOVE_TO: u8                   = 21;
    pub const HORIZONTAL_MOVE_TO: u8        = 22;
    pub const VERTICAL_STEM_HINT_MASK: u8   = 23;
    pub const CURVE_LINE: u8                = 24;
    pub const LINE_CURVE: u8                = 25;
    pub const VV_CURVE_TO: u8               = 26;
    pub const HH_CURVE_TO: u8               = 27;
    pub const SHORT_INT: u8                 = 28;
    pub const CALL_GLOBAL_SUBROUTINE: u8    = 29;
    pub const VH_CURVE_TO: u8               = 30;
    pub const HV_CURVE_TO: u8               = 31;
    pub const HFLEX: u8                     = 34;
    pub const FLEX: u8                      = 35;
    pub const HFLEX1: u8                    = 36;
    pub const FLEX1: u8                     = 37;
    pub const FIXED_16_16: u8               = 255;
}

#[allow(dead_code)]
mod top_dict_operator {
    pub const CHARSET_OFFSET: u16               = 15;
    pub const CHAR_STRINGS_OFFSET: u16          = 17;
    pub const PRIVATE_DICT_SIZE_AND_OFFSET: u16 = 18;
    pub const ROS: u16                          = 1230;
    pub const FD_ARRAY: u16                     = 1236;
    pub const FD_SELECT: u16                    = 1237;
}

mod private_dict_operator {
    pub const LOCAL_SUBROUTINES_OFFSET: u16 = 19;
}

#[allow(dead_code)]
#[derive(Clone, Copy)]
enum TtfType {
    Raw(&'static [u8]),
    TrueTypeMagic,
    OpenTypeMagic,
    FontCollectionMagic,
    Int8(i8),
    UInt8(u8),
    Int16(i16),
    UInt16(u16),
    Int32(i32),
    UInt32(u32),
    CFFInt(i32),
}

use TtfType::*;

fn convert(values: &[TtfType]) -> Vec<u8> {
    let mut data = Vec::with_capacity(256);
    for v in values {
        convert_type(*v, &mut data);
    }

    data
}

fn convert_type(value: TtfType, data: &mut Vec<u8>) {
    match value {
        TtfType::Raw(bytes) => {
            data.extend_from_slice(bytes);
        }
        TtfType::TrueTypeMagic => {
            data.extend_from_slice(&[0x00, 0x01, 0x00, 0x00]);
        }
        TtfType::OpenTypeMagic => {
            data.extend_from_slice(&[0x4F, 0x54, 0x54, 0x4F]);
        }
        TtfType::FontCollectionMagic => {
            data.extend_from_slice(&[0x74, 0x74, 0x63, 0x66]);
        }
        TtfType::Int8(n) => {
            data.extend_from_slice(&i8::to_be_bytes(n));
        }
        TtfType::UInt8(n) => {
            data.extend_from_slice(&u8::to_be_bytes(n));
        }
        TtfType::Int16(n) => {
            data.extend_from_slice(&i16::to_be_bytes(n));
        }
        TtfType::UInt16(n) => {
            data.extend_from_slice(&u16::to_be_bytes(n));
        }
        TtfType::Int32(n) => {
            data.extend_from_slice(&i32::to_be_bytes(n));
        }
        TtfType::UInt32(n) => {
            data.extend_from_slice(&u32::to_be_bytes(n));
        }
        TtfType::CFFInt(n) => {
            match n {
                -107..=107 => {
                    data.push((n as i16 + 139) as u8);
                }
                108..=1131 => {
                    let n = n - 108;
                    data.push(((n >> 8) + 247) as u8);
                    data.push((n & 0xFF) as u8);
                }
                -1131..=-108 => {
                    let n = -n - 108;
                    data.push(((n >> 8) + 251) as u8);
                    data.push((n & 0xFF) as u8);
                }
                -32768..=32767 => {
                    data.push(28);
                    data.extend_from_slice(&i16::to_be_bytes(n as i16));
                }
                _ => {
                    data.push(29);
                    data.extend_from_slice(&i32::to_be_bytes(n));
                }
            }
        }
    }
}

#[derive(Debug)]
struct Writer {
    data: Vec<u8>,
}

impl Writer {
    fn new() -> Self {
        Writer { data: Vec::with_capacity(256) }
    }

    fn offset(&self) -> usize {
        self.data.len()
    }

    fn write(&mut self, value: TtfType) {
        convert_type(value, &mut self.data);
    }
}


fn gen_cff(
    global_subrs: &[&[TtfType]],
    local_subrs: &[&[TtfType]],
    chars: &[TtfType],
) -> Vec<u8> {
    fn gen_global_subrs(subrs: &[&[TtfType]]) -> Vec<u8> {
        let mut w = Writer::new();
        for v1 in subrs {
            for v2 in v1.iter() {
                w.write(*v2);
            }
        }
        w.data
    }

    fn gen_local_subrs(subrs: &[&[TtfType]]) -> Vec<u8> {
        let mut w = Writer::new();
        for v1 in subrs {
            for v2 in v1.iter() {
                w.write(*v2);
            }
        }
        w.data
    }

    const EMPTY_INDEX_SIZE: usize = 2;
    const INDEX_HEADER_SIZE: usize = 5;

    // TODO: support multiple subrs
    assert!(global_subrs.len() <= 1);
    assert!(local_subrs.len() <= 1);

    let global_subrs_data = gen_global_subrs(global_subrs);
    let local_subrs_data = gen_local_subrs(local_subrs);
    let chars_data = convert(chars);

    assert!(global_subrs_data.len() < 255);
    assert!(local_subrs_data.len() < 255);
    assert!(chars_data.len() < 255);

    let mut w = Writer::new();
    // Header
    w.write(UInt8(1)); // major version
    w.write(UInt8(0)); // minor version
    w.write(UInt8(4)); // header size
    w.write(UInt8(0)); // absolute offset

    // Name INDEX
    w.write(UInt16(0)); // count

    // Top DICT
    // INDEX
    w.write(UInt16(1)); // count
    w.write(UInt8(1)); // offset size
    w.write(UInt8(1)); // index[0]

    let top_dict_idx2 = if local_subrs.is_empty() { 3 } else { 6 };
    w.write(UInt8(top_dict_idx2)); // index[1]
    // Item 0
    let mut charstr_offset = w.offset() + 2;
    charstr_offset += EMPTY_INDEX_SIZE; // String INDEX

    // Global Subroutines INDEX
    if !global_subrs_data.is_empty() {
        charstr_offset += INDEX_HEADER_SIZE + global_subrs_data.len();
    } else {
        charstr_offset += EMPTY_INDEX_SIZE;
    }

    if !local_subrs_data.is_empty() {
        charstr_offset += 3;
    }

    w.write(CFFInt(charstr_offset as i32));
    w.write(UInt8(top_dict_operator::CHAR_STRINGS_OFFSET as u8));

    if !local_subrs_data.is_empty() {
        // Item 1
        w.write(CFFInt(2)); // length
        w.write(CFFInt((charstr_offset + INDEX_HEADER_SIZE + chars_data.len()) as i32)); // offset
        w.write(UInt8(top_dict_operator::PRIVATE_DICT_SIZE_AND_OFFSET as u8));
    }

    // String INDEX
    w.write(UInt16(0)); // count

    // Global Subroutines INDEX
    if global_subrs_data.is_empty() {
        w.write(UInt16(0)); // count
    } else {
        w.write(UInt16(1)); // count
        w.write(UInt8(1)); // offset size
        w.write(UInt8(1)); // index[0]
        w.write(UInt8(global_subrs_data.len() as u8 + 1)); // index[1]
        w.data.extend_from_slice(&global_subrs_data);
    }

    // CharString INDEX
    w.write(UInt16(1)); // count
    w.write(UInt8(1)); // offset size
    w.write(UInt8(1)); // index[0]
    w.write(UInt8(chars_data.len() as u8 + 1)); // index[1]
    w.data.extend_from_slice(&chars_data);

    if !local_subrs_data.is_empty() {
        // The local subroutines offset is relative to the beginning of the Private DICT data.

        // Private DICT
        w.write(CFFInt(2));
        w.write(UInt8(private_dict_operator::LOCAL_SUBROUTINES_OFFSET as u8));

        // Local Subroutines INDEX
        w.write(UInt16(1)); // count
        w.write(UInt8(1)); // offset size
        w.write(UInt8(1)); // index[0]
        w.write(UInt8(local_subrs_data.len() as u8 + 1)); // index[1]
        w.data.extend_from_slice(&local_subrs_data);
    }

    w.data
}

#[test]
fn unsupported_version() {
    let data = convert(&[
        UInt8(10), // major version, only 1 is supported
        UInt8(0), // minor version
        UInt8(4), // header size
        UInt8(0), // absolute offset
    ]);

    assert!(cff::Table::parse(&data).is_none());
}

#[test]
fn non_default_header_size() {
    let data = convert(&[
        // Header
        UInt8(1), // major version
        UInt8(0), // minor version
        UInt8(8), // header size
        UInt8(0), // absolute offset

        // no-op, should be skipped
        UInt8(0),
        UInt8(0),
        UInt8(0),
        UInt8(0),

        // Name INDEX
        UInt16(0), // count

        // Top DICT
        // INDEX
        UInt16(1), // count
        UInt8(1), // offset size
        UInt8(1), // index[0]
        UInt8(3), // index[1]
        // Data
        CFFInt(21),
        UInt8(top_dict_operator::CHAR_STRINGS_OFFSET as u8),

        // String INDEX
        UInt16(0), // count

        // Global Subroutines INDEX
        UInt16(0), // count

        // CharString INDEX
        UInt16(1), // count
        UInt8(1), // offset size
        UInt8(1), // index[0]
        UInt8(4), // index[1]
        // Data
        CFFInt(10),
        UInt8(operator::HORIZONTAL_MOVE_TO),
        UInt8(operator::ENDCHAR),
    ]);

    let table = cff::Table::parse(&data).unwrap();
    let mut builder = Builder(String::new());
    let rect = table.outline(GlyphId(0), &mut builder).unwrap();

    assert_eq!(builder.0, "M 10 0 Z ");
    assert_eq!(rect, Rect { x_min: 10, y_min: 0, x_max: 10, y_max: 0 });
}

fn rect(x_min: i16, y_min: i16, x_max: i16, y_max: i16) -> Rect {
    Rect { x_min, y_min, x_max, y_max }
}

macro_rules! test_cs_with_subrs {
    ($name:ident, $glob:expr, $loc:expr, $values:expr, $path:expr, $rect_res:expr) => {
        #[test]
        fn $name() {
            let data = gen_cff($glob, $loc, $values);
            let table = cff::Table::parse(&data).unwrap();
            let mut builder = Builder(String::new());
            let rect = table.outline(GlyphId(0), &mut builder).unwrap();

            assert_eq!(builder.0, $path);
            assert_eq!(rect, $rect_res);
        }
    };
}

macro_rules! test_cs {
    ($name:ident, $values:expr, $path:expr, $rect_res:expr) => {
        test_cs_with_subrs!($name, &[], &[], $values, $path, $rect_res);
    };
}

macro_rules! test_cs_err {
    ($name:ident, $values:expr, $err:expr) => {
        #[test]
        fn $name() {
            let data = gen_cff(&[], &[], $values);
            let table = cff::Table::parse(&data).unwrap();
            let mut builder = Builder(String::new());
            let res = table.outline(GlyphId(0), &mut builder);
            assert_eq!(res.unwrap_err(), $err);
        }
    };
}

test_cs!(move_to, &[
    CFFInt(10), CFFInt(20), UInt8(operator::MOVE_TO),
    UInt8(operator::ENDCHAR),
], "M 10 20 Z ",
    rect(10, 20, 10, 20)
);

test_cs!(move_to_with_width, &[
    CFFInt(5), CFFInt(10), CFFInt(20), UInt8(operator::MOVE_TO),
    UInt8(operator::ENDCHAR),
], "M 10 20 Z ",
    rect(10, 20, 10, 20)
);

test_cs!(hmove_to, &[
    CFFInt(10), UInt8(operator::HORIZONTAL_MOVE_TO),
    UInt8(operator::ENDCHAR),
], "M 10 0 Z ",
    rect(10, 0, 10, 0)
);

test_cs!(hmove_to_with_width, &[
    CFFInt(10), CFFInt(20), UInt8(operator::HORIZONTAL_MOVE_TO),
    UInt8(operator::ENDCHAR),
], "M 20 0 Z ",
    rect(20, 0, 20, 0)
);

test_cs!(vmove_to, &[
    CFFInt(10), UInt8(operator::VERTICAL_MOVE_TO),
    UInt8(operator::ENDCHAR),
], "M 0 10 Z ",
    rect(0, 10, 0, 10)
);

test_cs!(vmove_to_with_width, &[
    CFFInt(10), CFFInt(20), UInt8(operator::VERTICAL_MOVE_TO),
    UInt8(operator::ENDCHAR),
], "M 0 20 Z ",
    rect(0, 20, 0, 20)
);

test_cs!(line_to, &[
    CFFInt(10), CFFInt(20), UInt8(operator::MOVE_TO),
    CFFInt(30), CFFInt(40), UInt8(operator::LINE_TO),
    UInt8(operator::ENDCHAR),
], "M 10 20 L 40 60 Z ",
    rect(10, 20, 40, 60)
);

test_cs!(line_to_with_multiple_pairs, &[
    CFFInt(10), CFFInt(20), UInt8(operator::MOVE_TO),
    CFFInt(30), CFFInt(40), CFFInt(50), CFFInt(60), UInt8(operator::LINE_TO),
    UInt8(operator::ENDCHAR),
], "M 10 20 L 40 60 L 90 120 Z ",
    rect(10, 20, 90, 120)
);

test_cs!(hline_to, &[
    CFFInt(10), CFFInt(20), UInt8(operator::MOVE_TO),
    CFFInt(30), UInt8(operator::HORIZONTAL_LINE_TO),
    UInt8(operator::ENDCHAR),
], "M 10 20 L 40 20 Z ",
    rect(10, 20, 40, 20)
);

test_cs!(hline_to_with_two_coords, &[
    CFFInt(10), CFFInt(20), UInt8(operator::MOVE_TO),
    CFFInt(30), CFFInt(40), UInt8(operator::HORIZONTAL_LINE_TO),
    UInt8(operator::ENDCHAR),
], "M 10 20 L 40 20 L 40 60 Z ",
    rect(10, 20, 40, 60)
);

test_cs!(hline_to_with_three_coords, &[
    CFFInt(10), CFFInt(20), UInt8(operator::MOVE_TO),
    CFFInt(30), CFFInt(40), CFFInt(50), UInt8(operator::HORIZONTAL_LINE_TO),
    UInt8(operator::ENDCHAR),
], "M 10 20 L 40 20 L 40 60 L 90 60 Z ",
    rect(10, 20, 90, 60)
);

test_cs!(vline_to, &[
    CFFInt(10), CFFInt(20), UInt8(operator::MOVE_TO),
    CFFInt(30), UInt8(operator::VERTICAL_LINE_TO),
    UInt8(operator::ENDCHAR),
], "M 10 20 L 10 50 Z ",
    rect(10, 20, 10, 50)
);

test_cs!(vline_to_with_two_coords, &[
    CFFInt(10), CFFInt(20), UInt8(operator::MOVE_TO),
    CFFInt(30), CFFInt(40), UInt8(operator::VERTICAL_LINE_TO),
    UInt8(operator::ENDCHAR),
], "M 10 20 L 10 50 L 50 50 Z ",
    rect(10, 20, 50, 50)
);

test_cs!(vline_to_with_three_coords, &[
    CFFInt(10), CFFInt(20), UInt8(operator::MOVE_TO),
    CFFInt(30), CFFInt(40), CFFInt(50), UInt8(operator::VERTICAL_LINE_TO),
    UInt8(operator::ENDCHAR),
], "M 10 20 L 10 50 L 50 50 L 50 100 Z ",
    rect(10, 20, 50, 100)
);

test_cs!(curve_to, &[
    CFFInt(10), CFFInt(20), UInt8(operator::MOVE_TO),
    CFFInt(30), CFFInt(40), CFFInt(50), CFFInt(60), CFFInt(70), CFFInt(80),
    UInt8(operator::CURVE_TO),
    UInt8(operator::ENDCHAR),
], "M 10 20 C 40 60 90 120 160 200 Z ",
    rect(10, 20, 160, 200)
);

test_cs!(curve_to_with_two_sets_of_coords, &[
    CFFInt(10), CFFInt(20), UInt8(operator::MOVE_TO),
    CFFInt(30), CFFInt(40), CFFInt(50), CFFInt(60), CFFInt(70), CFFInt(80),
    CFFInt(90), CFFInt(100), CFFInt(110), CFFInt(120), CFFInt(130), CFFInt(140),
    UInt8(operator::CURVE_TO),
    UInt8(operator::ENDCHAR),
], "M 10 20 C 40 60 90 120 160 200 C 250 300 360 420 490 560 Z ",
    rect(10, 20, 490, 560)
);

test_cs!(hh_curve_to, &[
    CFFInt(10), CFFInt(20), UInt8(operator::MOVE_TO),
    CFFInt(30), CFFInt(40), CFFInt(50), CFFInt(60), UInt8(operator::HH_CURVE_TO),
    UInt8(operator::ENDCHAR),
], "M 10 20 C 40 20 80 70 140 70 Z ",
    rect(10, 20, 140, 70)
);

test_cs!(hh_curve_to_with_y, &[
    CFFInt(10), CFFInt(20), UInt8(operator::MOVE_TO),
    CFFInt(30), CFFInt(40), CFFInt(50), CFFInt(60), CFFInt(70), UInt8(operator::HH_CURVE_TO),
    UInt8(operator::ENDCHAR),
], "M 10 20 C 50 50 100 110 170 110 Z ",
    rect(10, 20, 170, 110)
);

test_cs!(vv_curve_to, &[
    CFFInt(10), CFFInt(20), UInt8(operator::MOVE_TO),
    CFFInt(30), CFFInt(40), CFFInt(50), CFFInt(60), UInt8(operator::VV_CURVE_TO),
    UInt8(operator::ENDCHAR),
], "M 10 20 C 10 50 50 100 50 160 Z ",
    rect(10, 20, 50, 160)
);

test_cs!(vv_curve_to_with_x, &[
    CFFInt(10), CFFInt(20), UInt8(operator::MOVE_TO),
    CFFInt(30), CFFInt(40), CFFInt(50), CFFInt(60), CFFInt(70), UInt8(operator::VV_CURVE_TO),
    UInt8(operator::ENDCHAR),
], "M 10 20 C 40 60 90 120 90 190 Z ",
    rect(10, 20, 90, 190)
);

#[test]
fn only_endchar() {
    let data = gen_cff(&[], &[], &[UInt8(operator::ENDCHAR)]);
    let table = cff::Table::parse(&data).unwrap();
    let mut builder = Builder(String::new());
    assert!(table.outline(GlyphId(0), &mut builder).is_err());
}

test_cs_with_subrs!(local_subr,
    &[],
    &[&[
        CFFInt(30),
        CFFInt(40),
        UInt8(operator::LINE_TO),
        UInt8(operator::RETURN),
    ]],
    &[
        CFFInt(10),
        UInt8(operator::HORIZONTAL_MOVE_TO),
        CFFInt(0 - 107), // subr index - subr bias
        UInt8(operator::CALL_LOCAL_SUBROUTINE),
        UInt8(operator::ENDCHAR),
    ],
    "M 10 0 L 40 40 Z ",
    rect(10, 0, 40, 40)
);

test_cs_with_subrs!(endchar_in_subr,
    &[],
    &[&[
        CFFInt(30),
        CFFInt(40),
        UInt8(operator::LINE_TO),
        UInt8(operator::ENDCHAR),
    ]],
    &[
        CFFInt(10),
        UInt8(operator::HORIZONTAL_MOVE_TO),
        CFFInt(0 - 107), // subr index - subr bias
        UInt8(operator::CALL_LOCAL_SUBROUTINE),
    ],
    "M 10 0 L 40 40 Z ",
    rect(10, 0, 40, 40)
);

test_cs_with_subrs!(global_subr,
    &[&[
        CFFInt(30),
        CFFInt(40),
        UInt8(operator::LINE_TO),
        UInt8(operator::RETURN),
    ]],
    &[],
    &[
        CFFInt(10),
        UInt8(operator::HORIZONTAL_MOVE_TO),
        CFFInt(0 - 107), // subr index - subr bias
        UInt8(operator::CALL_GLOBAL_SUBROUTINE),
        UInt8(operator::ENDCHAR),
    ],
    "M 10 0 L 40 40 Z ",
    rect(10, 0, 40, 40)
);

test_cs_err!(reserved_operator, &[
    CFFInt(10), UInt8(2),
    UInt8(operator::ENDCHAR),
], CFFError::InvalidOperator);

test_cs_err!(line_to_without_move_to, &[
    CFFInt(10), CFFInt(20), UInt8(operator::LINE_TO),
    UInt8(operator::ENDCHAR),
], CFFError::MissingMoveTo);

// Width must be set only once.
test_cs_err!(two_vmove_to_with_width, &[
    CFFInt(10), CFFInt(20), UInt8(operator::VERTICAL_MOVE_TO),
    CFFInt(10), CFFInt(20), UInt8(operator::VERTICAL_MOVE_TO),
    UInt8(operator::ENDCHAR),
], CFFError::InvalidArgumentsStackLength);

test_cs_err!(move_to_with_too_many_coords, &[
    CFFInt(10), CFFInt(10), CFFInt(10), CFFInt(20), UInt8(operator::MOVE_TO),
    UInt8(operator::ENDCHAR),
], CFFError::InvalidArgumentsStackLength);

test_cs_err!(move_to_with_not_enought_coords, &[
    CFFInt(10), UInt8(operator::MOVE_TO),
    UInt8(operator::ENDCHAR),
], CFFError::InvalidArgumentsStackLength);

test_cs_err!(hmove_to_with_too_many_coords, &[
    CFFInt(10), CFFInt(10), CFFInt(10), UInt8(operator::HORIZONTAL_MOVE_TO),
    UInt8(operator::ENDCHAR),
], CFFError::InvalidArgumentsStackLength);

test_cs_err!(hmove_to_with_not_enought_coords, &[
    UInt8(operator::HORIZONTAL_MOVE_TO),
    UInt8(operator::ENDCHAR),
], CFFError::InvalidArgumentsStackLength);

test_cs_err!(vmove_to_with_too_many_coords, &[
    CFFInt(10), CFFInt(10), CFFInt(10), UInt8(operator::VERTICAL_MOVE_TO),
    UInt8(operator::ENDCHAR),
], CFFError::InvalidArgumentsStackLength);

test_cs_err!(vmove_to_with_not_enought_coords, &[
    UInt8(operator::VERTICAL_MOVE_TO),
    UInt8(operator::ENDCHAR),
], CFFError::InvalidArgumentsStackLength);

test_cs_err!(line_to_with_single_coord, &[
    CFFInt(10), CFFInt(20), UInt8(operator::MOVE_TO),
    CFFInt(30), UInt8(operator::LINE_TO),
    UInt8(operator::ENDCHAR),
], CFFError::InvalidArgumentsStackLength);

test_cs_err!(line_to_with_odd_number_of_coord, &[
    CFFInt(10), CFFInt(20), UInt8(operator::MOVE_TO),
    CFFInt(30), CFFInt(40), CFFInt(50), UInt8(operator::LINE_TO),
    UInt8(operator::ENDCHAR),
], CFFError::InvalidArgumentsStackLength);

test_cs_err!(hline_to_without_coords, &[
    CFFInt(10), CFFInt(20), UInt8(operator::MOVE_TO),
    UInt8(operator::HORIZONTAL_LINE_TO),
    UInt8(operator::ENDCHAR),
], CFFError::InvalidArgumentsStackLength);

test_cs_err!(vline_to_without_coords, &[
    CFFInt(10), CFFInt(20), UInt8(operator::MOVE_TO),
    UInt8(operator::VERTICAL_LINE_TO),
    UInt8(operator::ENDCHAR),
], CFFError::InvalidArgumentsStackLength);

test_cs_err!(curve_to_with_invalid_num_of_coords_1, &[
    CFFInt(10), CFFInt(20), UInt8(operator::MOVE_TO),
    CFFInt(30), CFFInt(40), CFFInt(50), CFFInt(60), UInt8(operator::CURVE_TO),
    UInt8(operator::ENDCHAR),
], CFFError::InvalidArgumentsStackLength);

test_cs_err!(curve_to_with_invalid_num_of_coords_2, &[
    CFFInt(10), CFFInt(20), UInt8(operator::MOVE_TO),
    CFFInt(30), CFFInt(40), CFFInt(50), CFFInt(60), CFFInt(70), CFFInt(80), CFFInt(90),
    UInt8(operator::CURVE_TO),
    UInt8(operator::ENDCHAR),
], CFFError::InvalidArgumentsStackLength);

test_cs_err!(hh_curve_to_with_not_enought_coords, &[
    CFFInt(10), CFFInt(20), UInt8(operator::MOVE_TO),
    CFFInt(30), CFFInt(40), CFFInt(50), UInt8(operator::HH_CURVE_TO),
    UInt8(operator::ENDCHAR),
], CFFError::InvalidArgumentsStackLength);

test_cs_err!(hh_curve_to_with_too_many_coords, &[
    CFFInt(10), CFFInt(20), UInt8(operator::MOVE_TO),
    CFFInt(30), CFFInt(40), CFFInt(50), CFFInt(30), CFFInt(40), CFFInt(50),
    UInt8(operator::HH_CURVE_TO),
    UInt8(operator::ENDCHAR),
], CFFError::InvalidArgumentsStackLength);

test_cs_err!(vv_curve_to_with_not_enought_coords, &[
    CFFInt(10), CFFInt(20), UInt8(operator::MOVE_TO),
    CFFInt(30), CFFInt(40), CFFInt(50), UInt8(operator::VV_CURVE_TO),
    UInt8(operator::ENDCHAR),
], CFFError::InvalidArgumentsStackLength);

test_cs_err!(vv_curve_to_with_too_many_coords, &[
    CFFInt(10), CFFInt(20), UInt8(operator::MOVE_TO),
    CFFInt(30), CFFInt(40), CFFInt(50), CFFInt(30), CFFInt(40), CFFInt(50),
    UInt8(operator::VV_CURVE_TO),
    UInt8(operator::ENDCHAR),
], CFFError::InvalidArgumentsStackLength);

test_cs_err!(multiple_endchar, &[
    UInt8(operator::ENDCHAR),
    UInt8(operator::ENDCHAR),
], CFFError::DataAfterEndChar);

test_cs_err!(seac_with_not_enough_data, &[
    CFFInt(0),
    CFFInt(0),
    CFFInt(0),
    CFFInt(0),
    UInt8(operator::ENDCHAR),
], CFFError::NestingLimitReached);

test_cs_err!(operands_overflow, &[
    CFFInt(0), CFFInt(1), CFFInt(2), CFFInt(3), CFFInt(4), CFFInt(5), CFFInt(6), CFFInt(7), CFFInt(8), CFFInt(9),
    CFFInt(0), CFFInt(1), CFFInt(2), CFFInt(3), CFFInt(4), CFFInt(5), CFFInt(6), CFFInt(7), CFFInt(8), CFFInt(9),
    CFFInt(0), CFFInt(1), CFFInt(2), CFFInt(3), CFFInt(4), CFFInt(5), CFFInt(6), CFFInt(7), CFFInt(8), CFFInt(9),
    CFFInt(0), CFFInt(1), CFFInt(2), CFFInt(3), CFFInt(4), CFFInt(5), CFFInt(6), CFFInt(7), CFFInt(8), CFFInt(9),
    CFFInt(0), CFFInt(1), CFFInt(2), CFFInt(3), CFFInt(4), CFFInt(5), CFFInt(6), CFFInt(7), CFFInt(8), CFFInt(9),
], CFFError::ArgumentsStackLimitReached);

test_cs_err!(operands_overflow_with_4_byte_ints, &[
    CFFInt(30000), CFFInt(30000), CFFInt(30000), CFFInt(30000), CFFInt(30000),
    CFFInt(30000), CFFInt(30000), CFFInt(30000), CFFInt(30000), CFFInt(30000),
    CFFInt(30000), CFFInt(30000), CFFInt(30000), CFFInt(30000), CFFInt(30000),
    CFFInt(30000), CFFInt(30000), CFFInt(30000), CFFInt(30000), CFFInt(30000),
    CFFInt(30000), CFFInt(30000), CFFInt(30000), CFFInt(30000), CFFInt(30000),
    CFFInt(30000), CFFInt(30000), CFFInt(30000), CFFInt(30000), CFFInt(30000),
    CFFInt(30000), CFFInt(30000), CFFInt(30000), CFFInt(30000), CFFInt(30000),
    CFFInt(30000), CFFInt(30000), CFFInt(30000), CFFInt(30000), CFFInt(30000),
    CFFInt(30000), CFFInt(30000), CFFInt(30000), CFFInt(30000), CFFInt(30000),
    CFFInt(30000), CFFInt(30000), CFFInt(30000), CFFInt(30000), CFFInt(30000),
], CFFError::ArgumentsStackLimitReached);

test_cs_err!(bbox_overflow, &[
    CFFInt(32767), UInt8(operator::HORIZONTAL_MOVE_TO),
    CFFInt(32767), UInt8(operator::HORIZONTAL_LINE_TO),
    UInt8(operator::ENDCHAR),
], CFFError::BboxOverflow);

#[test]
fn endchar_in_subr_with_extra_data_1() {
    let data = gen_cff(
        &[],
        &[&[
            CFFInt(30),
            CFFInt(40),
            UInt8(operator::LINE_TO),
            UInt8(operator::ENDCHAR),
        ]],
        &[
            CFFInt(10),
            UInt8(operator::HORIZONTAL_MOVE_TO),
            CFFInt(0 - 107), // subr index - subr bias
            UInt8(operator::CALL_LOCAL_SUBROUTINE),
            CFFInt(30),
            CFFInt(40),
            UInt8(operator::LINE_TO),
        ]
    );

    let table = cff::Table::parse(&data).unwrap();
    let mut builder = Builder(String::new());
    let res = table.outline(GlyphId(0), &mut builder);
    assert_eq!(res.unwrap_err(), CFFError::DataAfterEndChar);
}

#[test]
fn endchar_in_subr_with_extra_data_2() {
    let data = gen_cff(
        &[],
        &[&[
            CFFInt(30),
            CFFInt(40),
            UInt8(operator::LINE_TO),
            UInt8(operator::ENDCHAR),
            CFFInt(30),
            CFFInt(40),
            UInt8(operator::LINE_TO),
        ]],
        &[
            CFFInt(10),
            UInt8(operator::HORIZONTAL_MOVE_TO),
            CFFInt(0 - 107), // subr index - subr bias
            UInt8(operator::CALL_LOCAL_SUBROUTINE),
        ]
    );

    let table = cff::Table::parse(&data).unwrap();
    let mut builder = Builder(String::new());
    let res = table.outline(GlyphId(0), &mut builder);
    assert_eq!(res.unwrap_err(), CFFError::DataAfterEndChar);
}

#[test]
fn subr_without_return() {
    let data = gen_cff(
        &[],
        &[&[
            CFFInt(30),
            CFFInt(40),
            UInt8(operator::LINE_TO),
            UInt8(operator::ENDCHAR),
            CFFInt(30),
            CFFInt(40),
            UInt8(operator::LINE_TO),
        ]],
        &[
            CFFInt(10),
            UInt8(operator::HORIZONTAL_MOVE_TO),
            CFFInt(0 - 107), // subr index - subr bias
            UInt8(operator::CALL_LOCAL_SUBROUTINE),
        ]
    );

    let table = cff::Table::parse(&data).unwrap();
    let mut builder = Builder(String::new());
    let res = table.outline(GlyphId(0), &mut builder);
    assert_eq!(res.unwrap_err(), CFFError::DataAfterEndChar);
}

#[test]
fn recursive_local_subr() {
    let data = gen_cff(
        &[],
        &[&[
            CFFInt(0 - 107), // subr index - subr bias
            UInt8(operator::CALL_LOCAL_SUBROUTINE),
        ]],
        &[
            CFFInt(10),
            UInt8(operator::HORIZONTAL_MOVE_TO),
            CFFInt(0 - 107), // subr index - subr bias
            UInt8(operator::CALL_LOCAL_SUBROUTINE),
        ]
    );

    let table = cff::Table::parse(&data).unwrap();
    let mut builder = Builder(String::new());
    let res = table.outline(GlyphId(0), &mut builder);
    assert_eq!(res.unwrap_err(), CFFError::NestingLimitReached);
}

#[test]
fn recursive_global_subr() {
    let data = gen_cff(
        &[&[
            CFFInt(0 - 107), // subr index - subr bias
            UInt8(operator::CALL_GLOBAL_SUBROUTINE),
        ]],
        &[],
        &[
            CFFInt(10),
            UInt8(operator::HORIZONTAL_MOVE_TO),
            CFFInt(0 - 107), // subr index - subr bias
            UInt8(operator::CALL_GLOBAL_SUBROUTINE),
        ]
    );

    let table = cff::Table::parse(&data).unwrap();
    let mut builder = Builder(String::new());
    let res = table.outline(GlyphId(0), &mut builder);
    assert_eq!(res.unwrap_err(), CFFError::NestingLimitReached);
}

#[test]
fn recursive_mixed_subr() {
    let data = gen_cff(
        &[&[
            CFFInt(0 - 107), // subr index - subr bias
            UInt8(operator::CALL_LOCAL_SUBROUTINE),
        ]],
        &[&[
            CFFInt(0 - 107), // subr index - subr bias
            UInt8(operator::CALL_GLOBAL_SUBROUTINE),
        ]],
        &[
            CFFInt(10),
            UInt8(operator::HORIZONTAL_MOVE_TO),
            CFFInt(0 - 107), // subr index - subr bias
            UInt8(operator::CALL_GLOBAL_SUBROUTINE),
        ]
    );

    let table = cff::Table::parse(&data).unwrap();
    let mut builder = Builder(String::new());
    let res = table.outline(GlyphId(0), &mut builder);
    assert_eq!(res.unwrap_err(), CFFError::NestingLimitReached);
}

#[test]
fn zero_char_string_offset() {
    let data = convert(&[
        // Header
        UInt8(1), // major version
        UInt8(0), // minor version
        UInt8(4), // header size
        UInt8(0), // absolute offset

        // Name INDEX
        UInt16(0), // count

        // Top DICT
        // INDEX
        UInt16(1), // count
        UInt8(1), // offset size
        UInt8(1), // index[0]
        UInt8(3), // index[1]
        // Data
        CFFInt(0), // zero offset!
        UInt8(top_dict_operator::CHAR_STRINGS_OFFSET as u8),
    ]);

    assert!(cff::Table::parse(&data).is_none());
}

#[test]
fn invalid_char_string_offset() {
    let data = convert(&[
        // Header
        UInt8(1), // major version
        UInt8(0), // minor version
        UInt8(4), // header size
        UInt8(0), // absolute offset

        // Name INDEX
        UInt16(0), // count

        // Top DICT
        // INDEX
        UInt16(1), // count
        UInt8(1), // offset size
        UInt8(1), // index[0]
        UInt8(3), // index[1]
        // Data
        CFFInt(2), // invalid offset!
        UInt8(top_dict_operator::CHAR_STRINGS_OFFSET as u8),
    ]);

    assert!(cff::Table::parse(&data).is_none());
}

// TODO: return from main
// TODO: return without endchar
// TODO: data after return
// TODO: recursive subr
// TODO: HORIZONTAL_STEM
// TODO: VERTICAL_STEM
// TODO: HORIZONTAL_STEM_HINT_MASK
// TODO: HINT_MASK
// TODO: COUNTER_MASK
// TODO: VERTICAL_STEM_HINT_MASK
// TODO: CURVE_LINE
// TODO: LINE_CURVE
// TODO: VH_CURVE_TO
// TODO: HFLEX
// TODO: FLEX
// TODO: HFLEX1
// TODO: FLEX1
