#!/usr/bin/env python
"""
Usage:
    qc-headers [--dti] <dicom> <standard> <output>

Arguments:
    <dicom>         input dicom file to check.
    <standard>      'gold standard' dicom file to compare with.
    <output>        output log file.

Options:
    --dti      is it dti, will add in a comparison to gold standard bvals and bvecs

Diffs the non-ignored header fields (see DEFAULT_IGNORED_HEADERS) of dicom and
standard. Any differences found will be detailed in the output file.
If the output file already exists, it will be appended to instead of overwritten
so logs can be built on a session-wise, subject-wise, or project-wise basis.
NB: Settings are currently managed using the following global variables:
    DEFAULT_IGNORED_HEADERS
    INTEGER_TOLERANCES
    DECIMAL_TOLERANCES
This might (should?) be moved to a dedicated configuration file at some point.
"""
import os, sys
import collections
import logging
import pydicom as dcm
import numpy as np
from docopt import docopt

logging.basicConfig(level=logging.WARN, format="[%(name)s] %(levelname)s: %(message)s")
logger = logging.getLogger(os.path.basename(__file__))

DEFAULT_IGNORED_HEADERS = set([
    'AccessionNumber',
    'AcquisitionDate',
    'AcquisitionTime',
    'AcquisitionNumber',
    'AcquisitionMatrix',
    'Allegies',
    'AnatomicRegionSequence',
    'AngioFlag',
    'BeatRejectionFlag',
    'BitsAllocated',
    'BitsStored',
    'BodyPartExamined',
    'Columns',
    'CommentsOnThePerformedProcedureStep',
    'ContentDate',
    'ContentTime',
    'ContributingEquipmentSequence',
    'dBdt',
    'DeidentificationMethod',
    'DeidentificationMethodCodeSequence',
    'DeviceSerialNumber',
    'EchoNumbers',
    'EchoTrainLength',
    'EthnicGroup',
    'FrameOfReferenceUID',
    'HeartRate',
    'HighBit',
    'IconImageSequence',
    'ImageComments',
    'ImageOrientationPatient',
    'ImagePositionPatient',
    'ImagedNucleus',
    'ImageType',
    'ImagingFrequency',
    'InPlanePhaseEncodingDirection',
    'InStackPositionNumber',
    'InstanceCreationDate',
    'InstanceCreationTime',
    'InstitutionAddress',
    'InstitutionName',
    'InstitutionalDepartmentName',
    'InversionTime',
    'IssuerOfPatientID',
    'ImagesInAcquisition',
    'InstanceNumber',
    'LargestImagePixelValue',
    'OperatorsName',
    'MagneticFieldStrengh',
    'MRAcquisitionType',
    'NumberOfAverages',
    'PatientID',
    'PatientSize',
    'PixelData',
    'ProtocolName',
    'PhotometricInterpretation',
    'PositionReferenceIndicator',
    'RefdImageSequence',
    'RefdPerformedProcedureStepSequence',
    'RefdStudySequence',
    'ReferencedImageSequence',
    'ReferencedPerformedProcedureStepSequence',
    'ReferencedStudySequence',
    'RequestAttributesSequence',
    'RequestingPhysician',
    'PatientAge',
    'PatientBirthDate',
    'PatientIdentityRemoved',
    'PatientName',
    'PatientSex',
    'PatientWeight',
    'PatientTelephoneNumbers',
    'PercentPhaseFieldOfView',
    'PerformedProcedureStepID',
    'PerformedProcedureStepDescription',
    'PerformedProcedureStepStartDate',
    'PerformedProcedureStepStartTime',
    'PerformedStationName',
    'PerformingPhysicianName',
    'PhysiciansOfRecord',
    'PixelBandwidth',
    'PixelRepresentation',
    'PregnancyStatus',
    'ProcedureCodeSequence',
    'ReferringPhysicianName',
    'RequestedProcedureCodeSequence',
    'RequestedProcedureDescription',
    'RequestingService',
    'RescaleSlope',
    'RescaleIntercept',
    'RescaleType',
    'Rows',
    'SAR',
    'SamplesPerPixel',
    'ScanOptions',
    'ScanningSequence',
    'SequenceName',
    'SequenceVariant',
    'SOPInstanceUID',
    'SoftwareVersions',
    'SourceImageSequence',
    'SpecificCharacterSet',
    'SeriesNumber',
    'SeriesDate',
    'SeriesDescription',
    'SeriesInstanceUID',
    'SeriesTime',
    'SliceLocation',
    'SmallestImagePixelValue',
    'SOPClassUID',
    'StationName',
    'StorageMediaFileSetUID',
    'StudyDate',
    'StudyDescription',
    'StudyID',
    'StudyInstanceUID',
    'StudyTime',
    'TemporalPositionIdentifier',
    'TransmitCoilName',
    'TriggerTime',
    'WindowCenter',
    'WindowCenterWidthExplanation',
    'WindowWidth',
    'VariableFlipAngleFlag'
    'VariablePixelData'
])

INTEGER_TOLERANCES = {
    # field           : interger difference
    'ImagingFrequency': 1,
    'EchoTime': 5,
}

DECIMAL_TOLERANCES = {
    'RepetitionTime': 1 ,
    'SpacingBetweenSlices': 4
}

# represents a mismatch between expected and actual file headers
Mismatch = collections.namedtuple('Mismatch', ['header', 'expected', 'actual', 'tolerance'])

# Configuration object for the tolerances used in header compare
Tolerances = collections.namedtuple('Tolerances', ['integer', 'decimal'])

DEFAULT_TOLERANCES = Tolerances(
    integer=INTEGER_TOLERANCES,
    decimal=DECIMAL_TOLERANCES)

def compare_headers(stdhdr, cmphdr, tolerances=None, ignore=None):
    """
    Accepts two pydicom objects and prints out header value differences.
    Headers in ignore set are ignored.
    Returns a tuple containing a list of mismatched headers (as a list of
    Mismatch objects)
    """

    tolerances = tolerances or DEFAULT_TOLERANCES
    ignore = ignore or []

    # dir() is expensive so we cache results here
    stdhdrNames = stdhdr.dir()
    cmphdrNames = cmphdr.dir()

    # get the unignored headers
    headers = set(stdhdrNames).union(cmphdrNames).difference(ignore)

    mismatches = []

    for header in headers:
        if header not in stdhdrNames:
            mismatches.append(Mismatch(
                header=header, expected=None, actual=cmphdr.get(header), tolerance=None))
            continue

        if header not in cmphdrNames:
            mismatches.append(Mismatch(
                header=header, expected=stdhdr.get(header), actual=None, tolerance=None))
            continue

        stdval = stdhdr.get(header)
        cmpval = cmphdr.get(header)

        # integer level tolerance
        if header in tolerances.integer:
            n = tolerances.integer[header]
            stdApprox = np.round(float(stdval))
            cmpApprox = np.round(float(cmpval))
            if np.abs(stdApprox - cmpApprox) > n:
                mismatches.append(Mismatch(
                    header=header, expected=stdApprox, actual=cmpApprox, tolerance=n))

        # decimal level tolerance
        elif header in tolerances.decimal:
            n = tolerances.decimal[header]
            stdApprox = round(float(stdval), n)
            cmpApprox = round(float(cmpval), n)
            if stdApprox != cmpApprox:
                mismatches.append(Mismatch(
                    header=header, expected=stdApprox, actual=cmpApprox, tolerance=n))

        # no tolerance set
        elif str(cmpval) != str(stdval):
            mismatches.append(Mismatch(
                header=header, expected=stdval, actual=cmpval, tolerance=None))

    return mismatches

def get_header(filename):
    """
    Ensures the input dicom exists, and then attempts to parse it.
    """
    if not os.path.isfile(filename):
        logger.error('ERROR: input {} does not exist'.format(filename))
        sys.exit(1)
    try:
        hdr = dcm.read_file(filename)
        return hdr
    except:
        logger.error('ERROR: failed to parse {}'.format(filename))
        sys.exit(1)

def read_bvecs(filename):
    try:
        with open(filename, 'r') as f:
            a = np.fromstring(''.join(f.read().splitlines()),sep=" ").flatten()
            return a
    except:
        logger.error('ERROR: failed to parse {}'.format(filename))
        sys.exit(1)

def files_equal(path_f1,path_f2):
    '''
    Helper function to compare two files
    Arguments:
        path_f1                  Path to file 1
        path_f2                  Path to file 2

    Outputs:
        True or False
    '''

    try:
        with open(path_f1,'r') as f1, open(path_f2,'r') as f2:
            str1 = f1.readlines()
            str2 = f2.readlines()
    except IOError:
        raise

    return str1 == str2


def check_bfile(cmpdcm, stddcm, btype):

    '''
    Perform a comparison between b-file in standards and candidate file

    Arguments:
        cmpdcm                  Path to dcm file of candidate
        stddcm                  Path to dcm file of standard
        btype                   Type of b-file ('bvec','bval')
    '''

    btype = '.' + btype
    bfile = cmpdcm.replace('dcm','nii').replace('.nii',btype)
    std_bfile = stddcm.replace('.dcm',btype)

    try:

        if not files_equal(bfile, std_bfile) == True:
            error = 'does not match!'
        else:
            #Files identical return nothing
            return []

    except IOError:
        error = 'not found!'

    return [Mismatch(header= btype, expected='expected',
        actual= error, tolerance='None')]


def main():

    arguments = docopt(__doc__)

    dicom           =   arguments['<dicom>']
    standard        =   arguments['<standard>']
    output          =   arguments['<output>']

    dti             =   arguments['--dti']

    logging.info('Starting')

    outputDir = os.path.dirname(os.path.abspath(output))
    if not os.path.isdir(outputDir):
        logger.error('ERROR: output directory {} does not exist'.format(outputDir))
        sys.exit(1)

    # removed this command line argument for now
    #ignore_headers = ignore_headers and ignore_headers.split(",") or []
    #ignore_headers = DEFAULT_IGNORED_HEADERS.union(ignore_headers)
    ignore = DEFAULT_IGNORED_HEADERS
    tolerances = DEFAULT_TOLERANCES

    dicomHeader = dcm.read_file(dicom)
    standardHeader = dcm.read_file(standard)

    mismatches = compare_headers(standardHeader, dicomHeader, tolerances=tolerances, ignore=ignore)

    if dti:
        #mismatches.extend(check_bfile(dicom, standard, 'bvec'))
        mismatches.extend(check_bfile(dicom, standard, 'bval'))

    print('{} mismatches for {}'.format(len(mismatches), dicom))

    if os.path.isfile(output):
        writeMode = 'a'
    else:
        writeMode='w'

    with open(output, writeMode) as f:
        # write an empty file if we don't have any mismatches
        if len(mismatches) == 0:
            sys.exit(0)

        for m in mismatches:
            f.write("{}: header {}, expected = {}, actual = {} [tolerance = {}]\n".format(
                             dicom, m.header, m.expected, m.actual, m.tolerance))

if __name__ == '__main__':
    main()
