"""
 JADN Definitions

A JSON Abstract Data Notation (JADN) file contains a list of datatype definitions.  Each type definition
has a specified format - a list of four or five columns depending on whether the type is primitive or
structure: (name, base type, type options, type description [, fields]).

For the enumerated type each field definition is a list of three items: (tag, name, description).

For other structure types (array, choice, map, record) each field definition is a list of five items:
(tag, name, type, field options, field description).
"""
from copy import deepcopy
from dataclasses import Field, dataclass, field
from inspect import isfunction
from typing import List, Optional, Tuple, Union


class BasicDataclass:
    __annotations__: dict
    __default__: dict
    __keyindex__: Tuple[str, ...]

    def __init_subclass__(cls, **kwargs):
        cls.__keyindex__ = tuple(cls.__annotations__)
        cls.__default__ = {}
        for k in cls.__keyindex__:
            v = getattr(cls, k, None)
            if isinstance(v, Field):
                if isfunction(v.default):
                    cls.__default__[k] = v.default()
                elif isfunction(v.default_factory):
                    cls.__default__[k] = v.default_factory()
                else:
                    cls.__default__[k] = None
            else:
                cls.__default__[k] = v if isinstance(v, (int, float, str)) else deepcopy(v)

    def __getitem__(self, key: Union[int, slice, str]):
        if isinstance(key, slice):
            return [self[k] for k in self.__keyindex__[key]]
        if isinstance(key, int):
            key = list(self.__keyindex__)[key]
        return object.__getattribute__(self, key)

    def __setitem__(self, key: Union[int, str], val: any):
        if isinstance(key, int):
            key = list(self.__keyindex__)[key]
        return object.__setattr__(self, key, val)

    def __delitem__(self, key: Union[int, str]):
        if isinstance(key, int):
            key = list(self.__keyindex__)[key]
        object.__setattr__(self, self.__default__[key], self.__default__[key])

    def __len__(self):
        return len(self.__keyindex__)


# Datatype Definition columns
TypeName = 0            # Name of the type being defined
BaseType = 1            # Core type of the type being defined
TypeOptions = 2         # An array of zero or more TYPE_OPTIONS
TypeDesc = 3            # A non-normative description of the type
Fields = 4              # List of one or more items or fields

# Enumerated Item Definition columns
ItemID = 0              # Integer item identifier
ItemValue = 1           # String value of the item
ItemDesc = 2            # A non-normative description of the Enumerated item

# Field Definition columns
FieldID = 0             # Integer field identifier
FieldName = 1           # Name or label of the field
FieldType = 2           # Type of the field
FieldOptions = 3        # An array of zero or more FIELD_OPTIONS (and TYPE_OPTIONS if extended)
FieldDesc = 4           # A non-normative description of the field


# Dataclass Helpers
@dataclass
class EnumFieldDefinition(BasicDataclass):
    ItemID: int = 0
    ItemValue: str = ''
    ItemDesc: str = ''


@dataclass
class GenFieldDefinition(BasicDataclass):
    FieldID: int = 0
    FieldName: str = 'FieldName'
    FieldType: str = 'FieldType'
    FieldOptions: List[str] = field(default_factory=lambda: [])
    FieldDesc: str = ''


@dataclass
class TypeDefinition(BasicDataclass):
    TypeName: str = 'DefinitionName'
    BaseType: str = 'DefinitionType'
    TypeOptions: List[str] = field(default_factory=lambda: [])
    TypeDesc: str = ''
    Fields: Optional[Union[List[GenFieldDefinition], List[EnumFieldDefinition]]] = field(default_factory=lambda: [])


# Core datatypes
PRIMITIVE_TYPES = (
    'Binary',
    'Boolean',
    'Integer',
    'Number',
    'String',
)

SELECTOR_TYPES = (
    'Enumerated',       # enum option specifies fields derived from a defined type
    'Choice',
)

STRUCTURED_TYPES = (
    'Array',
    'ArrayOf',          # (value_type): instance is a container but definition has no fields
    'Map',
    'MapOf',            # (key_type, value_type): instance is a container but definition has no fields
    'Record',
)

CORE_TYPES = PRIMITIVE_TYPES + SELECTOR_TYPES + STRUCTURED_TYPES

FIELD_LENGTH = {
    'Binary': 0,
    'Boolean': 0,
    'Integer': 0,
    'Number': 0,
    'String': 0,
    'Enumerated': 3,    # 0 if Enumerated type definition contains enum or pointer option
    'Choice': 5,
    'Array': 5,
    'ArrayOf': 0,
    'Map': 5,
    'MapOf': 0,
    'Record': 5,
}


def is_builtin(t: str) -> bool:      # Is a core type
    return t in CORE_TYPES


def has_fields(t: str) -> bool:      # Is a type with fields listed in definition
    return FIELD_LENGTH[t] == 5 if is_builtin(t) else False


# Option Tags/Keys
#   JADN TypeOptions and FieldOptions contain a list of strings, each of which is an option.
#   The first character of an option string is the type ID; the remaining characters are the value.
#   The option string is converted into a Name: Value pair before use.
#   The tables list the unicode codepoint of the ID and the corresponding Name and value type.

TYPE_OPTIONS = {        # Option ID: (name, value type, canonical order) # ASCII ID
    61: ('id', lambda x: True, 1),          # '=', Enumerated type and Choice/Map/Record keys are ID not Name
    42: ('vtype', lambda x: x, 2),          # '*', Value type for ArrayOf and MapOf
    43: ('ktype', lambda x: x, 3),          # '+', Key type for MapOf
    35: ('enum', lambda x: x, 4),           # '#', enumeration derived from Array/Choice/Map/Record type
    62: ('pointer', lambda x: x, 5),        # '>', enumeration of pointers derived from Array/Choice/Map/Record type
    47: ('format', lambda x: x, 6),         # '/', semantic validation keyword, may affect serialization
    37: ('pattern', lambda x: x, 7),        # '%', regular expression that a string must match
    121: ('minf', float, 8),                # 'y', minimum Number value
    122: ('maxf', float, 9),                # 'z', maximum Number value
    123: ('minv', int, 10),                 # '{', minimum byte or text string length, Integer value, element count
    125: ('maxv', int, 11),                 # '}', maximum byte or text string length, Integer value, element count
    113: ('unique', lambda x: True, 12),    # 'q', ArrayOf instance must not contain duplicates
    115: ('set', lambda x: True, 13),       # 's', ArrayOf instance is unordered and unique
    98: ('unordered', lambda x: True, 14),  # 'b', ArrayOf instance is unordered and not unique (bag)
    88: ('extend', lambda x: True, 15),     # 'X', Type has an extension point where fields may be appended
    33: ('default', lambda x: x, 16),       # '!', Default or constant value of instances of this type
    0x2229: ('and', lambda x: x, 17),       # '∩', INTERSECTION - instance must also match referenced type (allOf)
    0x222a: ('or', lambda x: x, 18),        # '∪', UNION - instance must match at least one of the types (anyOf)
}

FIELD_OPTIONS = {
    91: ('minc', int, 19),                  # '[', minimum cardinality, default = 1, 0 = field is optional
    93: ('maxc', int, 20),                  # ']', maximum cardinality, default = 1, 0 = inherited max, not 1 = array
    38: ('tagid', int, 21),                 # '&', field that specifies the type of this field
    60: ('dir', lambda x: True, 22),        # '<', pointer enumeration treats field as a collection
    75: ('key', lambda x: True, 23),        # 'K', field is a primary key for this type
    76: ('link', lambda x: True, 24),       # 'L', field is a link (foreign key) to an instance of FieldType
}

OPTION_ID = {   # Pre-computed reverse index - MUST match TYPE_OPTIONS and FIELD_OPTIONS
    'id':       chr(61),
    'vtype':    chr(42),
    'ktype':    chr(43),
    'enum':     chr(35),
    'pointer':  chr(62),
    'format':   chr(47),
    'pattern':  chr(37),
    'minf':     chr(121),
    'maxf':     chr(122),
    'minv':     chr(123),
    'maxv':     chr(125),
    'unique':   chr(113),
    'set':      chr(115),
    'unordered': chr(98),
    'extend':   chr(88),
    'default':  chr(33),
    'and':      chr(0x2229),
    'or':       chr(0x222a),
    'minc':     chr(91),
    'maxc':     chr(93),
    'tagid':    chr(38),
    'dir':      chr(60),
    'key':      chr(75),
    'link':     chr(76),
}

REQUIRED_TYPE_OPTIONS = {
    'Binary': [],
    'Boolean': [],
    'Integer': [],
    'Number': [],
    'String': [],
    'Enumerated': [],
    'Choice': [],
    'Array': [],
    'ArrayOf': ['vtype'],
    'Map': [],
    'MapOf': ['ktype', 'vtype'],
    'Record': [],
}

ALLOWED_TYPE_OPTIONS = {
    'Binary': ['format', 'minv', 'maxv', 'and', 'or'],
    'Boolean': ['and', 'or'],
    'Integer': ['format', 'minv', 'maxv', 'and', 'or'],
    'Number': ['format', 'minf', 'maxf', 'and', 'or'],
    'String': ['format', 'pattern', 'minv', 'maxv', 'and', 'or'],
    'Enumerated': ['id', 'enum', 'pointer', 'extend', 'and', 'or'],
    'Choice': ['id', 'extend', 'and', 'or'],
    'Array': ['extend', 'format', 'minv', 'maxv', 'and', 'or'],
    'ArrayOf': ['vtype', 'minv', 'maxv', 'unique', 'set', 'unordered', 'and', 'or'],
    'Map': ['id', 'extend', 'minv', 'maxv', 'and', 'or'],
    'MapOf': ['ktype', 'vtype', 'minv', 'maxv', 'and', 'or'],
    'Record': ['extend', 'minv', 'maxv', 'and', 'or'],
}

# Ensure jsonschema prerequisite packages are installed, e.g., rfc3987 for uri/iri validation
FORMAT_JS_VALIDATE = {      # Semantic validation formats defined by JSON Schema 2019-09 Sec 7.3
    'date-time': 'String',
    'date': 'String',
    'time': 'String',
    'duration': 'String',
    # 'email': 'String',        # jsonschema package has deliberately buggy email - won't be fixed
    'idn-email': 'String',
    # 'hostname': 'String',     # jsonschema package needs bug fix
    'idn-hostname': 'String',
    'ipv4': 'String',           # doesn't allow netmask prefix length
    'ipv6': 'String',           # doesn't allow netmask prefix length
    'uri': 'String',
    'uri-reference': 'String',
    'iri': 'String',
    'iri-reference': 'String',
    'uuid': 'String',
    'uri-template': 'String',
    'json-pointer': 'String',
    'relative-json-pointer': 'String',
    'regex': 'String'
}

FORMAT_VALIDATE = {         # Semantic validation formats defined by JADN
    'email': 'String',          # Use this instead of jsonschema
    'hostname': 'String',       # Use this instead of jsonschema
    'eui': 'Binary',            # IEEE Extended Unique Identifier, 48 bits or 64 bits
    'ipv4-addr': 'Binary',      # IPv4 address as specified in RFC 791 Section 3.1
    'ipv6-addr': 'Binary',      # IPv6 address as specified in RFC 8200 Section 3
    'ipv4-net': 'Array',        # Binary IPv4 address and Integer prefix length, RFC 4632 Section 3.1
    'ipv6-net': 'Array',        # Binary IPv6 address and Integer prefix length, RFC 4291 Section 2.3
    'i8': 'Integer',            # Signed 8 bit integer [-128 .. 127]
    'i16': 'Integer',           # Signed 16 bit integer [-32768 .. 32767]
    'i32': 'Integer',           # Signed 32 bit integer [-2147483648 .. 2147483647]
    'i64': 'Integer',           # Signed 64 bit integer [-2^63 .. 2^63 -1]
    # 'u#': 'Integer',            # Unsigned '#'-bit integer or bit field where #>0, [0 .. 2^# -1]
}

FORMAT_SERIALIZE = {        # Data representation formats for one or more serializations
    'eui': 'Binary',            # IEEE EUI, 'hex-byte-colon' text representation, (e.g., 00:1B:44:11:3A:B7)
    'ipv4-addr': 'Binary',      # IPv4 'dotted-quad' text representation, RFC 2673 Section 3.2
    'ipv6-addr': 'Binary',      # IPv6 text representation, RFC 4291 Section 2.2
    'ipv4-net': 'Array',        # IPv4 Network Address CIDR text string, RFC 4632 Section 3.1
    'ipv6-net': 'Array',        # IPv6 Network Address CIDR text string, RFC 4291 Section 2.3
    'b': 'Binary',              # Base64url - RFC 4648 Section 5 (default text representation of Binary type)
    'x': 'Binary',              # Hex - base16 - lowercase out, case-folding in
    'X': 'Binary',              # Hex - RFC 4648 Section 8 - uppercase only
    'datetime-ms': 'Integer',   # Milliseconds from the epoch, RFC 3339 date-time text representation
    'i8': 'Integer',            # 8 bit field - these affect packed (RFC 791 style) serializations
    'i16': 'Integer',           # 16 bit field
    'i32': 'Integer',           # 32 bit field
    'i64': 'Integer',           # 64 bit field
    # 'u#': 'Integer',            # #-bit field
    'f16': 'Number',            # IEEE 754 Half-Precision Float - these affect CBOR serialization
    'f32': 'Number',            # IEEE 754 Single-Precision Float
    'f64': 'Number',            # IEEE 754 Double-Precision Float (default binary representation of Number type)
}

VALID_FORMATS = {**FORMAT_JS_VALIDATE, **FORMAT_VALIDATE, **FORMAT_SERIALIZE}

DEFAULT_CONFIG = {          # Configuration values to use if not specified in schema
    '$MaxBinary': 255,          # Maximum number of octets for Binary types
    '$MaxString': 255,          # Maximum number of characters for String types
    '$MaxElements': 100,        # Maximum number of items/properties for container types
    '$Sys': '$',                # System reserved character for TypeName
    '$TypeName': '^[A-Z][-$A-Za-z0-9]{0,31}$',   # Type Name regex
    '$FieldName': '^[a-z][_A-Za-z0-9]{0,31}$',   # Field Name regex
    '$NSID': '^[A-Za-z][A-Za-z0-9]{0,7}$',       # Namespace ID regex
    '$TypeRef': '^$'            # Placeholder.  Actual pattern is ($NSID ':')? $TypeName
}

EXTENSIONS = {
    'AnonymousType',            # TYPE_OPTIONS included in FieldOptions
    'Multiplicity',             # maxc other than 1, or minv other than 0 (optional) or 1 (required)
    'DerivedEnum',              # enum and pointer/dir options, create Enumerated type of fields or JSON Pointers
    'MapOfEnum',                # ktype option specifies an Enumerated type
    'Link',                     # key and link options
}

INFO_ORDER = ('title', 'package', 'version', 'description', 'comments',
              'copyright', 'license', 'namespaces', 'exports', 'config')    # Display order

GRAPH_DETAIL = ('conceptual', 'logical', 'information')

# Type Hinting
OPTION_TYPES = Union[int, float, str]
