# This file was copied from the flowUtils package on 5/6/2019.
# This represents the work of Josef Spidlen and the other flowUtils 
# authors who are also accordingly credited in the authors list for CytoML.
# 
# Details:
# https://github.com/jspidlen/flowUtils/
# GitHub version: 1.35.8
# Bioconductor version: 1.49.0

#############################################
## Code related to writing Gating-ML files ##
#############################################

# NOTE: If somebody has the time, it may be nice to 
# rewrite this in an object-oriented fashion :-) 

# Write objects in the flowEnv environment to an Gating-ML 2.0 XML file.
# If file is NULL then output is written to standard output.
write.gatingML <- function(flowEnv, file = NULL)
{
    if(!is.null(file) && !is(file, "character")) 
        stop("A file has to be either NULL or a character string.", call. = FALSE)
    if(is.null(flowEnv) || !is.environment(flowEnv))
        stop("A flowEnv environment with objects to be saved is requred.", call. = FALSE)
    if(!is.null(file) && substr(file, nchar(file) - 3, nchar(file)) != ".xml")
        file <- paste(file, "xml", sep=".")
    
    flowEnv[['.debugMessages']] = c()

    namespaces <- c(
        "gating" = "http://www.isac-net.org/std/Gating-ML/v2.0/gating", 
        "xsi" = "http://www.w3.org/2001/XMLSchema-instance", 
        "transforms" = "http://www.isac-net.org/std/Gating-ML/v2.0/transformations", 
        "data-type" = "http://www.isac-net.org/std/Gating-ML/v2.0/datatypes")
    
    gatingMLNode = suppressWarnings(xmlTree("gating:Gating-ML", namespaces = namespaces, 
        attrs = c("xsi:schemaLocation" = "http://www.isac-net.org/std/Gating-ML/v2.0/gating http://flowcyt.sourceforge.net/gating/2.0/xsd/Gating-ML.v2.0.xsd http://www.isac-net.org/std/Gating-ML/v2.0/transformations http://flowcyt.sourceforge.net/gating/2.0/xsd/Transformations.v2.0.xsd http://www.isac-net.org/std/Gating-ML/v2.0/datatypes http://flowcyt.sourceforge.net/gating/2.0/xsd/DataTypes.v2.0.xsd")))

    gatingMLNode$addNode("data-type:custom_info", close = FALSE)
    gatingMLNode$addNode("info", "Gating-ML 2.0 export generated by R/flowCore/CytoML")
    gatingMLNode$addNode("R-version", sessionInfo()$R.version$version.string)
    gatingMLNode$addNode("flowCore-version", as.character(packageVersion("flowCore")))
    gatingMLNode$addNode("CytoML-version", as.character(packageVersion("CytoML")))
    gatingMLNode$addNode("XML-version", as.character(packageVersion("XML")))
    gatingMLNode$closeTag()
    
    flowEnv[['.objectIDsWrittenToXMLOutput']] = list() # Use this list to collect XML Ids
	
    somethingUseful = FALSE
    for (x in ls(flowEnv)) {
	   object = objectNameToObject(x, flowEnv)
       if(is(object, "parameterFilter") || is(object, "singleParameterTransform") || is(object, "setOperationFilter"))
       {
		   somethingUseful = TRUE
           break
       }
    }
    if(!somethingUseful) warning("Nothing useful seems to be present in the environment; the output Gating-ML file may not be very useful.", call. = FALSE)

    # Go over everything and temporarily add transformations and argument gates to flowEnv
    # if they are not saved in flowEnv directly, but they are being used in other objects
    flowEnv[['.addedObjects']] = list() # List of object identifiers of objects that we have to temporarily add to flowEnv
    for (x in ls(flowEnv)) addReferencedObjectsToEnv(x, flowEnv) 
    
    flowEnv[['.singleParTransforms']] = new.env() # Use this env to collect transformations
    for (x in ls(flowEnv)) if(is(flowEnv[[x]], "singleParameterTransform")) collectTransform(x, flowEnv)
    
    # Transforms go first unless they can be skipped all together
    for (x in ls(flowEnv)) if(is(flowEnv[[x]], "transform"))
        if(!shouldTransformationBeSkipped(x, flowEnv)) addObjectToGatingML(gatingMLNode, x, flowEnv)
    for (x in ls(flowEnv)) if(!is(flowEnv[[x]], "transform")) addObjectToGatingML(gatingMLNode, x, flowEnv)
    
    if(!is.null(file)) sink(file = file)
    cat(saveXML(gatingMLNode$value(), encoding = "UTF-8"))
    if(!is.null(file)) sink()
    
    rm(list = ls(flowEnv[['.singleParTransforms']], all.names = TRUE), envir = flowEnv[['.singleParTransforms']])
    rm('.singleParTransforms', envir = flowEnv)
    
    rm(list = as.character(flowEnv[['.addedObjects']]), envir = flowEnv)
    rm('.addedObjects', envir = flowEnv)
    
    rm('.objectIDsWrittenToXMLOutput', envir = flowEnv) 
}

# Add the object x to the Gating-ML node
addObjectToGatingML <- function(gatingMLNode, x, flowEnv, addParent = NULL, forceGateId = NULL)
{
    if(is(x, "character")) object = flowEnv[[x]]
    else object = x
    switch(class(object),
        "rectangleGate" = addRectangleGateNode(gatingMLNode, x, flowEnv, addParent, forceGateId),
        "polygonGate" = addPolygonGateNode(gatingMLNode, x, flowEnv, addParent, forceGateId),
        "ellipsoidGate" = addEllipsoidGateNode(gatingMLNode, x, flowEnv, addParent, forceGateId),
        "quadGate" = addQuadGateNode(gatingMLNode, x, flowEnv, addParent, forceGateId),
        "intersectFilter" = addBooleanAndGateNode(gatingMLNode, x, flowEnv, addParent, forceGateId),
        "unionFilter" = addBooleanOrGateNode(gatingMLNode, x, flowEnv, addParent, forceGateId),
        "complementFilter" = addBooleanNotGateNode(gatingMLNode, x, flowEnv, addParent, forceGateId),
        "subsetFilter" = addGateWithParent(gatingMLNode, x, flowEnv),
        "compensation" = addCompensation(gatingMLNode, x, flowEnv),
        "asinhtGml2" = addAsinhtGml2(gatingMLNode, x, flowEnv),
        "hyperlogtGml2" = addHyperlogtGml2(gatingMLNode, x, flowEnv),
        "lintGml2" = addLintGml2(gatingMLNode, x, flowEnv),
        "logtGml2" = addLogtGml2(gatingMLNode, x, flowEnv),
        "logicletGml2" = addLogicletGml2(gatingMLNode, x, flowEnv),
        "ratiotGml2" = addRatiotGml2(gatingMLNode, x, flowEnv),
        "ratio" = addRatioGml1.5(gatingMLNode, x, flowEnv),
        "asinht" = addAsinhtGml1.5(gatingMLNode, x, flowEnv),
        "compensatedParameter" = NA,
        "unitytransform" = NA,
        "numeric" = NA,
        {
            errMessage <- paste("Class \'", class(object), "\' is not supported in Gating-ML 2.0 output.", sep="")
            if(is(object, "singleParameterTransform"))
                errMessage <- paste(errMessage, " Only Gating-ML 2.0 compatible transformations are supported by Gating-ML 2.0 output. Transformation \'", 
                    object@transformationId, "\' is not among those and cannot be included. Therefore, any gate referencing this transformation would be referencing a non-existent transformation in the Gating-ML output. Please correct the gates and transformations in your environment and try again.", sep="")
            if(is(object, "filter"))
                errMessage <- paste(errMessage, " Only Gating-ML 2.0 compatible gates are supported by Gating-ML 2.0 output. Filter \'", 
                  object@filterId, "\' is not among those and cannot be included. Please remove this filter and any references to it from the environment and try again.", sep="")
			stop(errMessage, call. = FALSE)    
        }
    )
}

# Add rectangle gate x to the Gating-ML node
addRectangleGateNode <- function(gatingMLNode, x, flowEnv, addParent, forceGateId)
{
    gate = objectNameToObject(x, flowEnv)
    if(!is(gate, "rectangleGate")) stop(paste("Unexpected object insted of a rectangleGate - ", class(gate))) 
    addDebugMessage(paste("Working on rectangleGate ", gate@filterId, sep=""), flowEnv)
    
    myID = getObjectId(gate, forceGateId, flowEnv)
    if(isIdWrittenToXMLAlready(myID, flowEnv)) return(FALSE) 
    attrs = c("gating:id" = myID)
    if (!is.null(addParent)) attrs = c(attrs, "gating:parent_id" = filterIdtoXMLId(addParent, flowEnv))

    gatingMLNode$addNode("gating:RectangleGate", attrs = attrs, close = FALSE)
    addDimensions(gatingMLNode, x, flowEnv)
    gatingMLNode$closeTag() # </gating:RectangleGate>
}

# Add polygon gate x to the Gating-ML node
addPolygonGateNode <- function(gatingMLNode, x, flowEnv, addParent, forceGateId)
{
    gate = objectNameToObject(x, flowEnv)
    if(!is(gate, "polygonGate")) stop(paste("Unexpected object insted of a polygonGate - ", class(gate))) 
    addDebugMessage(paste("Working on polygonGate ", gate@filterId, sep=""), flowEnv)
    
    myID = getObjectId(gate, forceGateId, flowEnv)
    if(isIdWrittenToXMLAlready(myID, flowEnv)) return(FALSE) 
    attrs = c("gating:id" = myID)
    if (!is.null(addParent)) attrs = c(attrs, "gating:parent_id" = filterIdtoXMLId(addParent, flowEnv))    

    gatingMLNode$addNode("gating:PolygonGate", attrs = attrs, close = FALSE)
    addDimensions(gatingMLNode, x, flowEnv)
    for (i in 1:length(gate@boundaries[,1]))
    {
        gatingMLNode$addNode("gating:vertex", close = FALSE)
        # attrs = c("data-type:value" = gate@boundaries[i,1])
        attrs = c("data-type:value" = as.numeric(gate@boundaries[i,1]))
        gatingMLNode$addNode("gating:coordinate", attrs = attrs)
        # attrs = c("data-type:value" = gate@boundaries[i,2])
        attrs = c("data-type:value" = as.numeric(gate@boundaries[i,2]))
        gatingMLNode$addNode("gating:coordinate", attrs = attrs)
        gatingMLNode$closeTag() # </gating:vertex>
    }
    gatingMLNode$closeTag() # </gating:PolygonGate>
}

# Add ellipse gate x to the Gating-ML node
addEllipsoidGateNode <- function(gatingMLNode, x, flowEnv, addParent, forceGateId)
{
    gate = objectNameToObject(x, flowEnv)
    if(!is(gate, "ellipsoidGate")) stop(paste("Unexpected object insted of an ellipsoidGate - ", class(gate))) 
    addDebugMessage(paste("Working on ellipsoidGate ", gate@filterId, sep=""), flowEnv)
    
    myID = getObjectId(gate, forceGateId, flowEnv)
    if(isIdWrittenToXMLAlready(myID, flowEnv)) return(FALSE) 
    attrs = c("gating:id" = myID)
    if (!is.null(addParent)) attrs = c(attrs, "gating:parent_id" = filterIdtoXMLId(addParent, flowEnv))    

    gatingMLNode$addNode("gating:EllipsoidGate", attrs = attrs, close = FALSE)
    addDimensions(gatingMLNode, x, flowEnv)
    
    gatingMLNode$addNode("gating:mean", close = FALSE)
    for (i in 1:length(gate@mean))
    {
        attrs = c("data-type:value" = as.numeric(gate@mean[i]))
        gatingMLNode$addNode("gating:coordinate", attrs = attrs)
    }
    gatingMLNode$closeTag() # </gating:mean>
    
    gatingMLNode$addNode("gating:covarianceMatrix", close = FALSE)
    for (row in 1:length(gate@cov[,1]))
    {
        gatingMLNode$addNode("gating:row", close = FALSE)
        for (column in 1:length(gate@cov[1,]))
        {
            attrs = c("data-type:value" = gate@cov[row,column])
            gatingMLNode$addNode("gating:entry", attrs = attrs)
        }
        gatingMLNode$closeTag() # </gating:row>
    }
    gatingMLNode$closeTag() # </gating:covarianceMatrix>
    
    attrs = c("data-type:value" = gate@distance ^ 2)
    gatingMLNode$addNode("gating:distanceSquare", attrs = attrs)
    
    gatingMLNode$closeTag() # </gating:EllipsoidGate>
}

# Add a Boolean AND gate x to the Gating-ML node
addBooleanAndGateNode <- function(gatingMLNode, x, flowEnv, addParent, forceGateId)
{
    gate = objectNameToObject(x, flowEnv)
    if(!is(gate, "intersectFilter")) stop(paste("Unexpected object insted of an intersectFilter - ", class(gate))) 
    addDebugMessage(paste("Working on intersectFilter ", gate@filterId, sep=""), flowEnv)
    
    myID = getObjectId(gate, forceGateId, flowEnv)
    if(isIdWrittenToXMLAlready(myID, flowEnv)) return(FALSE) 
    attrs = c("gating:id" = myID)
    if (!is.null(addParent)) attrs = c(attrs, "gating:parent_id" = filterIdtoXMLId(addParent, flowEnv))    
    
    gatingMLNode$addNode("gating:BooleanGate", attrs = attrs, close = FALSE)
    gatingMLNode$addNode("gating:and", close = FALSE)
    if(length(gate@filters) == 0) 
        stop("Boolean AND gates (intersectFilter) have to reference some arguments.", call. = FALSE)
    for (i in 1:length(gate@filters))
    {
        attrs = c("gating:ref" = filterIdtoXMLId(gate@filters[[i]]@filterId, flowEnv))
        gatingMLNode$addNode("gating:gateReference", attrs = attrs)
    }
    if(length(gate@filters)  == 1) 
    {
        # If there was just one referenced filter than we add it twice
        # since and/or gates require at least two arguments in Gating-ML 2.0
        gatingMLNode$addNode("gating:gateReference", attrs = attrs)
    } 
    gatingMLNode$closeTag() # </gating:and>
    gatingMLNode$closeTag() # </gating:BooleanGate>    
}

# Add a Boolean OR gate x to the Gating-ML node
addBooleanOrGateNode <- function(gatingMLNode, x, flowEnv, addParent, forceGateId)
{
    gate = objectNameToObject(x, flowEnv)
    if(!is(gate, "unionFilter")) stop(paste("Unexpected object insted of a unionFilter - ", class(gate))) 
    addDebugMessage(paste("Working on unionFilter ", gate@filterId, sep=""), flowEnv)
    
    myID = getObjectId(gate, forceGateId, flowEnv)
    if(isIdWrittenToXMLAlready(myID, flowEnv)) return(FALSE) 
    attrs = c("gating:id" = myID)
    if (!is.null(addParent)) attrs = c(attrs, "gating:parent_id" = filterIdtoXMLId(addParent, flowEnv))    
    
    gatingMLNode$addNode("gating:BooleanGate", attrs = attrs, close = FALSE)
    gatingMLNode$addNode("gating:or", close = FALSE)
    if(length(gate@filters) == 0) 
        stop("Boolean OR gates (unionFilter) have to reference some arguments.", call. = FALSE)
    for (i in 1:length(gate@filters))
    {
        attrs = c("gating:ref" = filterIdtoXMLId(gate@filters[[i]]@filterId, flowEnv))
        gatingMLNode$addNode("gating:gateReference", attrs = attrs)
    }
    if(length(gate@filters)  == 1) 
    {
        # If there was just one referenced filter than we add it twice
        # since and/or gates require at least two arguments in Gating-ML 2.0
        gatingMLNode$addNode("gating:gateReference", attrs = attrs)
    } 
    gatingMLNode$closeTag() # </gating:or>
    gatingMLNode$closeTag() # </gating:BooleanGate>    
}

# Add a Boolean NOT gate x to the Gating-ML node
addBooleanNotGateNode <- function(gatingMLNode, x, flowEnv, addParent, forceGateId)
{
    gate = objectNameToObject(x, flowEnv)
    if(!is(gate, "complementFilter")) stop(paste("Unexpected object insted of a complementFilter - ", class(gate))) 
    addDebugMessage(paste("Working on complementFilter ", gate@filterId, sep=""), flowEnv)
    
    myID = getObjectId(gate, forceGateId, flowEnv)
    if(isIdWrittenToXMLAlready(myID, flowEnv)) return(FALSE) 
    attrs = c("gating:id" = myID)
    if (!is.null(addParent)) attrs = c(attrs, "gating:parent_id" = filterIdtoXMLId(addParent, flowEnv))    

    gatingMLNode$addNode("gating:BooleanGate", attrs = attrs, close = FALSE)
    gatingMLNode$addNode("gating:not", close = FALSE)
    if(length(gate@filters)  == 1) 
    {
        attrs = c("gating:ref" = filterIdtoXMLId(gate@filters[[1]]@filterId, flowEnv))
        gatingMLNode$addNode("gating:gateReference", attrs = attrs)
    } else stop("Boolean NOT gates (complementFilter) have to reference exactly one argument.", call. = FALSE)
    gatingMLNode$closeTag() # </gating:not>
    gatingMLNode$closeTag() # </gating:BooleanGate>    
}

# Add a Quadrant gate x to the Gating-ML node
addQuadGateNode <- function(gatingMLNode, x, flowEnv, addParent, forceGateId)
{
    gate = objectNameToObject(x, flowEnv)
    if(!is(gate, "quadGate")) stop(paste("Unexpected object insted of a quadGate - ", class(gate))) 
    addDebugMessage(paste("Working on quadGate ", gate@filterId, sep=""), flowEnv)
    
    myID = getObjectId(gate, forceGateId, flowEnv)
    if(isIdWrittenToXMLAlready(myID, flowEnv)) return(FALSE) 
    attrs = c("gating:id" = myID)
    if (!is.null(addParent)) attrs = c(attrs, "gating:parent_id" = filterIdtoXMLId(addParent, flowEnv))    
    
    gatingMLNode$addNode("gating:QuadrantGate", attrs = attrs, close = FALSE)
    addDimensions(gatingMLNode, x, flowEnv, myID)
    
    attrs = c("gating:id" = paste(myID, ".PP", sep = ""))
    gatingMLNode$addNode("gating:Quadrant", attrs = attrs, close = FALSE)
    attrs = c("gating:divider_ref" = paste(myID, ".D1", sep = ""))
    attrs = c(attrs, "gating:location" = as.character(gate@boundary[1] + 1))
    gatingMLNode$addNode("gating:position", attrs = attrs)
    attrs = c("gating:divider_ref" = paste(myID, ".D2", sep = ""))
    attrs = c(attrs, "gating:location" = as.character(gate@boundary[2] + 1))
    gatingMLNode$addNode("gating:position", attrs = attrs)
    gatingMLNode$closeTag() # </gating:Quadrant>
 
    attrs = c("gating:id" = paste(myID, ".PN", sep = ""))
    gatingMLNode$addNode("gating:Quadrant", attrs = attrs, close = FALSE)
    attrs = c("gating:divider_ref" = paste(myID, ".D1", sep = ""))
    attrs = c(attrs, "gating:location" = as.character(gate@boundary[1] + 1))
    gatingMLNode$addNode("gating:position", attrs = attrs)
    attrs = c("gating:divider_ref" = paste(myID, ".D2", sep = ""))
    attrs = c(attrs, "gating:location" = as.character(gate@boundary[2] - 1))
    gatingMLNode$addNode("gating:position", attrs = attrs)
    gatingMLNode$closeTag() # </gating:Quadrant>
    
    attrs = c("gating:id" = paste(myID, ".NP", sep = ""))
    gatingMLNode$addNode("gating:Quadrant", attrs = attrs, close = FALSE)
    attrs = c("gating:divider_ref" = paste(myID, ".D1", sep = ""))
    attrs = c(attrs, "gating:location" = as.character(gate@boundary[1] - 1))
    gatingMLNode$addNode("gating:position", attrs = attrs)
    attrs = c("gating:divider_ref" = paste(myID, ".D2", sep = ""))
    attrs = c(attrs, "gating:location" = as.character(gate@boundary[2] + 1))
    gatingMLNode$addNode("gating:position", attrs = attrs)
    gatingMLNode$closeTag() # </gating:Quadrant>
    
    attrs = c("gating:id" = paste(myID, ".NN", sep = ""))
    gatingMLNode$addNode("gating:Quadrant", attrs = attrs, close = FALSE)
    attrs = c("gating:divider_ref" = paste(myID, ".D1", sep = ""))
    attrs = c(attrs, "gating:location" = as.character(gate@boundary[1] - 1))
    gatingMLNode$addNode("gating:position", attrs = attrs)
    attrs = c("gating:divider_ref" = paste(myID, ".D2", sep = ""))
    attrs = c(attrs, "gating:location" = as.character(gate@boundary[2] - 1))
    gatingMLNode$addNode("gating:position", attrs = attrs)
    gatingMLNode$closeTag() # </gating:Quadrant>
    
    gatingMLNode$closeTag() # </gating:QuadrantGate>
}

# Add a subsetFilter gate named x to the the Gating-ML node
addGateWithParent <- function(gatingMLNode, x, flowEnv)
{
    addDebugMessage(paste("Working on ", x, sep=""), flowEnv)
    gate = objectNameToObject(x, flowEnv)
    if (!is(gate, "subsetFilter")) stop(paste("Expected a subsetFilter to add a gate with a parent id, but found an object of class", class(gate)))
    if (length(gate@filters) == 2){
        newX = gate@filters[[1]]
        parent = gate@filters[[2]]
        if (is(parent, 'filterReference')) parentName = parent@name
        else parentName = parent@filterId
        addObjectToGatingML(gatingMLNode, newX, flowEnv, parentName, gate@filterId)    
    }
    else stop(paste("Unexpected length of filters for class", class(gate)))
}

# Add a compensation named x to the the Gating-ML node
addCompensation <- function(gatingMLNode, x, flowEnv)
{
    myComp = objectNameToObject(x, flowEnv)
    if(!is(myComp, "compensation")) stop(paste("Unexpected object insted of a compensation - ", class(myComp))) 
    addDebugMessage(paste("Working on compensation ", myComp@compensationId, sep=""), flowEnv)
    
    myID = getObjectId(myComp, NULL, flowEnv)
    if(isIdWrittenToXMLAlready(myID, flowEnv)) return(FALSE) 
    
    detectors <- colnames(myComp@spillover)
    if (is.null(detectors)) 
    {
        stop(paste("Cannot export a spillover matrix without column names (", myComp@compensationId, ").", sep=""))
        return
    }
    
    fluorochromes <- rownames(myComp@spillover)
    if(is.null(fluorochromes))
    {
        if(nrow(myComp@spillover) != ncol(myComp@spillover)) 
        {
            stop(paste("Cannot export a non-sqaure spillover (spectrum) matrix without row names (", myComp@compensationId, ").", sep=""))
            return
        }
        else
        {
            fluorochromes <- detectors
        }
    }
    
    attrs = c("transforms:id" = myID)
    gatingMLNode$addNode("transforms:spectrumMatrix", attrs = attrs, close = FALSE)
    
    gatingMLNode$addNode("transforms:fluorochromes", close = FALSE)
    for (fname in fluorochromes) 
    {
        attrs = c("data-type:name" = fname)
        gatingMLNode$addNode("data-type:fcs-dimension", attrs = attrs)
    }
    gatingMLNode$closeTag() # </transforms:fluorochromes>
    
    gatingMLNode$addNode("transforms:detectors", close = FALSE)
    for (dname in detectors) 
    {
        attrs = c("data-type:name" = dname)
        gatingMLNode$addNode("data-type:fcs-dimension", attrs = attrs)
    }
    gatingMLNode$closeTag() # </transforms:detectors>
    
    for (rowNo in 1:nrow(myComp@spillover))
    {
        gatingMLNode$addNode("transforms:spectrum", close = FALSE)
        for (colNo in 1:ncol(myComp@spillover)) 
        {
            # attrs = c("transforms:value" = myComp@spillover[rowNo,colNo])
            attrs = c("transforms:value" = as.vector(myComp@spillover[rowNo,colNo]))
            gatingMLNode$addNode("transforms:coefficient", attrs = attrs)
        }
        gatingMLNode$closeTag() # </transforms:spectrum>
    }
    
    gatingMLNode$closeTag() # </transforms:spectrumMatrix>
}

# Add an asinhtGml2 transformation named x to the the Gating-ML node
addAsinhtGml2 <- function(gatingMLNode, x, flowEnv)
{
    myTrans = objectNameToObject(x, flowEnv)
    if(!is(myTrans, "asinhtGml2")) stop(paste("Unexpected object insted of asinhtGml2 - ", class(myTrans))) 
    addDebugMessage(paste("Working on asinhtGml2 ", myTrans@transformationId, sep=""), flowEnv)
    
    myID = getObjectId(myTrans, NULL, flowEnv)
    if(isIdWrittenToXMLAlready(myID, flowEnv)) return(FALSE) 
    
    attrs = c("transforms:id" = myID)
    if (is.finite(myTrans@boundMin)) attrs = append(attrs, c("transforms:boundMin" = myTrans@boundMin))
    if (is.finite(myTrans@boundMax)) attrs = append(attrs, c("transforms:boundMax" = myTrans@boundMax))
    gatingMLNode$addNode("transforms:transformation", attrs = attrs, close = FALSE)
    attrs = c("transforms:T" = myTrans@T, "transforms:M" = myTrans@M, "transforms:A" = myTrans@A)
    gatingMLNode$addNode("transforms:fasinh", attrs = attrs)
    gatingMLNode$closeTag() # </transforms:transformation>    
}

# Add an asinht transformation named x to the the Gating-ML node.
# Encode asinht from Gating-ML 1.5 compatible parameterization using Gating-ML 2.0
# compatible parameterization as follows:
#
# asinht (ASinH from Gating-ML 1.5) is defined as 
# f(x) = asinh(a*x)*b
# asinhtGml2 (fasinh from Gating-ML 2.0) is defined as:
# f(x) = (asinh(x*sinh(M*log(10))/T) + A*log(10)) / ((M+A)*log(10))
# Therefore, we will encode asinht as asinhtGml2 by stating
# A = 0
# M = 1 / (b * log(10))
# T = (sinh(1/b)) / a
# which will give us exactly the right transformation in the Gating-ML 2.0 
# compatible parameterization. Btw. log is natural logarithm, i.e., based e
addAsinhtGml1.5 <- function(gatingMLNode, x, flowEnv)
{
    myTrans = objectNameToObject(x, flowEnv)
    if(!is(myTrans, "asinht")) stop(paste("Unexpected object insted of asinht - ", class(myTrans))) 
    addDebugMessage(paste("Working on asinht ", myTrans@transformationId, sep=""), flowEnv)

    myID = getObjectId(myTrans, NULL, flowEnv)
    if(isIdWrittenToXMLAlready(myID, flowEnv)) return(FALSE) 
    
    attrs = c("transforms:id" = myID)
    gatingMLNode$addNode("transforms:transformation", attrs = attrs, close = FALSE)
    
    A = 0
    M = 1 / (myTrans@b * log(10))
    T = (sinh(1/myTrans@b)) / myTrans@a
    attrs = c("transforms:T" = T, "transforms:M" = M, "transforms:A" = A)

    gatingMLNode$addNode("transforms:fasinh", attrs = attrs)
    gatingMLNode$closeTag() # </transforms:transformation>    
}

# Add a hyperlogtGml2 transformation named x to the the Gating-ML node
addHyperlogtGml2 <- function(gatingMLNode, x, flowEnv)
{
    myTrans = objectNameToObject(x, flowEnv)
    if(!is(myTrans, "hyperlogtGml2")) stop(paste("Unexpected object insted of hyperlogtGml2 - ", class(myTrans))) 
    addDebugMessage(paste("Working on hyperlogtGml2 ", myTrans@transformationId, sep=""), flowEnv)
    
    myID = getObjectId(myTrans, NULL, flowEnv)
    if(isIdWrittenToXMLAlready(myID, flowEnv)) return(FALSE) 
    
    attrs = c("transforms:id" = myID)
    if (is.finite(myTrans@boundMin)) attrs = append(attrs, c("transforms:boundMin" = myTrans@boundMin))
    if (is.finite(myTrans@boundMax)) attrs = append(attrs, c("transforms:boundMax" = myTrans@boundMax))
    gatingMLNode$addNode("transforms:transformation", attrs = attrs, close = FALSE)
    attrs = c("transforms:T" = myTrans@T, "transforms:M" = myTrans@M, "transforms:W" = myTrans@W, "transforms:A" = myTrans@A)
    gatingMLNode$addNode("transforms:hyperlog", attrs = attrs)
    gatingMLNode$closeTag() # </transforms:transformation>    
}

# Add a logicletGml2 transformation named x to the the Gating-ML node
addLogicletGml2 <- function(gatingMLNode, x, flowEnv)
{
    myTrans = objectNameToObject(x, flowEnv)
    if(!is(myTrans, "logicletGml2")) stop(paste("Unexpected object insted of logicletGml2 - ", class(myTrans))) 
    addDebugMessage(paste("Working on logicletGml2 ", myTrans@transformationId, sep=""), flowEnv)
    
    myID = getObjectId(myTrans, NULL, flowEnv)
    if(isIdWrittenToXMLAlready(myID, flowEnv)) return(FALSE) 
    
    attrs = c("transforms:id" = myID)
    if (is.finite(myTrans@boundMin)) attrs = append(attrs, c("transforms:boundMin" = myTrans@boundMin))
    if (is.finite(myTrans@boundMax)) attrs = append(attrs, c("transforms:boundMax" = myTrans@boundMax))
    gatingMLNode$addNode("transforms:transformation", attrs = attrs, close = FALSE)
    attrs = c("transforms:T" = myTrans@T, "transforms:M" = myTrans@M, "transforms:W" = myTrans@W, "transforms:A" = myTrans@A)
    gatingMLNode$addNode("transforms:logicle", attrs = attrs)
    gatingMLNode$closeTag() # </transforms:transformation>    
}

# Add a lintGml2 transformation named x to the the Gating-ML node
addLintGml2 <- function(gatingMLNode, x, flowEnv)
{
    myTrans = objectNameToObject(x, flowEnv)
    if(!is(myTrans, "lintGml2")) stop(paste("Unexpected object insted of lintGml2 - ", class(myTrans))) 
    addDebugMessage(paste("Working on lintGml2 ", myTrans@transformationId, sep=""), flowEnv)
    
    myID = getObjectId(myTrans, NULL, flowEnv)
    if(isIdWrittenToXMLAlready(myID, flowEnv)) return(FALSE) 
    
    attrs = c("transforms:id" = myID)
    if (is.finite(myTrans@boundMin)) attrs = append(attrs, c("transforms:boundMin" = myTrans@boundMin))
    if (is.finite(myTrans@boundMax)) attrs = append(attrs, c("transforms:boundMax" = myTrans@boundMax))
    gatingMLNode$addNode("transforms:transformation", attrs = attrs, close = FALSE)
    attrs = c("transforms:T" = myTrans@T, "transforms:A" = myTrans@A)
    gatingMLNode$addNode("transforms:flin", attrs = attrs)
    gatingMLNode$closeTag() # </transforms:transformation>    
}

# Add a logtGml2 transformation named x to the the Gating-ML node
addLogtGml2 <- function(gatingMLNode, x, flowEnv)
{
    myTrans = objectNameToObject(x, flowEnv)
    if(!is(myTrans, "logtGml2")) stop(paste("Unexpected object insted of logtGml2 - ", class(myTrans))) 
    addDebugMessage(paste("Working on logtGml2 ", myTrans@transformationId, sep=""), flowEnv)
    
    myID = getObjectId(myTrans, NULL, flowEnv)
    if(isIdWrittenToXMLAlready(myID, flowEnv)) return(FALSE) 
    
    attrs = c("transforms:id" = myID)
    if (is.finite(myTrans@boundMin)) attrs = append(attrs, c("transforms:boundMin" = myTrans@boundMin))
    if (is.finite(myTrans@boundMax)) attrs = append(attrs, c("transforms:boundMax" = myTrans@boundMax))
    gatingMLNode$addNode("transforms:transformation", attrs = attrs, close = FALSE)
    attrs = c("transforms:T" = myTrans@T, "transforms:M" = myTrans@M)
    gatingMLNode$addNode("transforms:flog", attrs = attrs)
    gatingMLNode$closeTag() # </transforms:transformation>    
}

# Add a ratiotGml2 transformation named x to the the Gating-ML node
addRatiotGml2 <- function(gatingMLNode, x, flowEnv)
{
    myTrans = objectNameToObject(x, flowEnv)
    if(!is(myTrans, "ratiotGml2")) stop(paste("Unexpected object insted of ratiotGml2 - ", class(myTrans))) 
    addDebugMessage(paste("Working on ratiotGml2 ", myTrans@transformationId, sep=""), flowEnv)
    
    myID = getObjectId(myTrans, NULL, flowEnv)
    if(isIdWrittenToXMLAlready(myID, flowEnv)) return(FALSE) 
    
    attrs = c("transforms:id" = myID)
    if (is.finite(myTrans@boundMin)) attrs = append(attrs, c("transforms:boundMin" = myTrans@boundMin))
    if (is.finite(myTrans@boundMax)) attrs = append(attrs, c("transforms:boundMax" = myTrans@boundMax))
    gatingMLNode$addNode("transforms:transformation", attrs = attrs, close = FALSE)
    attrs = c("transforms:A" = myTrans@pA, "transforms:B" = myTrans@pB, "transforms:C" = myTrans@pC)
    gatingMLNode$addNode("transforms:fratio", attrs = attrs, close = FALSE)
    addDimensionContents(gatingMLNode, myTrans@numerator, flowEnv)
    addDimensionContents(gatingMLNode, myTrans@denominator, flowEnv)
    gatingMLNode$closeTag() # </transforms:fratio>
    gatingMLNode$closeTag() # </transforms:transformation>    
}

# Add a ratio transformation (from Gating-ML 1.5) named x 
# to the the Gating-ML node. This will be translated to how "fratio" of Gating-ML 2.0
# (When we set A = 1, B = 0, C = 0 then ratio of Gating-ML 1.5 == fratio of Gating-ML 2.0)
addRatioGml1.5 <- function(gatingMLNode, x, flowEnv)
{
    myTrans = objectNameToObject(x, flowEnv)
    if(!is(myTrans, "ratio")) stop(paste("Unexpected object insted of ratio - ", class(myTrans))) 
    addDebugMessage(paste("Working on ratio ", myTrans@transformationId, sep=""), flowEnv)
    
    myID = getObjectId(myTrans, NULL, flowEnv)
    if(isIdWrittenToXMLAlready(myID, flowEnv)) return(FALSE) 
    
    attrs = c("transforms:id" = myID)
    gatingMLNode$addNode("transforms:transformation", attrs = attrs, close = FALSE)
    attrs = c("transforms:A" = "1", "transforms:B" = "0", "transforms:C" = "0")
    gatingMLNode$addNode("transforms:fratio", attrs = attrs, close = FALSE)
    addDimensionContents(gatingMLNode, myTrans@numerator, flowEnv)
    addDimensionContents(gatingMLNode, myTrans@denominator, flowEnv)
    gatingMLNode$closeTag() # </transforms:fratio>
    gatingMLNode$closeTag() # </transforms:transformation>    
}


# Add a Gating-ML dimension to a Gating-ML node
addDimensions <- function(gatingMLNode, x, flowEnv, quadGateDividerIdBasedName = NULL)
{
    gate = objectNameToObject(x, flowEnv)
    for (i in 1:length(gate@parameters))
    {
        attrs = c()
        parameter = gate@parameters[[i]]

        if (is(gate, "rectangleGate"))
        {
            min = gate@min[[i]]
            max = gate@max[[i]]
            if(min != -Inf) attrs = c(attrs, "gating:min" = min)
            if(max != Inf) attrs = c(attrs, "gating:max" = max)
        }

        if(is(parameter, "transformReference")) parameter = resolveTransformationReference(parameter)
        if(is(parameter, "unitytransform")) attrs = c(attrs, "gating:compensation-ref" = "uncompensated")
        else if(is(parameter, "singleParameterTransform"))
        {
            attrs = c(attrs, "gating:transformation-ref" = filterIdtoXMLId(parameter@transformationId, flowEnv))
            parameter = parameter@parameters
            if(is(parameter, "transformReference")) parameter = resolveTransformationReference(parameter)
            
            if(is(parameter, "unitytransform")) attrs = c(attrs, "gating:compensation-ref" = "uncompensated")
            else if(is(parameter, "compensatedParameter")) attrs = addCompensationRef(attrs, parameter, flowEnv)
            else if(is(parameter, "ratiotGml2") || is(parameter, "ratio")) attrs = addCompensationRef(attrs, parameter@numerator, flowEnv)
            else stop(paste("Unexpected parameter class ", class(parameter), ", compound transformations are not supported in Gating-ML 2.0.", sep=""))
        } 
        else if(is(parameter, "compensatedParameter")) attrs = addCompensationRef(attrs, parameter, flowEnv)
        else if(is(parameter, "ratiotGml2") || is(parameter, "ratio")) attrs = addCompensationRef(attrs, parameter@numerator, flowEnv)
        else stop(paste("Unexpected parameter class", class(parameter), "- not supported in Gating-ML 2.0 output)."))
        
        if(is(gate, "quadGate")) 
        {
            attrs = c(attrs, "gating:id" = paste(quadGateDividerIdBasedName, ".D", i, sep = ""))
            gatingMLNode$addNode("gating:divider", attrs = attrs, close = FALSE)
        }
        else gatingMLNode$addNode("gating:dimension", attrs = attrs, close = FALSE)
        
        addDimensionContents(gatingMLNode, parameter, flowEnv)
        if (is(gate, "quadGate")) gatingMLNode$addNode("gating:value", as.character(gate@boundary[i]))
        gatingMLNode$closeTag() # </gating:dimension> or </gating:divider>
    }
}

# Add the contents of a Gating-ML dimension to a Gating-ML node
addDimensionContents <- function(gatingMLNode, parameter, flowEnv)
{
    newDimension = FALSE
    if(is(parameter, "compensatedParameter")) 
    {
        if (parameter@spillRefId == "SpillFromFCS") 
            attrs = c("data-type:name" = parameter@parameters)
        else 
            attrs = c("data-type:name" = parameter@transformationId)
    }
    else if(is(parameter, "unitytransform")) attrs = c("data-type:name" = parameter@parameters)
    else if(is(parameter, "character")) attrs = c("data-type:name" = parameter)
    else if(is(parameter, "ratiotGml2") || is(parameter, "ratio")) {
        attrs = c("data-type:transformation-ref" = parameter@transformationId)
        newDimension = TRUE
    }
    else stop(paste("Unrecognized parameter type, class ", class(parameter), ". Note that compound transformations are not supported in Gating-ML 2.0.", sep=""))
    
    if(newDimension)
        gatingMLNode$addNode("data-type:new-dimension", attrs = attrs)
    else
        gatingMLNode$addNode("data-type:fcs-dimension", attrs = attrs)
}

# This converts the identifier to an XML safe identifier and also,
# if it is a singleParameterTransform and we have a different 
# 'representative' transform for those (saved in flowEnv[['.singleParTransforms']])
# then the identifier of the representative is used instead.
filterIdtoXMLId <- function(x, flowEnv)
{
    if(!(is.character(x))) stop(paste("Object of class", class(x), "cannot be converted to an XML identifier."))
    if(length(x) <= 0) stop(paste("An empty string cannot be converted to an XML identifier."))
    
    # First, if it is a singleParameterTransform then check for a representative and use it instead eventually
    trEnv = flowEnv[['.singleParTransforms']]
    trans = flowEnv[[x]]
    if(!is.null(trEnv) && !is.null(trans) && is(trans, "singleParameterTransform"))
    {
        key = createTransformIdentifier(trans)
        if (!is.null(trEnv[[key]])) x = trEnv[[key]]        
    }
    
    # Now make it a safe XML identifier
    # 1) Put an underscore prefix if it starts with a number 
    if(substr(x, 1, 1) >= "0" && substr(x, 1, 1) <= "9") x = paste("_", x, sep="")
    # 2) Replace 'strange characters with '.'
    for(i in 1:nchar(x)) {
        if(!isNCNameChar(substr(x, i, i))) x <- paste(substr(x, 0, i - 1), '.', substr(x, i + 1, nchar(x)), sep= "")
    }
    x
}

# Return true if you are sure that the character is safe to be placed in
# an XML identifier.
isNCNameChar <- function(char)
{
    # Based on the ASCII table and XML NCName syntax
    asciiValue = as.numeric(charToRaw(char))
    if(asciiValue < 45) return(FALSE) 
    if(asciiValue == 47) return(FALSE)
    if(asciiValue >= 58 && asciiValue <= 64) return(FALSE)
    if(asciiValue >= 91 && asciiValue <= 94) return(FALSE)
    if(asciiValue == 96) return(FALSE)
    if(asciiValue >= 123) return(FALSE)
    TRUE    
}

# Returns TRUE if and only if x is a singleParameterTransform
# and there is another equivalent singleParameterTransform
# in flowEnv that is the chosen representative among all
# equivalent transforms. This is used to merge transforms
# for Gating-ML 2.0 output since in Gating-ML 2.0, the same
# transformation is applicable to many FCS parameters. For us,
# the transformation with the shortest identifier is the chosen
# representative. This function requires the flowEnv[['.singleParTransforms']]
# to be set by calling the collectTransform function on all available
# transforms before shouldTransformationBeSkipped can be used.
shouldTransformationBeSkipped <- function(x, flowEnv)
{
    trEnv = flowEnv[['.singleParTransforms']]
    trans = flowEnv[[x]]
    if(!is.null(trEnv) && !is.null(trans) && is(trans, "singleParameterTransform"))
    {
        key = createTransformIdentifier(trans)
        if (!is.null(trEnv[[key]])){
            if (x == trEnv[[key]]) FALSE
            else TRUE
        } else FALSE
    } else FALSE
}

# Resolve transformation reference, return the transformation that the
# reference is pointing to.
resolveTransformationReference <- function(trRef)
{
    if(!is(trRef, "transformReference")) 
        stop(paste("Cannot call resolveTransformationReference on", class(trRef)))
    if(exists(trRef@transformationId, envir=trRef@searchEnv, inherits=FALSE))
        trRef@searchEnv[[trRef@transformationId]]
    else
        stop(paste("Cannot find", trRef@transformationId, "in the environment."))
}

# This will create an identifier of a singleParameterTransform that
# is based on the class and slot values, such as T, M, W, A, etc. as applicable
# for the various single parameter transformations. We will use this to 
# merge "the same transformations" applied to different FCS parameter into a single
# transformation in the Gating-ML 2.0 output.
createTransformIdentifier <- function(trans)
{
    name <- class(trans)
    for (slotName in slotNames(trans))
    {
        if(slotName != ".Data" && slotName != "parameters" && slotName != "transformationId")
        {
            slotValue = slot(trans, slotName)
            if(is(slotValue, "numeric") || is(slotValue, "character"))
            {
                name <- paste(name, slotName, slot(trans, slotName), sep = "_")
            }
        }
    }
    name
}

# The flowEnv[['.singleParTransforms']] environment will serve as a hashmap
# with keys based on values returned by createTransformIdentifier and
# values being the shortest transformationId value of all the transformations
# matching that key. That way, we can merge all these transformations into
# a single one in Gating-ML.
collectTransform <- function(x, flowEnv)
{
    trEnv = flowEnv[['.singleParTransforms']]
    trans = flowEnv[[x]]
    key = createTransformIdentifier(trans)
    if (is.null(trEnv[[key]]) || length(trEnv[[key]]) > trans@transformationId) trEnv[[key]] = trans@transformationId   
}

# Add a debug message to out list of debug messages in flowEnv[['.debugMessages']]
addDebugMessage <- function(msg, flowEnv)
{
    flowEnv[['.debugMessages']] = c(flowEnv[['.debugMessages']], msg)
}

# Return TRUE of the provided id has been checked (and supposedly written)
# before. Otherwise, add the id to the list in flowEnv[['.objectIDsWrittenToXMLOutput']]
# and retusn FALSE. This function is used to prevent writing multiple objects
# with the same ID to the Gating-ML output in case a gate or transformation
# with the same ID is stored several times in the flowEnv.
isIdWrittenToXMLAlready <- function(id, flowEnv)
{
    idsList = flowEnv[['.objectIDsWrittenToXMLOutput']]
    if (is.null(idsList[[id]])) {
        idsList[[id]] = TRUE
        flowEnv[['.objectIDsWrittenToXMLOutput']] = idsList 
        FALSE
    } else {
        addDebugMessage(paste("ID", id, "should be in the Gating-ML file already."), flowEnv)
        TRUE
    }
}

# Add an appropriate gating:compensation-ref attribute to the passed attrs
addCompensationRef <- function(attrs, parameter, flowEnv)
{
    if(is(parameter, "unitytransform")) attrs = c(attrs, "gating:compensation-ref" = "uncompensated")
    else if(is(parameter, "compensatedParameter")) 
    {
        if (parameter@spillRefId != "SpillFromFCS")
            attrs = c(attrs, "gating:compensation-ref" = filterIdtoXMLId(parameter@spillRefId, flowEnv))
        else 
            attrs = c(attrs, "gating:compensation-ref" = "FCS")
    }
    else stop(paste("Unexpected parameter class", class(parameter)))
    
    attrs
}

# Add to attrs the gating:min and/or gating:max attributes 
# based on dimension number i of a rectangle gate gate.
addRectGateMinMax <- function(attrs, gate, i)
{
    if (is(gate, "rectangleGate"))
    {
        min = gate@min[[i]]
        max = gate@max[[i]]
        if(min != -Inf) attrs = c(attrs, "gating:min" = min)
        if(max != Inf) attrs = c(attrs, "gating:max" = max)
    } else stop(paste("Unexpected gate class", class(gate), "- expected a rectangleGate."))

    attrs
}

# Get the XML compliant identifier of an object. This only works for object of type
# "filter", "transform" or "compensation". The filterIdtoXMLId function is incorporated,
# which includes the use of representative singleParameterTransforms instead of a different
# transform whenever it is applied to a different FCS parameter.
getObjectId <- function(object, forceGateId, flowEnv)
{
    if (is(object, "filter")) {
        if (is.null(forceGateId)) myID = filterIdtoXMLId(object@filterId, flowEnv)
        else myID = filterIdtoXMLId(forceGateId, flowEnv)    
    } else if (is(object, "transform")) {
        if (is.null(forceGateId)) myID = filterIdtoXMLId(object@transformationId, flowEnv)
        else myID = filterIdtoXMLId(forceGateId, flowEnv)
    } else if (is(object, "compensation")) {
        if (is.null(forceGateId)) myID = filterIdtoXMLId(object@compensationId, flowEnv)
        else myID = filterIdtoXMLId(forceGateId, flowEnv)
    }
    
    else stop(paste("Unexpected object to get id from, class", class(object)))
    myID
}

# If x is character then return flowEnv[[x]], otherwise return x
objectNameToObject <- function(x, flowEnv) 
{
    if(is(x, "character")) flowEnv[[x]]
    else x
}

# Check object named x in flowEnv and make sure
# flowEnv contains objects referenced from x, such as parameter
# transformations used in x. If objects are missing then
# add them to flowEnv and keep track of what has been
# added in the flowEnv[['.addedObjects']] list so that it can be
# removed at the end of the write.gatingML function. 
addReferencedObjectsToEnv <- function(x, flowEnv) 
{
    object = objectNameToObject(x, flowEnv)
    if(is(object, "parameterFilter")) 
        for(par in object@parameters) doubleCheckExistanceOfParameter(par, flowEnv)
    else if (is(object, "singleParameterTransform")) 
        doubleCheckExistanceOfParameter(object@parameters, flowEnv)
    else if (is(object, "setOperationFilter"))
        for(filt in object@filters) doubleCheckExistanceOfFilter(filt, flowEnv)
        
}

# If par is a transform then check whether it exists in the flowEnv environment, 
# and if it doesn't then add it there and make a note of it in flowEnv[['.addedObjects']]
doubleCheckExistanceOfParameter <- function(par, flowEnv)
{
    if(is(par, "transform")) 
    {
        if(!is.null(par@transformationId) && par@transformationId != "" && !exists(par@transformationId, envir=flowEnv, inherits=FALSE)) 
        {
            flowEnv[[par@transformationId]] <- par
            flowEnv[['.addedObjects']][[par@transformationId]] <- par@transformationId
            addReferencedObjectsToEnv(par@transformationId, flowEnv)
        }    
    }
}

# If filt is a concreteFilter then check whether it exists in the flowEnv environment, 
# and if it doesn't then add it there and make a note of it in flowEnv[['.addedObjects']]
doubleCheckExistanceOfFilter <- function(filt, flowEnv)
{
    if(is(filt, "concreteFilter")) 
    {
        if(!is.null(filt@filterId) && filt@filterId != "" && !exists(filt@filterId, envir=flowEnv, inherits=FALSE)) 
        {
            flowEnv[[filt@filterId]] <- filt
            flowEnv[['.addedObjects']][[filt@filterId]] <- filt@filterId
            addReferencedObjectsToEnv(filt@filterId, flowEnv)
        }    
    }
}