//! Statische Forms-Typinformation. Namen werden beim Übersetzen in diese
//! Tabellenindizes aufgelöst; die Laufzeit muss keine Strings durchsuchen.

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ObjectClass {
    Form,
    CheckBox,
    ComboBox,
    CommandButton,
    DirListBox,
    DriveListBox,
    FileListBox,
    Frame,
    HScrollBar,
    Label,
    ListBox,
    Menu,
    OptionButton,
    PictureBox,
    TextBox,
    Timer,
    VScrollBar,
    Screen,
    Spin,
}

impl ObjectClass {
    pub const ALL: [Self; 19] = [
        Self::Form,
        Self::CheckBox,
        Self::ComboBox,
        Self::CommandButton,
        Self::DirListBox,
        Self::DriveListBox,
        Self::FileListBox,
        Self::Frame,
        Self::HScrollBar,
        Self::Label,
        Self::ListBox,
        Self::Menu,
        Self::OptionButton,
        Self::PictureBox,
        Self::TextBox,
        Self::Timer,
        Self::VScrollBar,
        Self::Screen,
        Self::Spin,
    ];

    pub fn name(self) -> &'static str {
        match self {
            Self::Form => "FORM",
            Self::CheckBox => "CHECKBOX",
            Self::ComboBox => "COMBOBOX",
            Self::CommandButton => "COMMANDBUTTON",
            Self::DirListBox => "DIRLISTBOX",
            Self::DriveListBox => "DRIVELISTBOX",
            Self::FileListBox => "FILELISTBOX",
            Self::Frame => "FRAME",
            Self::HScrollBar => "HSCROLLBAR",
            Self::Label => "LABEL",
            Self::ListBox => "LISTBOX",
            Self::Menu => "MENU",
            Self::OptionButton => "OPTIONBUTTON",
            Self::PictureBox => "PICTUREBOX",
            Self::TextBox => "TEXTBOX",
            Self::Timer => "TIMER",
            Self::VScrollBar => "VSCROLLBAR",
            Self::Screen => "SCREEN",
            Self::Spin => "SPIN",
        }
    }

    pub fn display_name(self) -> &'static str {
        match self {
            Self::Form => "Form",
            Self::CheckBox => "CheckBox",
            Self::ComboBox => "ComboBox",
            Self::CommandButton => "CommandButton",
            Self::DirListBox => "DirListBox",
            Self::DriveListBox => "DriveListBox",
            Self::FileListBox => "FileListBox",
            Self::Frame => "Frame",
            Self::HScrollBar => "HScrollBar",
            Self::Label => "Label",
            Self::ListBox => "ListBox",
            Self::Menu => "Menu",
            Self::OptionButton => "OptionButton",
            Self::PictureBox => "PictureBox",
            Self::TextBox => "TextBox",
            Self::Timer => "Timer",
            Self::VScrollBar => "VScrollBar",
            Self::Screen => "Screen",
            Self::Spin => "Spin",
        }
    }

    pub fn parse(name: &str) -> Option<Self> {
        Self::ALL
            .into_iter()
            .find(|c| c.name().eq_ignore_ascii_case(name))
    }

    pub fn id(self) -> u8 {
        Self::ALL.iter().position(|c| *c == self).unwrap() as u8
    }

    pub fn from_id(id: u8) -> Option<Self> {
        Self::ALL.get(id as usize).copied()
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PropertyType {
    Integer,
    Single,
    String,
    Boolean,
    Object,
    IntegerArray,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PropertyDefault {
    Integer(i32),
    Single(f32),
    String(&'static str),
    Boolean(bool),
    Empty,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub struct PropertySpec {
    pub name: &'static str,
    pub ty: PropertyType,
    pub default: PropertyDefault,
    pub min: Option<i32>,
    pub max: Option<i32>,
    pub writable: bool,
}

const fn int(name: &'static str, default: i32) -> PropertySpec {
    PropertySpec {
        name,
        ty: PropertyType::Integer,
        default: PropertyDefault::Integer(default),
        min: None,
        max: None,
        writable: true,
    }
}
const fn range(name: &'static str, default: i32, min: i32, max: i32) -> PropertySpec {
    PropertySpec {
        name,
        ty: PropertyType::Integer,
        default: PropertyDefault::Integer(default),
        min: Some(min),
        max: Some(max),
        writable: true,
    }
}
const fn boolp(name: &'static str, default: bool) -> PropertySpec {
    PropertySpec {
        name,
        ty: PropertyType::Boolean,
        default: PropertyDefault::Boolean(default),
        min: None,
        max: None,
        writable: true,
    }
}
const fn string(name: &'static str, default: &'static str) -> PropertySpec {
    PropertySpec {
        name,
        ty: PropertyType::String,
        default: PropertyDefault::String(default),
        min: None,
        max: None,
        writable: true,
    }
}
const fn ro(name: &'static str, ty: PropertyType) -> PropertySpec {
    PropertySpec {
        name,
        ty,
        default: PropertyDefault::Empty,
        min: None,
        max: None,
        writable: false,
    }
}

/// Eigenschaftstabelle in stabiler Reihenfolge. Der Vektor ist klein und wird
/// nur beim Übersetzen bzw. Erzeugen eines Objekts aufgebaut.
pub fn properties(class: ObjectClass) -> Vec<PropertySpec> {
    use ObjectClass::*;
    let mut p = Vec::new();
    if !matches!(class, Timer | Menu | Screen) {
        p.extend([
            range("BACKCOLOR", 7, 0, 15),
            boolp("ENABLED", true),
            range("HEIGHT", 1, 1, 254),
            range("LEFT", 0, 0, 254),
            range("MOUSEPOINTER", 0, 0, 12),
            ro("PARENT", PropertyType::Object),
            string("TAG", ""),
            range("TOP", 0, 0, 254),
            boolp("VISIBLE", true),
            range("WIDTH", 1, 1, 254),
        ]);
    }
    if !matches!(class, Form | Timer | Menu | Screen) {
        p.push(int("INDEX", 0));
        p.push(int("TABINDEX", 0));
    }
    if !matches!(class, Form | Frame | Label | Timer | Menu | Screen) {
        p.push(boolp("TABSTOP", true));
    }
    if !matches!(class, Form | Timer | Menu | Screen) {
        p.push(string("CTLNAME", ""));
    }
    if matches!(
        class,
        Form | TextBox
            | ListBox
            | ComboBox
            | CheckBox
            | OptionButton
            | Label
            | HScrollBar
            | VScrollBar
            | PictureBox
    ) {
        p.push(range("FORECOLOR", 0, 0, 15));
    }
    if matches!(
        class,
        Form | CommandButton
            | TextBox
            | ListBox
            | ComboBox
            | CheckBox
            | OptionButton
            | Frame
            | Label
            | HScrollBar
            | VScrollBar
            | PictureBox
    ) {
        p.push(range("DRAGMODE", 0, 0, 1));
    }
    match class {
        Form => p.extend([
            boolp("AUTOREDRAW", false),
            range("BORDERSTYLE", 2, 0, 6),
            string("CAPTION", ""),
            boolp("CONTROLBOX", true),
            int("CURRENTX", 0),
            int("CURRENTY", 0),
            string("FORMNAME", ""),
            range("FORMTYPE", 0, 0, 1),
            boolp("MAXBUTTON", true),
            boolp("MINBUTTON", true),
            ro("SCALEHEIGHT", PropertyType::Integer),
            ro("SCALEWIDTH", PropertyType::Integer),
            range("WINDOWSTATE", 0, 0, 2),
        ]),
        CommandButton => p.extend([
            boolp("CANCEL", false),
            string("CAPTION", ""),
            boolp("DEFAULT", false),
            boolp("VALUE", false),
        ]),
        TextBox => p.extend([
            range("BORDERSTYLE", 1, 0, 1),
            boolp("MULTILINE", false),
            range("SCROLLBARS", 0, 0, 3),
            int("SELLENGTH", 0),
            int("SELSTART", 0),
            string("SELTEXT", ""),
            string("TEXT", ""),
        ]),
        ListBox => p.extend([
            ro("LIST", PropertyType::String),
            ro("LISTCOUNT", PropertyType::Integer),
            int("LISTINDEX", -1),
            boolp("SORTED", false),
            string("TEXT", ""),
        ]),
        ComboBox => p.extend([
            ro("LIST", PropertyType::String),
            ro("LISTCOUNT", PropertyType::Integer),
            int("LISTINDEX", -1),
            boolp("SORTED", false),
            int("SELLENGTH", 0),
            int("SELSTART", 0),
            string("SELTEXT", ""),
            range("STYLE", 0, 0, 2),
            string("TEXT", ""),
        ]),
        CheckBox => p.extend([string("CAPTION", ""), range("VALUE", 0, 0, 2)]),
        OptionButton => p.extend([string("CAPTION", ""), range("VALUE", 0, -1, 1)]),
        Frame => p.push(string("CAPTION", "")),
        Label => p.extend([
            range("ALIGNMENT", 0, 0, 2),
            boolp("AUTOSIZE", false),
            range("BORDERSTYLE", 0, 0, 2),
            string("CAPTION", ""),
        ]),
        HScrollBar | VScrollBar => p.extend([
            ro("ATTACHED", PropertyType::Boolean),
            range("LARGECHANGE", 1, 1, 32767),
            range("MIN", 0, -32768, 32767),
            range("MAX", 32767, -32768, 32767),
            range("SMALLCHANGE", 1, 1, 32767),
            range("VALUE", 0, -32768, 32767),
        ]),
        Spin => {
            p.extend([
                ro("BORDERSTYLE", PropertyType::Integer),
                range("INTERVAL", 250, 0, 65535),
                range("MIN", 0, -32768, 32767),
                range("MAX", 32767, -32768, 32767),
                range("STYLE", 0, 0, 1),
                range("VALUE", 0, -32768, 32767),
            ]);
            for name in ["HEIGHT", "WIDTH"] {
                if let Some(property) = p.iter_mut().find(|property| property.name == name) {
                    property.writable = false;
                }
            }
        }
        PictureBox => p.extend([
            boolp("AUTOREDRAW", false),
            range("BORDERSTYLE", 1, 0, 2),
            int("CURRENTX", 0),
            int("CURRENTY", 0),
            ro("SCALEHEIGHT", PropertyType::Integer),
            ro("SCALEWIDTH", PropertyType::Integer),
        ]),
        Timer => p.extend([
            string("CTLNAME", ""),
            boolp("ENABLED", true),
            int("INDEX", 0),
            range("INTERVAL", 0, 0, 65535),
            ro("PARENT", PropertyType::Object),
            string("TAG", ""),
        ]),
        Menu => p.extend([
            string("CAPTION", ""),
            boolp("CHECKED", false),
            string("CTLNAME", ""),
            boolp("ENABLED", true),
            int("INDEX", 0),
            ro("PARENT", PropertyType::Object),
            boolp("SEPARATOR", false),
            string("TAG", ""),
            boolp("VISIBLE", true),
            string("SHORTCUT", ""),
        ]),
        DirListBox => p.extend([
            ro("LIST", PropertyType::String),
            ro("LISTCOUNT", PropertyType::Integer),
            int("LISTINDEX", -1),
            string("PATH", ""),
            string("TEXT", ""),
        ]),
        DriveListBox => p.extend([
            ro("LIST", PropertyType::String),
            ro("LISTCOUNT", PropertyType::Integer),
            int("LISTINDEX", -1),
            string("DRIVE", ""),
            string("TEXT", ""),
        ]),
        FileListBox => p.extend([
            ro("LIST", PropertyType::String),
            ro("LISTCOUNT", PropertyType::Integer),
            int("LISTINDEX", -1),
            string("FILENAME", ""),
            string("PATH", ""),
            string("PATTERN", "*.*"),
            boolp("ARCHIVE", true),
            boolp("HIDDEN", false),
            boolp("NORMAL", true),
            boolp("READONLY", true),
            boolp("SYSTEM", false),
            string("TEXT", ""),
        ]),
        Screen => p.extend([
            ro("ACTIVECONTROL", PropertyType::Object),
            ro("ACTIVEFORM", PropertyType::Object),
            boolp("AUTOREDRAW", false),
            PropertySpec {
                name: "CONTROLPANEL",
                ty: PropertyType::IntegerArray,
                default: PropertyDefault::Integer(0),
                min: Some(0),
                max: Some(15),
                writable: true,
            },
            ro("HEIGHT", PropertyType::Integer),
            ro("WIDTH", PropertyType::Integer),
            range("MOUSEPOINTER", 0, 0, 12),
        ]),
    }
    if class == Form {
        if let Some(visible) = p.iter_mut().find(|p| p.name == "VISIBLE") {
            visible.default = PropertyDefault::Boolean(false);
        }
    }
    p
}

pub fn property(class: ObjectClass, name: &str) -> Option<(u16, PropertySpec)> {
    properties(class)
        .into_iter()
        .enumerate()
        .find(|(_, p)| p.name.eq_ignore_ascii_case(name))
        .map(|(i, p)| (i as u16, p))
}

pub fn methods(class: ObjectClass) -> &'static [&'static str] {
    use ObjectClass::*;
    match class {
        Form => &[
            "CLS",
            "DRAG",
            "HIDE",
            "LOAD",
            "MOVE",
            "PRINT",
            "PRINTFORM",
            "REFRESH",
            "SHOW",
            "TEXTHEIGHT",
            "TEXTWIDTH",
            "UNLOAD",
        ],
        ListBox | ComboBox => &[
            "ADDITEM",
            "DRAG",
            "MOVE",
            "REFRESH",
            "REMOVEITEM",
            "SETFOCUS",
        ],
        CommandButton | TextBox | CheckBox | OptionButton => {
            &["DRAG", "MOVE", "REFRESH", "SETFOCUS"]
        }
        Frame | Label | HScrollBar | VScrollBar => &["DRAG", "MOVE", "REFRESH"],
        Spin => &["DRAG", "REFRESH", "SETFOCUS"],
        PictureBox => &[
            "CLS",
            "DRAG",
            "MOVE",
            "PRINT",
            "REFRESH",
            "SETFOCUS",
            "TEXTHEIGHT",
            "TEXTWIDTH",
        ],
        Screen => &["HIDE", "SHOW"],
        _ => &[],
    }
}

pub fn method_is_implemented(class: ObjectClass, name: &str) -> bool {
    methods(class).contains(&name)
}

pub fn method_arity(class: ObjectClass, name: &str) -> Option<(usize, usize)> {
    match (class, name) {
        (ObjectClass::Form, "SHOW") => Some((0, 1)),
        (ObjectClass::Form, "HIDE" | "LOAD" | "UNLOAD")
        | (ObjectClass::Screen, "HIDE" | "SHOW") => Some((0, 0)),
        (_, "REFRESH" | "SETFOCUS" | "CLS" | "PRINTFORM") => Some((0, 0)),
        (_, "DRAG") => Some((1, 1)),
        (_, "MOVE") => Some((2, 4)),
        (ObjectClass::ListBox | ObjectClass::ComboBox, "ADDITEM") => Some((1, 2)),
        (ObjectClass::ListBox | ObjectClass::ComboBox, "REMOVEITEM") => Some((1, 1)),
        (ObjectClass::Form | ObjectClass::PictureBox, "PRINT") => Some((0, usize::MAX)),
        (ObjectClass::Form | ObjectClass::PictureBox, "TEXTWIDTH" | "TEXTHEIGHT") => Some((1, 1)),
        _ => None,
    }
}

pub fn method_return_type(class: ObjectClass, name: &str) -> Option<PropertyType> {
    match (class, name) {
        (ObjectClass::Form | ObjectClass::PictureBox, "TEXTWIDTH" | "TEXTHEIGHT") => {
            Some(PropertyType::Integer)
        }
        _ => None,
    }
}

pub fn events(class: ObjectClass) -> &'static [&'static str] {
    use ObjectClass::*;
    match class {
        Form => &[
            "CLICK",
            "DBLCLICK",
            "DRAGDROP",
            "DRAGOVER",
            "GOTFOCUS",
            "KEYDOWN",
            "KEYPRESS",
            "KEYUP",
            "LOAD",
            "LOSTFOCUS",
            "MOUSEDOWN",
            "MOUSEMOVE",
            "MOUSEUP",
            "PAINT",
            "RESIZE",
            "UNLOAD",
        ],
        CommandButton => &[
            "CLICK",
            "DRAGDROP",
            "DRAGOVER",
            "GOTFOCUS",
            "KEYDOWN",
            "KEYPRESS",
            "KEYUP",
            "LOSTFOCUS",
        ],
        TextBox => &[
            "CHANGE",
            "DRAGDROP",
            "DRAGOVER",
            "GOTFOCUS",
            "KEYDOWN",
            "KEYPRESS",
            "KEYUP",
            "LOSTFOCUS",
        ],
        ListBox => &[
            "CLICK",
            "DBLCLICK",
            "DRAGDROP",
            "DRAGOVER",
            "GOTFOCUS",
            "KEYDOWN",
            "KEYPRESS",
            "KEYUP",
            "LOSTFOCUS",
            "MOUSEDOWN",
            "MOUSEMOVE",
            "MOUSEUP",
        ],
        ComboBox => &[
            "CHANGE",
            "CLICK",
            "DBLCLICK",
            "DRAGDROP",
            "DRAGOVER",
            "DROPDOWN",
            "GOTFOCUS",
            "KEYDOWN",
            "KEYPRESS",
            "KEYUP",
            "LOSTFOCUS",
            "MOUSEDOWN",
            "MOUSEMOVE",
            "MOUSEUP",
        ],
        CheckBox => &[
            "CLICK",
            "DRAGDROP",
            "DRAGOVER",
            "GOTFOCUS",
            "KEYDOWN",
            "KEYPRESS",
            "KEYUP",
            "LOSTFOCUS",
        ],
        OptionButton => &[
            "CLICK",
            "DBLCLICK",
            "DRAGDROP",
            "DRAGOVER",
            "GOTFOCUS",
            "KEYDOWN",
            "KEYPRESS",
            "KEYUP",
            "LOSTFOCUS",
        ],
        Frame => &["DRAGDROP", "DRAGOVER"],
        Label => &[
            "CHANGE",
            "CLICK",
            "DBLCLICK",
            "DRAGDROP",
            "DRAGOVER",
            "MOUSEDOWN",
            "MOUSEMOVE",
            "MOUSEUP",
        ],
        HScrollBar | VScrollBar => &[
            "CHANGE",
            "DRAGDROP",
            "DRAGOVER",
            "GOTFOCUS",
            "KEYDOWN",
            "KEYPRESS",
            "KEYUP",
            "LOSTFOCUS",
        ],
        Spin => &[
            "CUSTOM",
            "DRAGDROP",
            "DRAGOVER",
            "GOTFOCUS",
            "KEYDOWN",
            "KEYPRESS",
            "KEYUP",
            "LOSTFOCUS",
        ],
        PictureBox => &[
            "CLICK",
            "DBLCLICK",
            "DRAGDROP",
            "DRAGOVER",
            "GOTFOCUS",
            "KEYDOWN",
            "KEYPRESS",
            "KEYUP",
            "LOSTFOCUS",
            "MOUSEDOWN",
            "MOUSEMOVE",
            "MOUSEUP",
            "PAINT",
        ],
        Timer => &["TIMER"],
        Menu => &["CLICK"],
        DirListBox => &["CHANGE", "CLICK", "DBLCLICK", "PATHCHANGE"],
        DriveListBox => &["CHANGE"],
        FileListBox => &["CLICK", "DBLCLICK", "PATTERNCHANGE", "PATHCHANGE"],
        Screen => &[],
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EventParamType {
    Integer,
    Single,
    Control,
}

pub fn event_params(event: &str) -> Option<&'static [(&'static str, EventParamType)]> {
    use EventParamType::*;
    Some(match event.to_ascii_uppercase().as_str() {
        "KEYDOWN" | "KEYUP" => &[("KEYCODE", Integer), ("SHIFT", Integer)],
        "KEYPRESS" => &[("KEYASCII", Integer)],
        "MOUSEDOWN" | "MOUSEMOVE" | "MOUSEUP" => &[
            ("BUTTON", Integer),
            ("SHIFT", Integer),
            ("X", Single),
            ("Y", Single),
        ],
        "DRAGDROP" => &[("SOURCE", Control), ("X", Single), ("Y", Single)],
        "DRAGOVER" => &[
            ("SOURCE", Control),
            ("X", Single),
            ("Y", Single),
            ("STATE", Integer),
        ],
        "UNLOAD" => &[("CANCEL", Integer)],
        "CUSTOM" => &[("EVENTTYPE", Integer)],
        "CLICK" | "DBLCLICK" | "CHANGE" | "DROPDOWN" | "GOTFOCUS" | "LOSTFOCUS" | "LOAD"
        | "PAINT" | "RESIZE" | "TIMER" | "PATHCHANGE" | "PATTERNCHANGE" => &[],
        _ => return None,
    })
}

#[derive(Debug, Clone, PartialEq)]
pub struct FormObject {
    pub name: String,
    pub class: ObjectClass,
    pub parent_form: Option<String>,
    pub parent: Option<u16>,
    pub array: bool,
}

#[derive(Debug, Clone, Default, PartialEq)]
pub struct FormCatalog {
    pub objects: Vec<FormObject>,
}

impl FormCatalog {
    pub fn add(
        &mut self,
        name: impl Into<String>,
        class: ObjectClass,
        parent_form: Option<&str>,
        array: bool,
    ) -> u16 {
        let id = self.objects.len() as u16;
        self.objects.push(FormObject {
            name: name.into().to_uppercase(),
            class,
            parent: parent_form
                .and_then(|name| {
                    self.objects
                        .iter()
                        .rposition(|o| o.name.eq_ignore_ascii_case(name))
                })
                .map(|id| id as u16),
            parent_form: parent_form.map(str::to_uppercase),
            array,
        });
        id
    }

    pub fn find(&self, name: &str) -> Option<(u16, &FormObject)> {
        self.objects
            .iter()
            .enumerate()
            .find(|(_, o)| o.name.eq_ignore_ascii_case(name))
            .map(|(i, o)| (i as u16, o))
    }

    pub fn append(&mut self, other: &Self) {
        let offset = self.objects.len() as u16;
        self.objects
            .extend(other.objects.iter().cloned().map(|mut object| {
                object.parent = object.parent.map(|id| id + offset);
                object
            }));
    }

    pub fn find_in(&self, name: &str, form: &str) -> Option<(u16, &FormObject)> {
        self.objects
            .iter()
            .enumerate()
            .find(|(_, o)| o.name.eq_ignore_ascii_case(name) && self.belongs_to(o, form))
            .map(|(id, o)| (id as u16, o))
    }

    pub fn belongs_to(&self, object: &FormObject, form: &str) -> bool {
        let mut parent = object.parent;
        for _ in 0..self.objects.len() {
            let Some(object) = parent.and_then(|id| self.objects.get(id as usize)) else {
                return false;
            };
            if object.name.eq_ignore_ascii_case(form) {
                return true;
            }
            parent = object.parent;
        }
        false
    }
}

/// Tastenkombinationen, die der Forms-Host für Menü-Shortcuts liefert.
pub fn menu_shortcuts() -> Vec<String> {
    let mut out = vec![String::new()];
    for prefix in ["", "Ctrl+", "Alt+", "Shift+", "Ctrl+Shift+"] {
        for n in 1..=10 {
            out.push(format!("{prefix}F{n}"));
        }
    }
    for c in ('A'..='Z').filter(|c| *c != 'C') {
        out.push(format!("Ctrl+{c}"));
    }
    out
}
