# -*- coding: utf-8 -*-
#
# Copyright (c) 2012-2013, CRS4
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of
# this software and associated documentation files (the "Software"), to deal in
# the Software without restriction, including without limitation the rights to
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
# the Software, and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

"""
HL7apy - helper functions
"""

import os
import sys
import collections
import importlib

from hl7apy.exceptions import UnsupportedVersion, InvalidEncodingChars, UnknownValidationLevel
from hl7apy.consts import DEFAULT_ENCODING_CHARS, DEFAULT_VERSION, VALIDATION_LEVEL

_DEFAULT_ENCODING_CHARS = DEFAULT_ENCODING_CHARS
_DEFAULT_VERSION = DEFAULT_VERSION
_DEFAULT_VALIDATION_LEVEL = VALIDATION_LEVEL.QUIET

def check_encoding_chars(encoding_chars):
    """
    Validate the given encoding chars

    :type encoding_chars: ``dict``
    :param encoding_chars: the encoding chars (see :func:`hl7apy.set_default_encoding_chars`)
    :raises: :exc:`hl7apy.exceptions.InvalidEncodingChars` if the given encoding chars are not valid
    """
    if not isinstance(encoding_chars, collections.MutableMapping):
        raise InvalidEncodingChars
    required = {'FIELD', 'COMPONENT', 'SUBCOMPONENT', 'REPETITION', 'ESCAPE'}
    missing = required - set(encoding_chars.keys())
    if missing:
        raise InvalidEncodingChars('Missing required encoding chars')

    values = [ v for k, v in encoding_chars.items() if k in required ]
    if len(values) > len(set(values)):
        raise InvalidEncodingChars('Found duplicate encoding chars')

def check_validation_level(validation_level):
    """
    Validate the given validation level

    :type validation_level: ``int``
    :param validation_level: validation level (see :class:`hl7apy.consts.VALIDATION_LEVEL`)
    :raises: :exc:`hl7apy.exceptions.UnknownValidationLevel` if the given validation level is unsupported
    """
    if validation_level not in (VALIDATION_LEVEL.QUIET, VALIDATION_LEVEL.STRICT):
        raise UnknownValidationLevel

def check_version(version):
    """
    Validate the given version number

    :type version: ``basestring``
    :param version: the version to validate (e.g. ``2.6``)
    :raises: :class:`hl7apy.exceptions.UnsupportedVersion` if the given version is unsupported
    """
    if version not in SUPPORTED_LIBRARIES:
        raise UnsupportedVersion(version)

def get_default_encoding_chars():
    """
    Get the default encoding chars

    :rtype: ``dict``
    :returns: the encoding chars (see :func:`hl7apy.set_default_encoding_chars`)

    >>> print get_default_encoding_chars()['FIELD']
    |
    """
    return _DEFAULT_ENCODING_CHARS

def get_default_version():
    """
    Get the default version

    :rtype: ``basestring``
    :returns: the default version

    >>> print get_default_version()
    2.5
    """
    return _DEFAULT_VERSION

def get_default_validation_level():
    """
    Get the default validation level

    :rtype: ``basestring``
    :returns: the default validation level

    >>> print get_default_validation_level()
    2
    """
    return _DEFAULT_VALIDATION_LEVEL

def set_default_validation_level(validation_level):
    """
    Set the given validation level as default

    :type validation_level: ``int``
    :param validation_level: validation level (see :class:`hl7apy.consts.VALIDATION_LEVEL`)
    :raises: :exc:`hl7apy.exceptions.UnknownValidationLevel` if the given validation level is unsupported

    >>> set_default_validation_level(3)
    Traceback (most recent call last):
        ...
    UnknownValidationLevel
    >>> set_default_validation_level(VALIDATION_LEVEL.QUIET)
    >>> print get_default_validation_level()
    2
    """
    check_validation_level(validation_level)

    global _DEFAULT_VALIDATION_LEVEL

    _DEFAULT_VALIDATION_LEVEL = validation_level

def set_default_version(version):
    """
    Set the given version as default

    :type version: ``basestring``
    :param version: the new default version (e.g. ``2.6``)
    :raises: :class:`hl7apy.exceptions.UnsupportedVersion` if the given version is unsupported

    >>> set_default_version('22')
    Traceback (most recent call last):
        ...
    UnsupportedVersion: The version 22 is not supported
    >>> set_default_version('2.3')
    >>> print get_default_version()
    2.3
    """
    check_version(version)

    global _DEFAULT_VERSION

    _DEFAULT_VERSION = version

def set_default_encoding_chars(encoding_chars):
    """
    Set the given encoding chars as default

    :type encoding_chars: ``dict``
    :param encoding_chars: the new encoding chars
    :raises: :class:`hl7apy.exceptions.InvalidEncodingChars` if the given encoding chars are not valid

    The *encoding_chars* dictionary should contain the following keys:

    +--------------+-----------------+
    |Key           |Default          |
    +==============+=================+
    |GROUP         |``\\r``           |
    +--------------+-----------------+
    |SEGMENT       |``\\r``           |
    +--------------+-----------------+
    |FIELD         |``|``            |
    +--------------+-----------------+
    |COMPONENT     |``^``            |
    +--------------+-----------------+
    |SUBCOMPONENT  |``&``            |
    +--------------+-----------------+
    |REPETITION    |``~``            |
    +--------------+-----------------+
    |ESCAPE        |``\\``            |
    +--------------+-----------------+

    >>> set_default_encoding_chars({'FIELD': '!'})
    Traceback (most recent call last):
        ...
    InvalidEncodingChars: Invalid encoding chars
    >>> set_default_encoding_chars({'FIELD': '!', 'COMPONENT': 'C', 'SUBCOMPONENT': 'S', 'REPETITION': 'R', 'ESCAPE': '\\\\'})
    >>> print get_default_encoding_chars()['FIELD']
    !
    """
    check_encoding_chars(encoding_chars)

    encoding_chars.update({'GROUP' : '\r', 'SEGMENT' : '\r'})

    global _DEFAULT_ENCODING_CHARS

    _DEFAULT_ENCODING_CHARS = encoding_chars

def load_library(version):
    """
    Load the correct module according to the version

    :type version: ``basestring``
    :param version: the version of the library to be loaded (e.g. '2.6')
    :rtype: module object
    """
    check_version(version)
    module_name = SUPPORTED_LIBRARIES[version]
    lib = sys.modules.get(module_name)
    if lib is None:
        lib = importlib.import_module(module_name)
    return lib

def load_reference(name, element_type, version):
    """
    Look for an element of the given type, name and version and return its reference structure

    :type element_type: ``basestring``
    :param element_type: the element type to look for (e.g. 'Segment')
    :type name: ``basestring``
    :param name: the element name to look for (e.g. 'MSH')
    :type version: ``basestring``
    :param version: the version of the library where to search the element (e.g. '2.6')
    :rtype: ``dict``
    :return: a dictionary describing the element structure
    :raise: ``KeyError`` if the element has not been found

    The returned dictionary will contain the following keys:

    +--------------+--------------------------------------------+
    |Key           |Value                                       |
    +==============+============================================+
    |cls           |an :class:`hl7apy.core.Element` subclass    |
    +--------------+--------------------------------------------+
    |name          |the Element name (e.g. PID)                 |
    +--------------+--------------------------------------------+
    |ref           |a tuple of one of the following format:     |
    |              |                                            |
    |              |('leaf', <datatype>, <longName>, <table>)   |
    |              |('sequence', (<child>, (<min>, <max>), ...))|
    +--------------+--------------------------------------------+

    >>> load_reference('UNKNOWN', 'Segment', '2.5')
    Traceback (most recent call last):
    ...
    KeyError: 'UNKNOWN'
    >>> r = load_reference('ADT_A01', 'Message', '2.5')
    >>> print r[0]
    sequence
    >>> r = load_reference('MSH_3', 'Field', '2.5')
    >>> print r[0]
    leaf
    """
    lib = load_library(version)
    ref = lib.get(name, element_type)
    return ref

def find_reference(name, element_types, version):
    """
    Look for an element of the given name and version into the given types and return its reference structure

    :type name: ``basestring``
    :param name: the element name to look for (e.g. 'MSH')
    :type types: ``list`` or ``tuple``
    :param types: the element classes where to look for the element (e.g. (Group, Segment))
    :type version: ``basestring``
    :param version: the version of the library where to search the element (e.g. '2.6')
    :rtype: ``dict``
    :return: a dictionary describing the element structure
    :raise: :class:`hl7apy.exceptions.ChildNotFound` if the element has not been found

    >>> from hl7apy.core import Message, Segment
    >>> find_reference('UNKNOWN', (Segment, ), '2.5')
    Traceback (most recent call last):
    ...
    ChildNotFound: No child named UNKNOWN
    >>> find_reference('ADT_A01', (Segment,),  '2.5')
    Traceback (most recent call last):
    ...
    ChildNotFound: No child named ADT_A01
    >>> r = find_reference('ADT_A01', (Message,),  '2.5')
    >>> print r['name'], r['cls']
    ADT_A01 <class 'hl7apy.core.Message'>
    """
    lib = load_library(version)
    ref = lib.find(name, element_types)
    return ref

def _discover_libraries():
    current_dir = os.path.dirname(__file__)
    return {o[1:].replace("_", "."): "hl7apy.{}".format(o)
            for o in os.listdir(current_dir) if o.startswith("v2_")}

SUPPORTED_LIBRARIES = _discover_libraries()

if __name__ == '__main__':

    import doctest
    doctest.testmod()
