"""
ADX3 translation example.
To test with the basic file use adxFromExcel.testTranslateFile()

WARNING this requires the enhanced version of ElementTree which has added the prolog
parameter and its output to class ElementTree.
See https://www.seegrid.csiro.au/twiki/pub/Xmml/AdX3MappingALSChemexSheet/elementtreeUpdateForPrologs.zip

OR, remove the prolog=pi param as commented below **1

NOTE don't strip tabs when extracting headings, so we have entries in heading rows corresponding to the data cols
which means we need to have the blank headings in place.

TODO
This file will be refactored so the output objects are separated from the input parsing, that way it can be adapted to different sheets.

USAGE NOTE
We can generate inline patterns with the dictionaries in a file with the measurements or generate multiple files
with shared dictionaries. The objects in this sheet support either pattern - functions such as
buildPrepsDefElements will not care to where they are writing.

The only area of concern if using multiple files is customising the references to no longer be relative.


TODO - include times in all the generation classes but we are NOT concerned with that at present as the input sheets lack times

TODO optimise params where identical, eg:
	<history>
		<ProcessingHistory gml:id="abc123_b">
			<gml:name codeSpace="http://www.alschemex.com">abc123_b</gml:name>
			<gml:name codeSpace="http://www.newmont.com">NMT345thf</gml:name>
			<!-- the company-assigned identifier for the source specimen is also recorded in the Specimen Receipt event -->
			<!-- the specimen has the same company-assigned identifier as another specimen, but has a different lab-assigned identifier since it has a different prep history -->
			<status>Stored</adx:status>
			<protoEvents xl:href="#A_31"/>
			<param xl:href="#abc123_a1"/>
			<param xl:href="#abc123_a2"/>
			<param xl:href="#abc123_a3"/>
			<param xl:href="#abc123_a4"/>
		</adx:ProcessingHistory>
	</adx:history>

"""
from elementtree import ElementTree as ET
import datetime

#-----------------------------------------------------------------------------------------
#             G E N E R I C 
#-----------------------------------------------------------------------------------------
   
def testCreateXMLFile():
    """
    Simple test to prove I can output a valid XML file using elementree
    """
    root = ET.Element('test')
    tree = ET.ElementTree(root)
    asub = ET.SubElement(root, 'aTestSubby')
    asub.set('someAtt', '123')
    asub.text = "Hi there Andy"
    tree.write('test.xml')

#-----------------------------------------------------------------------------------------
#             X M L   O U T P U T
#
# these classes should NOT know anything about parsing, they are for output only!
# that makes them portable to a wide variety of input sheets
#-----------------------------------------------------------------------------------------

# mainly namespace attributes, starts with the initial attribute for the report as it is passed into a constructor
nsAttribs = { 'gml:id':"rep1",
              'xmlns':"http://www.seegrid.csiro.au/xml/adx/3",
              'xmlns:swe':"http://www.opengis.net/swe",
              'xmlns:sa':"http://www.seegrid.csiro.au/xml/sampling",
              'xmlns:om':"http://www.opengis.net/om",
              'xmlns:gml':"http://www.opengis.net/gml",
              'xmlns:gmd':"http://www.isotc211.org/2005/gmd",
              'xmlns:gco':"http://www.isotc211.org/2005/gco",
              'xmlns:xl':"http://www.w3.org/1999/xlink",
              'xmlns:xsi':"http://www.w3.org/2001/XMLSchema-instance",
              'xsi:schemaLocation':"http://www.seegrid.csiro.au/xml/adx/3 ../adx.xsd"
              }

# see id2ActivityType
activityTypesByID = {'LOG':'Transfer', 'CRU':'Crush', 'SPL':'Split', 'PUL':'Pulverize'}

#deviceSetupsByID = { 'AA23':adxPrepSeq('AA23_seq', 'AA23 digest seq', ('digest1') ),
#                     'GRA21':adxPrepSeq('GRA21_seq', 'GRA21 digest seq', ('digest1') ),
#                     'MS41':adxPrepSeq('MS41_seq', 'MS41 fusion seq', ('fusion1') )
#                    }

def TextElement(tag, body, attrib={}):
    ' shortcut for using ET.element and assigning text body'
    ret = ET.Element(tag, attrib)
    ret.text = body
    return ret

def TextSubElement(parent, tag, body, attrib={}):
    ' shortcut for using ET.element and assigning text body'
    ret = ET.SubElement(parent, tag, attrib)
    ret.text = body
    return ret

def id2ActivityType(id):
    """
    map id to an activityType enumeration, using first 3 chars if no match,
    default 'Split'
    """
    if activityTypesByID.has_key(id):
        return activityTypesByID[ id ]
    if activityTypesByID.has_key(id[0:3]): # prefix eg: LOG
        return activityTypesByID[ id[0:3] ]
    return 'Split'  # default?

    
def id2MeasureType(id):
    """
    map id to an assayMethod enumeration, using first 3 chars if no match,
    default 'Split'
    """
    if id.find('MS'):
        return 'MS'
    if id.find('AA'):
        return 'AAS'
    if id.find('GRA'):
        return 'GRAVIMETRIC'
    return 'Calculated'  # default?


def typeDesc2SampleType(desc):
    """
    map a descriptive string to a SpecimenTypeCode
    TODO: mapping of other types as samples come along
    """
    if 'Core' in desc:
        return 'Core'
    return 'Pulp'



class gmlIDbuilder:
    """
    base for things that build unique GML id values
    """    
    gID = 0
    gGMLUniqueIDs = {}
    
    def __init__(self, id=None):
        self.idSuffix = 0
        self.id = self.makeUniqueID(id)

    def makeUniqueID(self, id=None):
        if id is None:
            id = 'g' + str(gmlIDbuilder.gID)
            gmlIDbuilder.gID += 1
        candidateID = id
        while gmlIDbuilder.gGMLUniqueIDs.has_key(candidateID):
            candidateID = id + "_" + str(gmlIDbuilder.gID)
            gmlIDbuilder.gID += 1
        gmlIDbuilder.gGMLUniqueIDs[candidateID] = candidateID
        return candidateID
    makeUniqueID = classmethod(makeUniqueID)

##    def clone(self):
##        """
##        safe clone of us, override for any classes that contain clonable gmlIDbuilder subclasses
##        """
##        ret = copy.deepcopy(self)
##        ret.id = self.makeUniqueID()
##        return ret

    def gmlID(self):
        """
        generates without suffix so can easily refer back
        """
        return {'gml:id':self.id}

    def gmlUniqueID(self):
        """
        each time called, generates unique suffix
        """
        self.idSuffix += 1
        return {'gml:id':self.id+'_'+str(self.idSuffix)}

    
class adxPrepStep(gmlIDbuilder):
    """
    Individual steps pointed to by an adxPrepSeq
    """
    def __init__(self, id, name, desc):
        gmlIDbuilder.__init__(self, id)
        self.name = name
        self.desc = desc
        self.activityCode = id2ActivityType(id)

    def buildElement(self):
        """
        builds an element structure, eg:
        <gml:definitionMember>
            <SpecimenPreparationActivity gml:id="rec1">
                <gml:name>Receive Sample</gml:name>
                <activityType>Transfer</activityType>
                <gml:description>
        """       
        ret = ET.Element('gml:definitionMember')
        pe = ET.SubElement(ret, 'SpecimenPreparationActivity', self.gmlID())
        if len(self.desc)>0:
            pdesc = ET.SubElement(pe, 'gml:description')
            pdesc.text = self.desc
        pn = ET.SubElement(pe, 'gml:name')
        if len(self.name) > 0:
            pn.text = self.name
        else:
            pn.text = self.id
        pa = ET.SubElement(pe, 'activityType')
        pa.text = self.activityCode
        return ret


class adxAnalyte:
    def __init__(self, analyte, lowerDetectLimit, lowerDetectUOM):
        self.analyte = analyte
        self.lowerDetectLimit = lowerDetectLimit
        self.lowerDetectUOM = lowerDetectUOM

    def buildElement(self, appendTo):
        """
        builds an element structure
            <analyteDetails>
                <AnalyteSensitivity>
                    <analyte>Sb</analyte>
                    <lowerDetectionLimit uom="ppm">0.1</lowerDetectionLimit>
                    <upperDetectionLimit uom="percent">0.1</upperDetectionLimit>
        """       
        ald = ET.SubElement(appendTo, 'analyteDetails')
        as = ET.SubElement(ald, 'AnalyteSensitivity')
        al = ET.SubElement(as, 'analyte')
        al.text = self.analyte
        ld = ET.SubElement(as, 'lowerDetectionLimit', {'uom':self.lowerDetectUOM})
        ld.text = self.lowerDetectLimit
        

class adxProcedure(gmlIDbuilder):
    """
    cols corresponding to a procedure used to measure a sample
    """
    def __init__(self, instrumentSig, heads, startCol, endCol):
        """
        instrumentSig is something like ME-MS41 or Au-GRA21 or Au-AA21
        """
        id = instrumentSig[0:2]
        gmlIDbuilder.__init__(self, id)
        self.instrumentSig = instrumentSig
        self.startCol = startCol
        self.endCol = endCol
        self.desc = ''
        self.name = ''
        self.analytes = []
        for i in xrange(self.startCol, self.endCol):
            self.analytes.append( adxAnalyte(heads[2][i], heads[4][i], heads[3][i]) )

    def buildElement(self):
        """
        builds an element structure
            <gml:definitionMember>
                <AssayProcedure gml:id="ICPMS1">
                    <gml:description>Inductively coupled plasma mass spectrometry #1</gml:description>
                    <gml:name>ICP-MS 1</gml:name>
                    <om:method>Assay</om:method>
                    <assayMethod>ICP-MS</assayMethod>
        """       
        ret = ET.Element('gml:definitionMember')
        pe = ET.SubElement(ret, 'AssayProcedure', self.gmlID())
        if len(self.desc)>0:
            pdesc = ET.SubElement(pe, 'gml:description')
            pdesc.text = self.desc
        pn = ET.SubElement(pe, 'gml:name')
        if len(self.name) > 0:
            pn.text = self.name
        else:
            pn.text = self.id
        pe.append( TextElement('om:method', 'Assay') )  # assume all Assay methods
        pe.append( TextElement('assayMethod', id2MeasureType(self.id)) )
        for al in self.analytes:
            al.buildElement(pe)
        return ret
    
    


class adxMeasurement(gmlIDbuilder):
    def __init__(self, id, processingHistory, proc, analyte, wt, wtUnits):
        """
        Keep proc in here for now, may be interested in writing later
        """
        gmlIDbuilder.__init__(self, id)
        self.processingHistory = processingHistory
        self.proc = proc
        self.wt = wt
        self.analyte = analyte
        self.wtUnits = wtUnits
    
     
    def buildMeasurementElement(self, appendTo):
        """
        builds an element structure
            <measurement>
                <Assay gml:id="abc123_a_sb1_r">
                    <of ref="abc123_a"/>
                    <via xl:href="#1"/>
                    <result uom="ppm">0.1</om:result>
                    <analyte>Sb</analyte>
        """
        if self.wt[0]=='<':
            return  # HACK - skip measurements which are below threshold
        am = ET.SubElement(appendTo, 'measurement')
        as = ET.SubElement(am, 'Assay', self.gmlUniqueID())
        res = ET.SubElement(as, 'result', {'uom':self.wtUnits} )
        ET.SubElement(as, 'of', {'ref':self.processingHistory.id} )
        res.text = self.wt
        al = ET.SubElement(as, 'analyte')
        al.text = self.analyte


class adxProtoHistory(gmlIDbuilder):
    """
    describes the prototype histories
    """
    # class vars
    uniqueEventChains = {}

    def historyMatching(cls, eventKey, inDict=uniqueEventChains):
        """
        static method for lookup in a dictionary, creating a history if necessary
        """
        if inDict.has_key(eventKey):
            return inDict[eventKey], 0  # existing hist
        newHist = adxProtoHistory(eventKey)
        inDict[eventKey] = newHist
        return newHist, 1   # isnew
    historyMatching = classmethod(historyMatching)

   
    def __init__(self, eventKey, name=''):
        gmlIDbuilder.__init__(self)
        self.eventKey = eventKey
        self.events = []
        self.name = name

    def buildElement(self):
        """
        builds an element structure
            <gml:definitionMember>
                <ProtoHistory gml:id="A_31_reject">
                    <gml:name>Path A 31 Reject</gml:name>
                    <protoEvent xl:href="#log1_1"/>
                    <protoEvent xl:href="#dry1_1"/>
        """       
        ret = ET.Element('gml:definitionMember')
        ph = ET.SubElement(ret, 'ProtoHistory', self.gmlID())
        pn = ET.SubElement(ph, 'gml:name')
        if len(self.name) > 0:
            pn.text = self.name
        else:
            pn.text = self.id
        for ev in self.events:
            ET.SubElement(ph, 'protoEvent', {'xl:href':ev.id} )
        return ret

# there's an implied adxProtoEvent and adxProtoParam parent but it isn't necessary as a class so
# we just have the leaves - one per set of types in adxProcessingLeaves.xsd
# so we reuse the same classes for events and params, using buildElement and buildParamElement
# NOTE tuples used for uom types like mass!, t[0]=quantity, t[1]=uom
# NOTE2 there may appear to be a lot of repeated code in the following but leave for ease of fine-tuning!!!!
#  - later refactor the buildParams and buildElement into a parent class if they end up only parameterised by a couple of values
class adxProtoPulpPrep(gmlIDbuilder):
    """
    generates PulpPrepProtoEvent and PulpPrepEventParams
    """
    def __init__(self, procUsed=None, massPrior=None, massPost=None, fillsEvent=None):
        gmlIDbuilder.__init__(self)
        self.procUsed = procUsed
        self.massPrior = massPrior
        self.massPost = massPost
        self.fillsEvent = fillsEvent

	def status(self):
		return 'Stored'  #  'Specimen stored by provider' - if this is the last thing supplying status

    def buildParams(self, appendTo):
        """
        adds common params
        """       
        if self.procUsed is not None:
            ET.SubElement(appendTo, 'procedureUsed', {'xl:href':'#'+self.procUsed} )
        if self.massPrior is not None:
            TextSubElement(appendTo, 'massPrior', self.massPrior[0], {'uom':self.massPrior[1]} )
        if self.massPost is not None:
            TextSubElement(appendTo, 'massPost', self.massPost[0], {'uom':self.massPost[1]} )

    def buildElement(self):
        """
        builds an element structure, typically just
            <gml:definitionMember>
                <PulpPrepProtoEvent gml:id="99">
                    <procedureUsed xl:href="#crush1"/>
        """       
        ret = ET.Element('gml:definitionMember')
        r1 = ET.SubElement(ret, 'PulpPrepProtoEvent', self.gmlID())
        ET.SubElement(r1, 'gml:name')
        self.buildParams(r1)
        return ret

    def buildParamElement(self):
        """
        builds an element structure, typically just
            <param>
                <PulpPrepProtoParam gml:id="abc123_a3">
                    <fills xl:href="g1"/>
                    <massPrior uom="kg">2.64</adx:massPrior>
                    <massPost uom="kg">2.61</adx:massPost>
        """       
        ret = ET.Element('param')
        r1 = ET.SubElement(ret, 'PulpPrepProtoParam', self.gmlID())
        ET.SubElement(r1, 'fills', {'xl:href':'#'+self.fillsEvent} )
        self.buildParams(r1)
        return ret
    

class adxProtoRejectStore(gmlIDbuilder):
    """
    generates RejectStoreProtoEvent and RejectStoreProtoParam
    """
    def __init__(self, procUsed=None, mass=None, fillsEvent=None):
        gmlIDbuilder.__init__(self)
        self.procUsed = procUsed
        self.mass = mass
        self.fillsEvent = fillsEvent

	def status(self):
		return 'Stored'  #  'Specimen stored by provider' - if this is the last thing supplying status

    def buildParams(self, appendTo):
        """
        adds common params
        """       
        if self.procUsed is not None:
            ET.SubElement(appendTo, 'procedureUsed', {'xl:href':'#'+self.procUsed} )
        if self.mass is not None:
            TextSubElement(appendTo, 'mass', self.mass[0], {'uom':self.mass[1]} )

    def buildElement(self):
        """
        builds an element structure, typically just
            <gml:definitionMember>
                <RejectStoreProtoEvent gml:id="99">
                    <procedureUsed xl:href="#crush1"/>
        """       
        ret = ET.Element('gml:definitionMember')
        r1 = ET.SubElement(ret, 'RejectStoreProtoEvent', self.gmlID())
        ET.SubElement(r1, 'gml:name')
        self.buildParams(r1)
        return ret

    def buildParamElement(self):
        """
        builds an element structure, typically just
            <param>
                <RejectStoreProtoParam gml:id="abc123_a3">
                    <fills xl:href="g1"/>
                    <massPrior uom="kg">2.64</adx:massPrior>
                    <massPost uom="kg">2.61</adx:massPost>
        """       
        ret = ET.Element('param')
        r1 = ET.SubElement(ret, 'RejectStoreProtoParam', self.gmlID())
        ET.SubElement(r1, 'fills', {'xl:href':'#'+self.fillsEvent} )
        self.buildParams(r1)
        return ret
    

class adxProtoInstrumentPrep(gmlIDbuilder):
    """
    generates InstrumentPrepProtoEvent and InstrumentPrepProtoParam
    """
    def __init__(self, procUsed=None, massPrior=None, massPost=None, fillsEvent=None):
        gmlIDbuilder.__init__(self)
        self.procUsed = procUsed
        self.massPrior = massPrior
        self.massPost = massPost
        self.fillsEvent = fillsEvent

	def status(self):
		return 'DSP'  #  'Specimen destroyed in preparation' - if this is the last thing supplying status

    def buildParams(self, appendTo):
        """
        adds common params
        """       
        if self.procUsed is not None:
            ET.SubElement(appendTo, 'procedureUsed', {'xl:href':'#'+self.procUsed} )
        if self.massPrior is not None:
            TextSubElement(appendTo, 'massPrior', self.massPrior[0], {'uom':self.massPrior[1]} )
        if self.massPost is not None:
            TextSubElement(appendTo, 'massPost', self.massPost[0], {'uom':self.massPost[1]} )

    def buildElement(self):
        """
        builds an element structure, typically just
            <gml:definitionMember>
                <InstrumentPrepProtoEvent gml:id="99">
                    <procedureUsed xl:href="#crush1"/>
        """       
        ret = ET.Element('gml:definitionMember')
        r1 = ET.SubElement(ret, 'InstrumentPrepProtoEvent', self.gmlID())
        ET.SubElement(r1, 'gml:name')
        self.buildParams(r1)
        return ret

    def buildParamElement(self):
        """
        builds an element structure, typically just
            <param>
                <InstrumentPrepProtoParam gml:id="abc123_a3">
                    <fills xl:href="g1"/>
                    <massPrior uom="kg">2.64</adx:massPrior>
                    <massPost uom="kg">2.61</adx:massPost>
        """       
        ret = ET.Element('param')
        r1 = ET.SubElement(ret, 'InstrumentPrepProtoParam', self.gmlID())
        ET.SubElement(r1, 'fills', {'xl:href':'#'+self.fillsEvent} )
        self.buildParams(r1)
        return ret

    

class adxProtoSampleReceipt(gmlIDbuilder):
    """
    generates SampleReceiptProtoEvent and SampleReceiptProtoParam
    """
    def __init__(self, procUsed=None, originalSampleID=None, mass=None, type=None, fillsEvent=None):
        gmlIDbuilder.__init__(self)
        self.procUsed = procUsed
        self.originalSampleID = originalSampleID
        self.mass = mass
        self.type = type
        self.fillsEvent = fillsEvent

	def status(self):
		return self.procUsed  #  if this is the only thing supplying status

    def buildParams(self, appendTo):
        """
        adds common params
        """       
        if self.procUsed is not None:
            ET.SubElement(appendTo, 'procedureUsed', {'xl:href':'#'+self.procUsed} )
        if self.originalSampleID is not None:
            TextSubElement(appendTo, 'originalSampleID', self.originalSampleID)
        if self.mass is not None:
            TextSubElement(appendTo, 'mass', self.mass[0], {'uom':self.mass[1]} )
        if self.type is not None:
            TextSubElement(appendTo, 'type', self.type.id)

    def buildElement(self):
        """
        builds an element structure, typically just
            <gml:definitionMember>
                <SampleReceiptProtoEvent gml:id="99">
                    <procedureUsed xl:href="#crush1"/>
        """       
        ret = ET.Element('gml:definitionMember')
        r1 = ET.SubElement(ret, 'SampleReceiptProtoEvent', self.gmlID())
        ET.SubElement(r1, 'gml:name')
        self.buildParams(r1)
        return ret

    def buildParamElement(self):
        """
        builds an element structure, typically just
            <param>
                <SampleReceiptProtoParam gml:id="abc123_a3">
                    <fills xl:href="g1"/>
                    <originalSampleID>NMT345thf</adx:originalSampleID>
                    <mass uom="kg">2.64</adx:mass>
                    <type>Core</adx:type>
        """       
        ret = ET.Element('param')
        r1 = ET.SubElement(ret, 'SampleReceiptProtoParam', self.gmlID())
        ET.SubElement(r1, 'fills', {'xl:href':'#'+self.fillsEvent} )
        self.buildParams(r1)
        return ret
    

class adxProtoPulpReuse(gmlIDbuilder):
    """
    generates PulpReuseProtoEvent and PulpReuseProtoParam
    """
    def __init__(self, procUsed=None, originalSampleID=None, mass=None, fillsEvent=None):
        gmlIDbuilder.__init__(self)
        self.procUsed = procUsed
        self.originalSampleID = originalSampleID
        self.mass = mass
        self.fillsEvent = fillsEvent

	def status(self):
		return self.procUsed  #  if this is the only thing supplying status

    def buildParams(self, appendTo):
        """
        adds common params
        """       
        if self.procUsed is not None:
            ET.SubElement(appendTo, 'procedureUsed', {'xl:href':'#'+self.procUsed} )
        if self.originalSampleID is not None:
            TextSubElement(appendTo, 'originalSampleID', self.originalSampleID)
        if self.mass is not None:
            TextSubElement(appendTo, 'mass', self.mass[0], {'uom':self.mass[1]} )

    def buildElement(self):
        """
        builds an element structure, typically just
            <gml:definitionMember>
                <PulpReuseProtoEvent gml:id="99">
                    <procedureUsed xl:href="#crush1"/>
        """       
        ret = ET.Element('gml:definitionMember')
        r1 = ET.SubElement(ret, 'PulpReuseProtoEvent', self.gmlID())
        ET.SubElement(r1, 'gml:name')
        self.buildParams(r1)
        return ret

    def buildParamElement(self):
        """
        builds an element structure, typically just
            <param>
                <PulpReuseProtoParam gml:id="abc123_a3">
                    <fills xl:href="g1"/>
                    <originalSampleID>NMT345thf</adx:originalSampleID>
                    <mass uom="kg">2.64</adx:mass>
                    <type>Core</adx:type>
        """       
        ret = ET.Element('param')
        r1 = ET.SubElement(ret, 'PulpReuseProtoParam', self.gmlID())
        ET.SubElement(r1, 'fills', {'xl:href':'#'+self.fillsEvent} )
        self.buildParams(r1)
        return ret
    

class adxProtoRepeat(gmlIDbuilder):
    """
    generates RepeatProtoEvent and RepeatProtoParam
    """
    def __init__(self, procUsed, originalSampleID=None, mass=None, fillsEvent=None):
        gmlIDbuilder.__init__(self)
        self.procUsed = procUsed
        self.originalSampleID = originalSampleID
        self.mass = mass
        self.fillsEvent = fillsEvent

    def status(self):
        return self.procUsed  #  if this is the only thing supplying status

    def buildParams(self, appendTo):
        """
        adds common params
        """       
        if self.procUsed is not None:
            ET.SubElement(appendTo, 'procedureUsed', {'xl:href':'#'+self.procUsed} )
        if self.originalSampleID is not None:
            TextSubElement(appendTo, 'originalSampleID', self.originalSampleID)
        if self.mass is not None:
            TextSubElement(appendTo, 'mass', self.mass[0], {'uom':self.mass[1]} )

    def buildElement(self):
        """
        builds an element structure, typically just
            <gml:definitionMember>
                <RepeatProtoEvent gml:id="99">
                    <procedureUsed xl:href="#crush1"/>
        """       
        ret = ET.Element('gml:definitionMember')
        r1 = ET.SubElement(ret, 'RepeatProtoEvent', self.gmlID())
        ET.SubElement(r1, 'gml:name')
        self.buildParams(r1)
        return ret

    def buildParamElement(self):
        """
        builds an element structure, typically just
            <param>
                <RepeatProtoParam gml:id="abc123_a3">
                    <fills xl:href="g1"/>
                    <originalSampleID>NMT345thf</adx:originalSampleID>
                    <mass uom="kg">2.64</adx:mass>
                    <type>Core</adx:type>
        """       
        ret = ET.Element('param')
        r1 = ET.SubElement(ret, 'RepeatProtoParam', self.gmlID())
        ET.SubElement(r1, 'fills', {'xl:href':'#'+self.fillsEvent} )
        self.buildParams(r1)
        return ret


class adxProtoMeasure (gmlIDbuilder):
    """ 
    generates MeasureProtoEvent and MeasureEventParams
    """
    def __init__(self, procUsed, mass=None, fillsEvent=None):
        gmlIDbuilder.__init__(self)
        self.procUsed = procUsed
        self.mass = mass
        self.fillsEvent = fillsEvent
        
    def status(self):
        return 'DSA'  #  'Specimen destroyed in Analysis' if this is the only thing supplying status

    def buildParams(self, appendTo):
        """
        adds common params
        """       
        if self.procUsed is not None:
            ET.SubElement(appendTo, 'procedureUsed', {'xl:href':'#'+self.procUsed} )
##        if self.mass is not None:
##            TextSubElement(appendTo, 'mass', self.mass[0], {'uom':self.mass[1]} )

    def buildElement(self):
        """
        builds an element structure, typically just
            <gml:definitionMember>
                <MeasureProtoEvent gml:id="99">
                    <procedureUsed xl:href="#ME"/>
        """       
        ret = ET.Element('gml:definitionMember')
        r1 = ET.SubElement(ret, 'MeasureProtoEvent', self.gmlID())
        ET.SubElement(r1, 'gml:name')
        self.buildParams(r1)
        return ret

    def buildParamElement(self):
        """
        builds an element structure, typically just
            <param>
                <MeasureEventParams gml:id="abc123_a3">
                    <fills xl:href="g1"/>
        TODO decide if there's anything else worth recording in a measure event
        """       
        ret = ET.Element('param')
        r1 = ET.SubElement(ret, 'MeasureEventParams', self.gmlID())
        ET.SubElement(r1, 'fills', {'xl:href':'#'+self.fillsEvent} )
        self.buildParams(r1)
        return ret
    
    

class adxProcessingHistory(gmlIDbuilder):
    """
    describes a history, creating nested parameters and prototypical events
    """   
    # class vars
    protoHistories = []
    protoEvents = []
    
    def __init__(self, id, protoHist=None):
        gmlIDbuilder.__init__(self, id)
        self.protoHist = protoHist
        self.measurements = []
        self.params = []

    def buildElement(self):
        """
        builds an element structure
            <history>
                <ProcessingHistory gml:id="abc123_b">
                    <status>Stored</adx:status>
                    <protoEvents xl:href="#A_31"/>
                    <param xl:href="#abc123_a1"/>
                    <param xl:href="#abc123_a2"/>
                    ...
        """
        ret = ET.Element('history')
        ph = ET.SubElement(ret, 'ProcessingHistory', self.gmlID())
        if len(self.protoHist.events) == 0:
            TextSubElement(ph, 'status', 'SNR')       # todo - need better default but think this is IMPOSSIBLE!
        else:
            TextSubElement(ph, 'status', self.protoHist.events[-1].status())       
        ET.SubElement(ph, 'protoEvents', {'xl:href':'#'+self.protoHist.id} )
        for p in self.params:
            ph.append( p.buildParamElement() )
        return ret

    def buildMeasurementElements(self, appendTo):
        for m in self.measurements:
            m.buildMeasurementElement(appendTo)
    
      


#-----------------------------------------------------------------------------------------
#             P A R S I N G   S P E C I F I C   F I L E 
#-----------------------------------------------------------------------------------------

# index of classes as factories, bit of a hack hardcoding in this file for now
classesByID = {'LOG-21':adxProtoSampleReceipt, 'LOG-24':adxProtoPulpReuse,
               'CRU-31':adxProtoPulpPrep, 'SPL-21':adxProtoPulpPrep, 'PUL-31':adxProtoPulpPrep,
               'ME':adxProtoInstrumentPrep, 'Au':adxProtoInstrumentPrep,
               'SNR':adxProtoSampleReceipt  # last being bit of a hack for Sample Not Received
               }
instrumentPrepClassesByID = {'ME':adxProtoInstrumentPrep, 'Au':adxProtoInstrumentPrep}
measureClassesByID = {'ME':adxProtoMeasure, 'Au':adxProtoMeasure}


def makeReport(desc, status, dateRep, dateRec, orderID, batchNum, company):
    ret = ET.Element('Report', nsAttribs)
    ret.append( TextElement('gml:description', desc) )
    ret.append( TextElement('reportStatus', status) )
    ret.append( TextElement('reportDate', dateRep) )
    r = ET.Element('requestor')
    rp = ET.SubElement(r, 'gmd:CI_ResponsibleParty')
    ro = ET.SubElement(rp, 'gmd:organisationName')
    ro.append( TextElement('gco:CharacterString', company) )
    rp = ET.SubElement(rp, 'gmd:role')
    ret.append( r )
    ret.append( TextElement('requestorsOrderID', orderID) )
    ret.append( TextElement('orderSubmittalDate', dateRec) )
    ret.append( ET.Element('provider') )
    ret.append( TextElement('providersOrderID', batchNum) )
    ret.append( TextElement('orderReceiptDate', dateRec) )
    return ret


def headingLinesToReportElement(inSheet):
    """
    returns an Element which is the root of the translated file
    and the heading lines of the file have been read in to form its basic properties
    uses strip('\t\n') because we just want a single block of text, regardless of horizontal position
    """
    # read successive lines of heading in strict order based on sample file    
    repStatusString = inSheet.readline().strip('\t\n')
    batchNum, repStatus = repStatusString.split(' - ')
    if repStatus=="Finalized":
        repStatus = "COMPLETE"  # hack to put acceptable value in there
    repClient = inSheet.readline().strip('\t\n').split(' : ')[1]
    numSamples = int( inSheet.readline().strip('\t\n').split(' : ')[1] )
    dateRep = str(datetime.date.today())
    dateRec = inSheet.readline().strip('\t\n').split(' : ')[1].split(' ')[0]  # eg: 'DATE RECEIVED : 2006-01-26  DATE FINALIZED : 2006-02-01'
    projString = inSheet.readline().strip('\t\n')
    certCommentString = inSheet.readline().strip('\t\n').replace('"', '')
    poNumber = inSheet.readline().strip('\t\n').split(' : ')[1].strip('"')
    # now we have the simple report properties, can make the root object
    desc = '\n'.join((repStatusString, projString, certCommentString))    
    ret = makeReport(desc, repStatus, dateRep, dateRec, poNumber, batchNum, repClient)
    return ret, numSamples


def buildProcDefs(firstMeasureCol, heads):
    """
    creates list of procedures, mainly working out their column range
    """
    ret = []
    measureStart = firstMeasureCol
    lastCol = len(heads[1])
    lastID = heads[1][firstMeasureCol]
    for i in xrange(firstMeasureCol+1, lastCol):
        currID = heads[1][i]
        if not (currID == lastID or (currID[0:3]=='ME-' and lastID[0:3]=='ME-')):
            #hack - cope with ME-MS41 samples wt reported under the column ME-ICP41i
            ret.append( adxProcedure(lastID, heads, measureStart, i-1) )
            lastID = currID
            measureStart = i
    ret.append( adxProcedure(lastID, heads, measureStart, lastCol-1) )
    return ret



def buildIncomingSampleProcessingHistories(specimenWtCol, heads, sampleRows, firstPrepCol, numPreps, storeActivityCols, procs):
    """
    creates list of processingHistories, by iterating down the rows
    Note that we abstract storeActivityCols rather than assuming there is just one!

    TODO
    events to add
    - tray
    - measure
    in another, similar function
    - repeats
    - pulpreuse from otherRows
    """
    histories = []
    wtUnits = heads[3][specimenWtCol]  # fixed weight for all in this table
    for row in sampleRows:
        wt = row[specimenWtCol]
        receiptCodes = [heads[1][i] for i in storeActivityCols if row[i]=='Y']  # eg: ['LOG-21']
        if len(receiptCodes)==0:  # did not arrive!
            partEvtsKey = 'SNR'
            snrHist, isNewHist = adxProtoHistory.historyMatching(partEvtsKey)
            if isNewHist:
                snrHist.events.append( adxProtoSampleReceipt(type='SNR') ) # ONLY one event - receipt failure
            continue
        partEvtsKey = ''.join( row[firstPrepCol:firstPrepCol+numPreps] ).replace('*','n')  # eg: 'YnYYY'
        commonProcs = [heads[1][i] for i in xrange(firstPrepCol, firstPrepCol+numPreps) if row[i]=='Y'] # eg: ['LOG-21', 'CRU-31', 'SPL-21', 'PUL-31']
        measureSuffix = 1 # for ID building
        # iterate across the process columns in the row, forming new event chains each time
        for p in procs:
            measureValue = row[p.endCol]
            if measureValue != '*':  # have a value in this col
                measureTuple = (measureValue, heads[3][p.endCol])  # uom from column header, eg: g
                fullEvtKey = partEvtsKey + p.id # default history for this instrument
                protoHist, isNewHist = adxProtoHistory.historyMatching(fullEvtKey)
                if isNewHist:
                    for proc in commonProcs:
						# warning the following approach assumes all the class inits have same param order!
                        protoHist.events.append( classesByID[proc](proc) )  # look up matching factory, TODO - pass in other params
                    proc = p.id[0:2]  # NASTY HACK for now
                    protoHist.events.append( instrumentPrepClassesByID[proc](proc))
                    protoHist.events.append( measureClassesByID[proc](proc, measureTuple))
                    adxProcessingHistory.protoHistories.append( protoHist )
                    adxProcessingHistory.protoEvents.extend( protoHist.events )
                else:
                    pass  # later compare instrument setup and make unique copy if needed
                ph = adxProcessingHistory(row[2], protoHist)
                # now save any unique params we need to flavour protoHist for ph
                proc = commonProcs[0]
                ph.params.append( classesByID[proc](originalSampleID=row[2], mass=(wt, wtUnits), fillsEvent=protoHist.events[0].id ) )
                
                histories.append(ph)
                for i in xrange(p.startCol, p.endCol-1):
                    ph.measurements.append( adxMeasurement(ph.id+'_'+str(measureSuffix), ph, p, heads[2][i], row[i], heads[3][i]) )
                    measureSuffix += 1
    return histories 

    

    
def buildDictElement(id, name, desc, entries):
    """
    builds element
    <definition>
        <gml:Dictionary gml:id="dic1">
            <gml:description>
            <gml:name>
    """
    ret = ET.Element('definition')
    dict = ET.SubElement(ret, 'gml:Dictionary', {'gml:id':id})
    de = ET.SubElement(dict, 'gml:description')
    de.text = desc
    ne = ET.SubElement(dict, 'gml:name')
    ne.text = name
    for e in entries:
        dict.append( e.buildElement() )
    return ret

 
def translateFile(filename):
    """
    returns a fully populated Element which is the root of the translated file
    and can be written out as a tree with ET.ElementTree(ret)
    """
    inSheet = open(filename)
    ret, numSamples = headingLinesToReportElement(inSheet)  # PARSING AND MAKING ROOT ELEMENT in one!
    numHeadingRows = 5
    # get heading lines
    heads = [inSheet.readline().rstrip('\n').split('\t') for i in xrange(0,numHeadingRows)]
    sampleRows = [inSheet.readline().rstrip('\n').split('\t') for i in xrange(0,numSamples)]
    otherRows = inSheet.readlines()
    # now have all the data loaded so
    inSheet.close()
    
    # extract objects
    firstPrepCol = heads[0].index('Sample Preparation') + 1 # allow for Type col
    numPreps = heads[0].count('Sample Preparation') - 1
    prepsRange = xrange(firstPrepCol, firstPrepCol+numPreps)
    preps = [adxPrepStep(heads[1][i], heads[2][i], heads[3][i]) for i in prepsRange]
    numProcParamCols = 5  # this might be found by analysis in a generalised script
    firstMeasureCol = firstPrepCol + numPreps + numProcParamCols
    procs = buildProcDefs(firstMeasureCol, heads)
    specimenWtCol = firstMeasureCol - 1 # probably very arbitrary but it's the last col of that group in this sample
    logCode = activityTypesByID['LOG']
    storeActivities = [prep.id for prep in preps if prep.activityCode==logCode]  # eg: ['LOG-21', 'LOG-24']
    storeActivityCols = [i for i in prepsRange if heads[1][i] in storeActivities]  # eg: [4, 5]
    histories = buildIncomingSampleProcessingHistories(specimenWtCol, heads, sampleRows, firstPrepCol, numPreps, storeActivityCols, procs)
    
    # add more elements to the output    
    ret.append( buildDictElement('dic1', 'prepProc1', 'List of Sample Preparation Activities', preps) )
    ret.append( buildDictElement('dic2', 'assayProc1', 'List of Analytical Procedures', procs) )
    ret.append( buildDictElement('dic3', 'protoHists', 'List of Prototypical Histories that apply to many samples',
                                 adxProcessingHistory.protoHistories) )
    ret.append( buildDictElement('dic4', 'protoHistories', 'List of ProtoEvents shared by the histories in dic3',
                                 adxProcessingHistory.protoEvents) )
    for ph in histories:  
        ret.append( ph.buildElement() )
    for ph in histories:
        ph.buildMeasurementElements(ret)
    return ret


def testTranslateFile(outFileName='translatedAquireTestExample.xml'):
    import os
    os.chdir(r'C:\dev\xmml\trunk\ADX\3.0\Examples')
    pi = ET.ProcessingInstruction('xml', 'version="1.0"')
    # **1 remove the , prolog=pi if you haven't updated your ElementTree as mentioned above
    tree = ET.ElementTree( translateFile('Aquire_Test_Example_EL06006411.txt'), prolog=pi )
    tree.write(outFileName)


if __name__ == '__main__':
    testTranslateFile()
    
