#!/usr/bin/env python
# -*- coding: utf-8 -*-

'''
Created on 16th February, 2017:

File containing all useful functions for the different executables.

@author: Benjamin Yvernault, Electrical Engineering, Vanderbilt University
'''

import logging
import os
import sys

from .errors import XnatToolsError, XnatToolsUserError

LENGTH = 64
DISPLAY_TEMPLATE = """#######################################################\
#########
{name}
#                                                              #
# Developed by the MASI Lab Vanderbilt University, TN, USA.    #
# If issues, please start a thread here:                       #
# https://groups.google.com/forum/#!forum/vuiis-cci            #
# Usage:                                                       #
{description}
# Examples:                                                    #
#     Check the help for examples by running --help            #
################################################################
{extra}"""
ARGS_DISPLAY = """Arguments:
{args}"""
CSV_HEADER = [
    'object_type', 'project_id', 'subject_label', 'session_type',
    'session_label', 'as_label', 'as_type', 'as_description', 'as_quality',
    'resource', 'fpath'
]
ORDER = ['commun', 'project', 'subject', 'session', 'scan', 'assessor']
XNAT_MODALITIES = {
    'CT': {'xsitype': 'xnat:ctSessionData',
           'info': 'An event in which CT scans are obtained on a subject'},
    'MR': {'xsitype': 'xnat:mrSessionData',
           'info': 'An event in which MR scans are obtained on a subject'},
    'PET': {'xsitype': 'xnat:petSessionData',
            'info': 'An event in which PET scans are obtained on a subject'},
    'EPS': {'xsitype': 'xnat:epsSessionData',
            'info': 'Cardiac Electrophysiology Session'},
    'DX': {'xsitype': 'xnat:dxSessionData',
           'info': 'An event in which Digital Radiography scans are obtained \
on a subject'},
    'RT': {'xsitype': 'xnat:rtSessionData', 'info': 'Radiotherapy Session'},
    'EEG': {'xsitype': 'xnat:eegSessionData',
            'info': 'Electroencephalography Session'},
    'HD': {'xsitype': 'xnat:hdSessionData', 'info': 'Hemodynamic Session'},
    'DX3DCRANIOFACIAL': {'xsitype': 'xnat:dx3DCraniofacialSessionData',
                         'info': 'X-Ray 3D Craniofacial Session'},
    'ECG': {'xsitype': 'xnat:ecgSessionData',
            'info': 'Electrocardiography Session'},
    'OTHERDICOM': {'xsitype': 'xnat:otherDicomSessionData',
                   'info': 'DICOM study of undetermined type'},
    'RF': {'xsitype': 'xnat:rfSessionData',
           'info': 'Radiofluoroscopy Session'},
    'XA3D': {'xsitype': 'xnat:xa3DSessionData',
             'info': 'X-Ray 3D Angiography Session'},
    'ESV': {'xsitype': 'xnat:esvSessionData',
            'info': 'Video Endoscopy Session'},
    'XC': {'xsitype': 'xnat:xcSessionData',
           'info': 'Visible Light Photography Session'},
    'XA': {'xsitype': 'xnat:xaSessionData',
           'info': 'An event in which X-ray Angiography scans are obtained on \
a subject'},
    'MEG': {'xsitype': 'xnat:megSessionData',
            'info': 'Magnetoencephalography Session'},
    'IO': {'xsitype': 'xnat:ioSessionData',
           'info': 'Intraoral Radiography Session'},
    'CR': {'xsitype': 'xnat:crSessionData',
           'info': 'Computed Radiography Session'},
    'GM': {'xsitype': 'xnat:gmSessionData',
           'info': 'Visible Light Microscopy Session'},
    'GMV': {'xsitype': 'xnat:gmvSessionData',
            'info': 'Video Microscopy Session'},
    'ES': {'xsitype': 'xnat:esSessionData',
           'info': 'Visible Light Endoscopy Session'},
    'OPT': {'xsitype': 'xnat:optSessionData',
            'info': 'Ophthalmic Tomography Session'},
    'MG': {'xsitype': 'xnat:mgSessionData',
           'info': 'Digital Mammography Session'},
    'US': {'xsitype': 'xnat:usSessionData',
           'info': 'Ultrasound Session'},
    'XCV': {'xsitype': 'xnat:xcvSessionData',
            'info': 'Video Photography Session'},
    'SM': {'xsitype': 'xnat:smSessionData',
           'info': 'Visible Light Slide-Coordinates Microscopy Session'},
    'OP': {'xsitype': 'xnat:opSessionData',
           'info': 'Ophthalmic Photography Session'},
}


def parse_args(name, description, add_tools_arguments, purpose,
               extra_display=''):
    """
    Method to parse arguments base on argparse

    :param name: name of the script
    :param description: description of the script for help display
    :param add_tools_arguments: fct to add arguments to parser
    :param purpose: purpose of the script
    :param extra_display: extra display
    :return: parser object
    """
    from argparse import ArgumentParser, RawTextHelpFormatter
    parser = ArgumentParser(prog=name, description=description,
                            formatter_class=RawTextHelpFormatter)
    parser.add_argument('--host', dest='host', default=None,
                        help='Host for XNAT. Default: env XNAT_HOST.')
    parser.add_argument('-u', '--username', dest='username', default=None,
                        help='Username for XNAT.')
    parser = add_tools_arguments(parser)
    main_display(name, purpose, extra_display)
    args = parser.parse_args()
    args_display(args)
    return args


def main_display(name, description, extra_display=''):
    """
    Main display of the executables before any process

    :param name: name of the executable
    :param description: description to display
    :param extra_display: extra display
    :return: None
    """
    _spaces = (LENGTH - len(name)) // 2
    _name = edit_string_size(name, left_spaces=_spaces)
    _desc = edit_string_size(description, left_spaces=4)
    print((DISPLAY_TEMPLATE.format(name=_name, description=_desc,
                                  extra=extra_display)))
    print_separators(symbol='-')


def args_display(args):
    """
    Display arguments set by user

    :param args: arguments from parse_args
    :param description: description to display
    :return: None
    """
    _args = list()
    for key, value in list(vars(args).items()):
        _format = '  %*s -> %*s'
        if isinstance(value, bool):
            if value:
                _args.append(_format % (-15, key.replace('_', ' '), -43, 'on'))
        else:
            if value:
                _args.append(_format % (-15, key.replace('_', ' '),
                                        -43, get_proper_str(value, True)))

    print((ARGS_DISPLAY.format(args='\n'.join(_args))))
    print_separators(symbol='-', return_line=True)


def run_tool(script, description, add_tools_arguments, purpose, run_tool_fct,
             extra_display=''):
    """
    Main function to run for all xnat tools.
    Set args/display and run core function (run_tool_fct)
    See tools for example.

    :param script: name of script
    :param add_tools_arguments: fct adding arguments to parser
    :param display: dictionary coding display
    :param run_tool_fct: core fct for the tool
    :param extra_display: extra display after checking args
    :return: None
    """
    args = parse_args(script, description, add_tools_arguments, purpose,
                      extra_display)
    run_tool_fct(args)


def setup_info_logger(name, log_file=None):
    """
    Using logger for the executables output.
     Setting the information for the logger.

    :param name: Name of the logger
    :param log_file: log file path to write outputs
    :return: logging object
    """
    if log_file:
        handler = logging.FileHandler(log_file, 'w')
    else:
        handler = logging.StreamHandler(sys.stdout)

    logger = logging.getLogger(name)
    logger.setLevel(logging.INFO)
    logger.addHandler(handler)
    return logger


def edit_string_size(strings, max_length=LENGTH - 4, left_spaces=0,
                     sformat='# %s%s #', symbol=' '):
    """
    Edit the string to by adding spaces at the beginning and end.
    If string is a list, return list of strings edited.

    :param string: string to edit or list of strings
    :param length: length of the string
    :return: new string of length 60
    """
    if isinstance(strings, str):
        _lspaces = symbol * int(left_spaces)
        if symbol != ' ':
            _lspaces = '%s ' % _lspaces[:-1]
        _str = '%s%s' % (_lspaces, strings)
        _space = (max_length - len(_str))
        return sformat % (_str, symbol * int(_space))
    elif isinstance(strings, list):
        # Separate the string in several
        new_strings = list()
        for string in strings:
            _str = edit_string_size(string, max_length, left_spaces)
            new_strings.append(_str)
        return '\n'.join(new_strings)
    else:
        err = "Wrong type for 'strings' argument in edit_string_size. Required\
 <type 'str'> or <type 'list'>, found: %s"
        raise XnatToolsError(err % type(string))


def read_txt(txt_file, exe_name=''):
    """
    Method to read the txt file path with per line, the name
     of the variable on REDCap you want to extract

    :param txt_file: filepath
    :return: list of REDCap variables
    """
    if txt_file:
        print(('INFO: Export data from text file %s ...' % txt_file))
        obj_list = list()
        if not os.path.exists(txt_file):
            err = 'file %s does not exist.'
            raise XnatToolsUserError(exe_name, err % txt_file)
        else:
            with open(txt_file, 'r') as input_file:
                for line in input_file:
                    obj_list.append(line.strip().split('\n')[0])
    else:
        obj_list = None
    return obj_list


def write_csv(csv_string, csv_file, exe_name=''):
    """
    Method to write the report as a csv file
     with the values from REDCap

    :param csv_string: data to write in the csv
    :param csv_file: csv filepath
    :param exe_name: name of executable running the function for error
    :return: None
    """
    print('INFO: Writing report ...')
    basedir = os.path.basedir(csv_file)
    if not os.path.exists(basedir):
        err = 'Path %s not found for report. Give an existing parent folder.'
        raise XnatToolsUserError(exe_name, err % csv_file)
    with open(csv_file, 'w') as output_file:
        for line in csv_string:
            output_file.write(line)


def get_option_list(string, all_value='all'):
    """
    Method to switch the string of value separated by a comma into a list.
     If the value is 'all', keep all.

    :param string: string to change
    :param all_value: value to return if all used
    :return: None if not set, all if all, list of value otherwise
    """
    if not string or string == 'nan':
        return None
    elif string == 'all':
        return all_value
    else:
        return string.split(',')


def get_proper_str(str_option, end=False, size=43):
    """
    Method to shorten a string into the proper size for display

    :param str_option: string to shorten
    :param end: keep the end of the string visible (default beginning)
    :return: shortened string
    """
    if len(str_option) > size:
        if end:
            return '...%s' % str_option[-(size - 3):]
        else:
            return '%s...' % str_option[:(size - 3)]
    else:
        return str_option


def prompt_user_yes_no(question):
    """Prompt the user for a question with answer Y/N.

    :return: True if yes, False if no, ask again if any other answer
    """
    value = ''
    while value.lower() not in ['yes', 'no', 'n', 'y']:
        value = input("%s [yes/no] " % question)

    if value.lower() in ['yes', 'y']:
        return True
    else:
        return False


def print_separators(symbol='=', length=LENGTH, return_line=False):
    """
    Print line to separate: symbolx60

    :param symbol: symbol to print length time (60)
    """
    _format = '%s'
    if return_line:
        _format = '%s\n'
    print((_format % (symbol * length)))


def print_end(name):
    """
    Last display when the tool script finish.
    """
    _spaces = (LENGTH - 6 - len(name)) / 2
    print(('%s\n' % edit_string_size(name, max_length=LENGTH - 6,
                                    left_spaces=_spaces,
                                    sformat='%s DONE %s', symbol='=')))


def get_gender_from_label(gender):
    """
    Method to get the passed gender in XNAT format

    :param gender: gender selected
    :return: value accepted by xnat: 'female' or 'male'
    """
    if gender.lower() in ['female', 'f']:
        return 'female'
    elif gender.lower() in ['male', 'm']:
        return 'male'
    else:
        return 'unknown'


def get_handedness_from_label(handedness):
    """
    Method to get the passed handedness in XNAT format

    :param handedness: handedness selected
    :return: value accepted by xnat: 'right', 'left', or 'ambidextrous'
    """
    if handedness.lower() in ['right', 'r']:
        return 'right'
    elif handedness.lower() in ['left', 'l']:
        return 'left'
    elif handedness.lower() in ['ambidextrous', 'a']:
        return 'ambidextrous'
    else:
        return 'unknown'


def get_resources_list(object_dict, resources_list):
    """
    Method to get the list of resources labels

    :param object_dict: dictionary to describe XNAT object parameters
    :param resources_list: list of resources requested from the user
    :return: None if empty, 'all' if all selected, list otherwise
    """
    # Get list of resources' label
    if resources_list == 'all':
        res_list = object_dict('resources', None)
    else:
        res_list = resources_list
    return res_list


def display_item(project, subject=None, session=None):
    """
    Method to display the tree

    :param project: project ID on XNAT
    :param subject: subject label on XNAT
    :param session: session label on XNAT
    :return: None
    """
    print(('Project: %s' % (project)))
    if subject:
        print(('  +Subject: %s' % (subject)))
        if session:
            print(('    *Session: %s' % (session)))


def is_assessor_type(obj_type):
    """
    Function to return if type is assessor.

    :param obj_dict: dictionary to describe XNAT object parameters
    :return: boolean
    """
    _okeys = list(obj_type.keys())
    return 'xsiType' in _okeys or 'procstatus' in _okeys


def get_obj_info(ind, nb, obj):
    """
    Return info on the object for tools display (upload/download).

    :param ind: index of item to display in the list
    :param nb: number of item in the list of objects
    :param obj: object dictionary in the list
    :return: none
    """
    _format = '  + %d/%d objects: %s'
    _f_scan = 'scan %s %s'
    _f_s_extra = '-- type: %s -- series description: %s -- quality: %s'
    _f_assr = 'proc %s %s '
    _f_a_extra = '-- job status: %s -- QC status: %s'
    if is_assessor_type(obj):
        extra = ''
        _status = obj.get('procstatus', '')
        _qcstatus = obj.get('qcstatus', '')
        if _status or _qcstatus:
            extra = _f_a_extra % (_status, _qcstatus)

        info = _f_assr % (obj['label'], extra)
    else:
        extra = ''
        _type = obj.get('type', '')
        _desc = obj.get('series_description', '')
        _qual = obj.get('quality', '')
        if _type or _desc or _qual:
            extra = _f_s_extra % (_type, _desc, _qual)

        info = _f_scan % (obj['ID'], extra)

    return _format % (ind, nb, info)


def new_tree_object(previous, obj):
    """
    Check that we are still on the same project/subject/session.

    :param previous: previous loop info
    :param obj: object
    :return: True if new info, False otherwise
    """
    if obj['project_id'] != previous['project'] or \
       obj['subject_label'] != previous['subject'] or \
       obj['session_label'] != previous['session']:
        return True

    return False