//
// Copyright (c) Lenovo, 2012. All rights reserved.
//
// The structure and organization of this software is valuable and confidential
// property of the Lenovo Corporation.
// This code is not be modified, adapted, sold or used as the basis of
// derivative works or commercial products without the express written
// permission of the Lenovo Corporation.
//
// File Name:
//
//    constraints.js
//
// Version:
//
//    0.20
//
// Abstract:
//
//    Javascript constraints file for Lenovo v4 printer drivers.
//

var pskPrefix = "psk";
var pskNs = "http://schemas.microsoft.com/windows/2003/08/printing/printschemakeywords";
var psfPrefix = "psf";
var psfNs = "http://schemas.microsoft.com/windows/2003/08/printing/printschemaframework";
var xsdPrefix = "xsd";
var xsdNs = "http://www.w3.org/2001/XMLSchema";
var xsiPrefix = "xsi";
var xsiNs = "http://www.w3.org/2001/XMLSchema-instance";
var saPrefix = "ns0000";
var saNs = "http://schemas.microsoft.com/windows/2003/08/printing/LENOVOPRINTER";

var NODE_ELEMENT = 1;
var NODE_ATTRIBUTE = 2;

var namespaces = {};
namespaces[psfPrefix] = psfNs;
namespaces[pskPrefix] = pskNs;
namespaces[xsdPrefix] = xsdNs;
namespaces[xsiPrefix] = xsiNs;
namespaces[saPrefix] = saNs;
var prefixes = {};
prefixes[psfNs] = psfPrefix;
prefixes[pskNs] = pskPrefix;
prefixes[xsdNs] = xsdPrefix;
prefixes[xsiNs] = xsiPrefix;
prefixes[saNs] = saPrefix;

var PREFIX_CANONICAL = "canonical";
var PREFIX_REAL = "real";

if (!String.prototype.trim) {
    String.prototype.trim = function () {
        return this.replace(/^\s+|\s+$/g, '');
    };
}

var PrintSchemaSelectionType_PickOne = 0;
var PrintSchemaSelectionType_PickMany = 1;

function validatePrintTicket(printTicket, scriptContext) {
    //Debug.writeln("<!-- validatePrintTicket, PT In: -->\r\n", printTicket.XmlNode.xml);
    var result = validatePrintTicket2(printTicket, scriptContext);
    //Debug.writeln("<!-- validatePrintTicket, PT Out: -->\r\n", printTicket.XmlNode.xml);
    return result;
}
function validatePrintTicket2(printTicket, scriptContext) {
    /// <summary>
    ///     Validates a print ticket.
    /// </summary>
    /// <param name="printTicket" type="IPrintSchemaTicket">
    ///     Print ticket to be validated.
    /// </param>
    /// <param name="scriptContext" type="IPrinterScriptContext">
    ///     Script context object.
    /// </param>
    /// <returns type="Number" integer="true">
    ///     Integer value indicating validation status.
    ///         retval 1 - Print ticket is valid and was not modified.
    ///         retval 2 - Print ticket was modified to make it valid.
    ///         retval 0 - Print ticket is invalid  (not demonstrated by this example).
    /// </returns>

    debugger;

    var devMode = GetDecodedDevmode(printTicket);
    //return 1;

    var retVal = 1;

    // Set the selection namespace on the printTicket's XmlNode. This instance allows us to query for
    // nodes belonging to the 'psfNs' namespace
    setSelectionNamespace(
        printTicket.XmlNode,
        psfPrefix,
        psfNs);

    var printCapabilities = null; //printTicket.GetCapabilities();

    var lockedFeaturesString = safeGetString(scriptContext.QueueProperties, "LockedFeatures");
    var lockedFeatures = parseNameValuePairsString2(lockedFeaturesString);
    var defaultOverridesString = safeGetString(scriptContext.QueueProperties, "DefaultOverrides");
    var defaultOverrides = parseNameValuePairsString2(defaultOverridesString);
    var defaultValuesString = safeGetString(scriptContext.DriverProperties, "DefaultValues");
    var defaultValues = parseNameValuePairsString2(defaultValuesString);

    var constraintsString = safeGetString(scriptContext.QueueProperties, "Constraints") || safeGetString(scriptContext.DriverProperties, "Constraints");
    var constraints = parseConstraints(constraintsString);

    var featureNodes = printTicket.XmlNode.selectNodes("//" + psfPrefix + ":Feature");
    var featureValues = makeFeatureValuesMap(printTicket, printCapabilities, featureNodes);

    var featureNodesToRemove = [];

    for (var i = 0; i < featureNodes.length; ++i) {
        var featureNode = featureNodes.item(i);
        var featureName = getElementName(featureNode, PREFIX_CANONICAL);
        //var feature = getFeature(printTicket, featureName, PREFIX_REAL);
        var pcFeature = printCapabilities && getFeature(printCapabilities, featureName, PREFIX_REAL);
        //if (!feature)
        //    continue;
        if (pcFeature && pcFeature.SelectionType != PrintSchemaSelectionType_PickOne)
            continue;

        var resetFeature = false;

        if (lockedFeatures.hasOwnProperty(featureName))
            resetFeature = true;

        if (!resetFeature && isFeatureValueConstrained(featureName, featureValues, constraints))
            resetFeature = true;

        //switch (featureName) {
        //    case "psk:PageOrientation": {
        //        var mediaSizeValue = featureValues["psk:PageMediaSize"];
        //        if (mediaSizeValue == "psk:CustomMediaSize") {
        //            var widthParameterInitNode = getParameterInit(printTicket.XmlNode, pskNs, "PageMediaSizeMediaSizeWidth");
        //            var heightParameterInitNode = getParameterInit(printTicket.XmlNode, pskNs, "PageMediaSizeMediaSizeHeight");
        //            var width = parseInt(GetPropertyValue(widthParameterInitNode));
        //            var height = parseInt(GetPropertyValue(heightParameterInitNode));
                    
        //            var widthParameterDefNode = getParameterDef(printCapabilities.XmlNode, pskNs, "PageMediaSizeMediaSizeWidth");
        //            var heightParameterDefNode = getParameterDef(printCapabilities.XmlNode, pskNs, "PageMediaSizeMediaSizeHeight");
        //            var widthParameterDef = parseParameterDef(widthParameterDefNode);
        //            var heightParameterDef = parseParameterDef(heightParameterDefNode);

        //            if (width < widthParameterDef.MinValue || width > widthParameterDef.MaxValue) {
        //                setPropertyValue(widthParameterInitNode, widthParameterDef.DefaultValue);
        //                retVal = 2;
        //            }
        //            if (height < heightParameterDef.MinValue || height > heightParameterDef.MaxValue) {
        //                setPropertyValue(heightParameterInitNode, heightParameterDef.DefaultValue);
        //                retVal = 2;
        //            }
        //        }
        //        break;
        //    }
        //}

        if (resetFeature) {
            var defaultValue = defaultOverrides[featureName] || lockedFeatures[featureName] || defaultValues[featureName];

            if (defaultValue) {
                var currentValue = featureValues[featureName];
                if (currentValue != defaultValue) {
                    removeChildElements(featureNode, psfPrefix + ":Option");
                    var values = defaultValue.split(",");
                    forEach(values, function (value) {
                        addChildElement(featureNode, psfNs, "Option", value);
                    });
                    retVal = 1;
                }
            }
            else {
                featureNodesToRemove.push(featureNode);
                retVal = 1;
            }

            //var options = printCapabilities.GetOptions(feature);
            //var defaultOption = defaultValue && defaultValue.name && defaultValue.ns ?
            //    pcFeature.GetOption(defaultValue.name, defaultValue.ns) : null;
            //if (!defaultOption) {
            //    defaultOption = options.GetAt(0);
            //}
            //if (selectedOption.Name != defaultOption.Name || selectedOption.NamespaceUri != defaultOption.NamespaceUri) {
            //    setSelectedOptionName(feature, defaultOption.NamespaceUri, defaultOption.Name);
            //    retVal = 2;
            //}
        }
    }

    for (var i = 0; i < featureNodesToRemove.length; ++i) {
        removeElement(featureNodesToRemove[i]);
    }

    return retVal;
}

function completePrintCapabilities(printTicket, scriptContext, printCapabilities) {
    //Debug.writeln("<!-- completePrintCapabilities -->");
    completePrintCapabilities2(printTicket, scriptContext, printCapabilities);
}
function completePrintCapabilities2(printTicket, scriptContext, printCapabilities) {
    /// <summary>
    ///     This API is called to allow the PrintCapabilities object to be modified.
    /// </summary>
    /// <param name="printTicket" type="IPrintSchemaTicket" mayBeNull="true">
    ///     If not 'null', the print ticket's settings are used to customize the print capabilities.
    /// </param>
    /// <param name="scriptContext" type="IPrinterScriptContext">
    ///     Script context object.
    /// </param>
    /// <param name="printCapabilities" type="IPrintSchemaCapabilities">
    ///     Print capabilities object to be customized.
    /// </param>

    debugger;
    //return;

    var printCaps = getPrintCaps(scriptContext);
    AddFeaturesAndParameters(printCapabilities, printCaps);

    if (!printTicket) {
        return;
    }

    var devMode = GetDecodedDevmode(printTicket);

    setSelectionNamespace(
        printTicket.XmlNode,
        psfPrefix,
        psfNs);

    setSelectionNamespace(
        printCapabilities.XmlNode,
        psfPrefix,
        psfNs);

    // mark locked features
    var lockedFeaturesString = safeGetString(scriptContext.QueueProperties, "LockedFeatures");
    var lockedFeatures = parseNameValuePairsString(lockedFeaturesString);
    var defaultOverridesString = safeGetString(scriptContext.QueueProperties, "DefaultOverrides");
    var defaultOverrides = parseNameValuePairsString(defaultOverridesString);
    var defaultValuesString = safeGetString(scriptContext.DriverProperties, "DefaultValues");
    var defaultValues = parseNameValuePairsString(defaultValuesString);
    for (var featureName in lockedFeatures) {
        var feature = getFeature(printCapabilities, featureName);
        if (feature) {
            var options = printCapabilities.GetOptions(feature);
            var defaultValue = defaultOverrides[featureName] || lockedFeatures[featureName] || defaultValues[featureName];
            var defaultOption = defaultValue && defaultValue.name && defaultValue.ns ?
                feature.GetOption(defaultValue.name, defaultValue.ns) : null;
            if (!defaultOption) {
                defaultOption = options.GetAt(0);
            }
            for (var j = 0; j < options.Count; ++j) {
                var option = options.GetAt(j);
                if (option.Name == defaultOption.Name && option.NamespaceUri == defaultOption.NamespaceUri)
                    continue;
                option.XmlNode.setAttribute("constrained", NameWithNs(printCapabilities.XmlNode, pskNs, "AdminSettings"));
            }
        }
    }
}

function convertDevModeToPrintTicket(devModeProperties, scriptContext, printTicket) {
    //Debug.writeln("<!-- convertDevModeToPrintTicket, PT In: -->\r\n", printTicket.XmlNode.xml);
    convertDevModeToPrintTicket2(devModeProperties, scriptContext, printTicket);
    //Debug.writeln("<!-- convertDevModeToPrintTicket, PT Out: -->\r\n", printTicket.XmlNode.xml);
}
function convertDevModeToPrintTicket2(devModeProperties, scriptContext, printTicket) {
    /// <summary>
    ///     This API is called to convert values from the DEVMODE property bag into a PrintTicket.
    /// </summary>
    /// <param name="devModeProperties" type="IPrinterScriptablePropertyBag">
    ///     The object that represents the DEVMODE property bag.
    /// </param>
    /// <param name="scriptContext" type="IPrinterScriptContext">
    ///     Script context object.
    /// </param>
    /// <param name="printTicket" type="IPrintSchemaTicket">
    ///     PrintTicket object.
    /// </param>

    debugger;

    var devMode = GetDecodedDevmode(printTicket);

    setSelectionNamespace(
        printTicket.XmlNode,
        psfPrefix,
        psfNs);

    var value = devModeProperties.GetString("AllValues");

    //Debug.writeln("<!-- convertDevModeToPrintTicket, DM In: -->\r\n<AllValues>\r\n", value, "</AllValues>\r\n");

    var values = parseNameValuePairsString2(value);

    var printCaps = getPrintCaps(scriptContext);

    UnpackFeatureValues(values, printCaps, featurePackingDefinitions);

    if (true) {
        // special support for psk:PageResolution because it does not distinguish options with same resolution
        var qualityValue = GetSimpleFeatureValue(printTicket, "psk:PageOutputQuality");
        if (qualityValue) {
            values["psk:PageResolution"] = qualityValue;
            delete values["psk:PageResolution/psk:ResolutionX"];
            delete values["psk:PageResolution/psk:ResolutionY"];
            values["psk:PageOutputQuality"] = qualityValue;
            delete values["psk:PageOutputQuality/ns0000:IResolution"]; // HACK: hardcoded property names
            delete values["psk:PageOutputQuality/ns0000:BPP"];
            delete values["psk:PageOutputQuality/ns0000:ImageQuality"];
        }
    }

    AddFeaturesAndParameters(printTicket, printCaps, values);

    if (true) {
        // special support for Duplex feature
        var featureNode = getFeatureNode(printTicket.XmlNode, "psk:JobDuplexAllDocumentsContiguously", PREFIX_CANONICAL);
        if (featureNode) {
            var optionNode = getSelectedOptionNode(featureNode);
            if (optionNode) {
                var scoredPropDef = { name: "psk:DuplexMode", value: "Auto", type: xsdPrefix + ":string" };
                var propertyNode = addChildElement(optionNode, psfNs, "ScoredProperty", scoredPropDef.name);
                AddValue(propertyNode, scoredPropDef.value, scoredPropDef.type);
            }
        }
    }
}

function GetSimpleFeatureValue(printTicket, featureName) {
    var featureNode = getFeatureNode(printTicket.XmlNode, featureName, PREFIX_CANONICAL);
    if (featureNode) {
        var value = getSelectedOptionName(featureNode, PREFIX_CANONICAL);
        return value;
    }
    return null;
}

function convertPrintTicketToDevMode(printTicket, scriptContext, devModeProperties) {
    //Debug.writeln("<!-- convertPrintTicketToDevMode, PT In: -->\r\n", printTicket.XmlNode.xml);
    convertPrintTicketToDevMode2(printTicket, scriptContext, devModeProperties);
}
function convertPrintTicketToDevMode2(printTicket, scriptContext, devModeProperties) {
    /// <summary>
    ///     This API is called to convert values from a PrintTicket into the DEVMODE property bag.
    /// </summary>
    /// <param name="printTicket" type="IPrintSchemaTicket">
    ///     PrintTicket object.
    /// </param>
    /// <param name="scriptContext" type="IPrinterScriptContext">
    ///     Script context object.
    /// </param>
    /// <param name="devModeProperties" type="IPrinterScriptablePropertyBag">
    ///     The object that represents the DEVMODE property bag.
    /// </param>

    debugger;

    var devMode = GetDecodedDevmode(printTicket);

    setSelectionNamespace(
        printTicket.XmlNode,
        psfPrefix,
        psfNs);

    var prevValue = devModeProperties.GetString("AllValues");

    var values = {};

    var printCaps = getPrintCaps(scriptContext);

    ExtractFeatureValues(printTicket, printCaps.features, values);
    ExtractParameterValues(printTicket, printCaps.paramDefs, values);

    PackFeatureValues(values, printCaps, featurePackingDefinitions);

    var newValue = makeNameValuePairsString(values);

    //Debug.writeln("<!-- convertPrintTicketToDevMode, DM Out: -->\r\n<AllValues>\r\n", newValue, "</AllValues>\r\n");

    if (prevValue != newValue) {
        devModeProperties.SetString("AllValues", newValue);
    }
}

function ExtractFeatureValues(printTicket, featureDefs, values) {
    for (var i = 0; i < featureDefs.length; ++i) {
        var featureDef = featureDefs[i];
        if (!featureDef)
            continue;

        var featureNode = getFeatureNode(printTicket.XmlNode, featureDef.name, PREFIX_CANONICAL);

        if (featureNode) {
            if (featureDef.options) {
                var featureName = featureDef.name;
                var optionNodes = featureNode.selectNodes(psfPrefix + ":Option");
                for (var j = 0; j < optionNodes.length; ++j) {
                    var optionNode = optionNodes.item(j);
                    var value = getElementName(optionNode, PREFIX_CANONICAL);
                    if (value != null) {
                        if (featureDef.pickMany) {
                            values[featureName + "/" + j] = value;
                        }
                        else {
                            values[featureName] = value;
                        }
                    }

                    ExtractScoredPropertyValues(featureDef, optionNode, j, featureDef.options, value, values)

                    if (!featureDef.pickMany)
                        break;
                }
            }

            if (featureDef.subFeatures)
                ExtractFeatureValues(printTicket, featureDef.subFeatures, values);
        }
    }
}
function ExtractParameterValues(printTicket, paramDefs, values) {
    for (var i = 0; i < paramDefs.length; ++i) {
        var paramDef = paramDefs[i];
        if (!paramDef)
            continue;

        var paramInitNode = getParameterInitNode(printTicket.XmlNode, paramDef.name, PREFIX_CANONICAL);

        if (paramInitNode) {
            var value = GetPropertyValue(paramInitNode);
            if (value != null) {
                values[paramDef.name] = value;
            }
        }
    }
}
function ExtractScoredPropertyValues(featureDef, optionNode, optionNumber, optionDefs, optionName, values) {
    var optionDef = findInArray(optionDefs, function (o) {
        if (o.name)
            return o.name == optionName;
        else if (o.scoredProps) {
            var allMatch = true;
            forEach(o.scoredProps, function (scoredPropDef) {
                /*if (!scoredPropDef.paramRef)*/ { // FUI can generate ScoredProperty with Value even if it is with ParameterRef in PrintCapabilities.
                    var scoredPropValue = GetScoredPropertyValue(optionNode, scoredPropDef.name);
                    if (scoredPropDef.value && scoredPropValue != scoredPropDef.value) {
                        allMatch = false;
                        return false; // break
                    }
                }
            });
            return allMatch;
        }
        return false;
    });
    if (optionDef && optionDef.scoredProps) {
        forEach(optionDef.scoredProps, function (scoredPropDef) {
            /*if (!scoredPropDef.paramRef)*/ { // FUI can generate ScoredProperty with Value even if it is with ParameterRef in PrintCapabilities.
                var scoredPropValue = GetScoredPropertyValue(optionNode, scoredPropDef.name);
                if (scoredPropValue != null) {
                    var scoredPropFullName = getScoredPropertyFullName(featureDef, scoredPropDef.name, optionNumber);
                    values[scoredPropFullName] = scoredPropValue;
                }
            }
        });
    }
    else if (!optionDef) {
        // HACK: Support for options not defined in PrintCaps (like custom media size)
        var scoredPropNodes = optionNode.selectNodes(psfPrefix + ":ScoredProperty");
        var scoredPropNames = []
        for (var j = 0; j < scoredPropNodes.length; ++j) {
            var scoredPropNode = scoredPropNodes.item(j);
            var scoredPropName = getElementName(scoredPropNode, PREFIX_CANONICAL);
            var scoredPropFullName = getScoredPropertyFullName(featureDef, scoredPropName, optionNumber);
            var scoredPropValue = GetPropertyValue(scoredPropNode);
            if (scoredPropValue != null) {
                values[scoredPropFullName] = scoredPropValue;
            }
            else {
                var paramRefNode = scoredPropNode.selectSingleNode(psfPrefix + ":ParameterRef");
                if (paramRefNode != null) {
                    var paramRefName = getElementName(paramRefNode, PREFIX_CANONICAL);
                    values[getScoredPropRefName(scoredPropFullName)] = paramRefName;
                }
            }
            scoredPropNames.push(scoredPropName);
        }
        if (scoredPropNames.length > 0) {
            values[getScoredPropsArrayName(featureDef, optionNumber)] = scoredPropNames.join(",");
        }
    }
}
function GetScoredPropertyValue(optionNode, name) {
    var scoredPropertyNode = findElementNode(optionNode, psfPrefix + ":ScoredProperty", name, PREFIX_CANONICAL);
    if (!scoredPropertyNode)
        return null;
    var value = GetPropertyValue(scoredPropertyNode);
    return value;
}

var featurePackingDefinitions = {
    "ns0000:DocumentInsertPages":
        [
            { propName: "ns0000:InsertMediaTypeID", targetFeature: "psk:PageMediaType", targetProp: "ns0000:MediaID", isId: true },
            { propName: "ns0000:InsertMediaType", targetFeature: "psk:PageMediaType", targetProp: null, isId: false, dictBy: "ns0000:InsertMediaTypeID" }, // "dictBy" property should go after its corresponding ID-property
            { propName: "ns0000:InsertMediaSourceID", targetFeature: "psk:PageInputBin", targetProp: "ns0000:PageInputBinID", isId: true },
            { propName: "ns0000:InsertMediaSource", targetFeature: "psk:PageInputBin", targetProp: null, isId: false, dictBy: "ns0000:InsertMediaSourceID" }, // "dictBy" property should go after its corresponding ID-property
            { propName: "ns0000:InsertPageNumber", targetFeature: null, targetProp: null, isId: true },
        ],
    "ns0000:DocumentExceptPages":
        [
            { propName: "ns0000:ExceptMediaTypeID", targetFeature: "psk:PageMediaType", targetProp: "ns0000:MediaID", isId: true },
            { propName: "ns0000:ExceptMediaType", targetFeature: "psk:PageMediaType", targetProp: null, isId: false, dictBy: "ns0000:ExceptMediaTypeID" }, // "dictBy" property should go after its corresponding ID-property
            { propName: "ns0000:ExceptMediaSourceID", targetFeature: "psk:PageInputBin", targetProp: "ns0000:PageInputBinID", isId: true },
            { propName: "ns0000:ExceptMediaSource", targetFeature: "psk:PageInputBin", targetProp: null, isId: false, dictBy: "ns0000:ExceptMediaSourceID" }, // "dictBy" property should go after its corresponding ID-property
            { propName: "ns0000:ExceptStartPage", targetFeature: null, targetProp: null, isId: true },
            { propName: "ns0000:ExceptEndPage", targetFeature: null, targetProp: null, isId: true },
            { propName: "ns0000:ExceptDuplex", targetFeature: null, targetProp: null, isId: true },
        ]
};

function PackFeatureValues(values, printCaps, featurePackingDefinitions) {
    for (var featureName in featurePackingDefinitions) {
        var packPropsDefs = featurePackingDefinitions[featureName];

        var featureDef = findInArray(printCaps.features, function (featureDef) { return featureDef.name == featureName });
        if (!featureDef || !featureDef.pickMany)
            continue;

        var packedValues = [];
        var dicts = {};
        var savedValues = {};

        for (var optionNumber = 0; ; ++optionNumber) {
            var propValues = [];
            var optionFound = false;
            for (var i = 0; i < packPropsDefs.length; ++i) {
                var packDef = packPropsDefs[i];
                if (!packDef)
                    break;
                var propFullName = getScoredPropertyFullName(featureDef, packDef.propName, optionNumber);
                var propValue = values[propFullName];
                if (propValue) {
                    delete values[propFullName];
                    savedValues[propFullName] = propValue;
                    optionFound = true;
                }
                if (packDef.isId) {
                    propValues.push(propValue || "");
                }
                else if (packDef.dictBy) {
                    var keyFullName = getScoredPropertyFullName(featureDef, packDef.dictBy, optionNumber);
                    var key = savedValues[keyFullName] || values[keyFullName];
                    if (key) {
                        var dictForProp = dicts[packDef.propName];
                        if (!dictForProp) {
                            dictForProp = {};
                            dicts[packDef.propName] = dictForProp;
                        }
                        dictForProp[key] = propValue;
                    }
                }
            }

            if (!optionFound)
                break;

            var packedValue = propValues.join("_");
            packedValues.push(packedValue);
        }

        var packedValueString = packedValues.join(",");
        values[featureName + "/packed"] = packedValueString;

        for (var propName in dicts) {
            var dictForProp = dicts[propName];
            var dictValuesString = makeNameValuePairsString(dictForProp, ";");
            values[featureName + "/dict/" + propName] = dictValuesString;
        }
    }
}

function UnpackFeatureValues(values, printCaps, featurePackingDefinitions) {
    for (var featureName in featurePackingDefinitions) {
        var packPropsDefs = featurePackingDefinitions[featureName];

        var featureDef = findInArray(printCaps.features, function (featureDef) { return featureDef.name == featureName });
        if (!featureDef || !featureDef.pickMany)
            continue;

        var packedFeatureName = featureName + "/packed";
        var packedValueString = values[packedFeatureName];
        if (!packedValueString)
            continue;

        delete values[packedFeatureName];

        var packedValues = packedValueString.split(",");
        var dicts = {};

        for (var optionNumber = 0; optionNumber < packedValues.length; ++optionNumber) {
            var packedValue = packedValues[optionNumber];
            var propValues = packedValue.split("_");
            var propValueIndex = 0;
            for (var i = 0; i < packPropsDefs.length && propValueIndex < propValues.length; ++i) {
                var packDef = packPropsDefs[i];
                if (!packDef)
                    break;
                var propFullName = getScoredPropertyFullName(featureDef, packDef.propName, optionNumber);
                if (packDef.isId) {
                    var propValue = propValues[propValueIndex++];
                    if (propValue) {
                        values[propFullName] = propValue;
                    }
                }
                else if (packDef.dictBy) {
                    var keyFullName = getScoredPropertyFullName(featureDef, packDef.dictBy, optionNumber);
                    var key = values[keyFullName]; // HACK: assumes that its corresponding ID-property is already unpacked
                    if (key) {
                        var dictForProp = dicts[packDef.propName];
                        if (!dictForProp) {
                            var dictName = featureName + "/dict/" + packDef.propName;
                            var dictValuesString = values[dictName];
                            delete values[dictName];
                            if (dictValuesString) {
                                dictForProp = parseNameValuePairsString2(dictValuesString);
                                dicts[packDef.propName] = dictForProp;
                            }
                        }
                        if (dictForProp) {
                            var propValue = dictForProp[key];
                            if (propValue) {
                                values[propFullName] = propValue;
                            }
                        }
                    }
                }
            }
        }
    }
}

/**************************************************************
*                                                             *
*              Print Capabilities                             *
*                                                             *
**************************************************************/

function getPrintCaps(scriptContext) {
    var printCapsJson = safeGetString(scriptContext.DriverProperties, "PrintCaps");
    if (printCapsJson) {
        var printCaps = eval("(" + printCapsJson + ")");
        return printCaps;
    }
    return {};
}

/**************************************************************
*                                                             *
*              Utility functions                              *
*                                                             *
**************************************************************/

function parseConstraints(constraintsString) {
    // Sample input:
    //    psk:PageOutputColor/ns0000:JobBlackOptimization!=ns0000:ON;
    //    psk:JobInputBin=ns0000:UPPER/psk:PageMediaSize=psk:ISOA4,psk:ISOA3,psk:ISOA5;
    //    psk:JobInputBin=ns0000:UPPER/psk:PageMediaType!=ns0000:Transparency,ns0000:Envelope;
    // Corresponding output:
    //    {
    //        "psk:PageOutputColor": {
    //            notPermittedWhen: {
    //                "ns0000:JobBlackOptimization": ["ns0000:ON"]
    //            }
    //        },
    //        "psk:JobInputBin": {
    //            valueConstraints: {
    //                "ns0000:UPPER": {
    //                    permittedWhen: {
    //                        "psk:PageMediaSize": ["psk:ISOA4", "psk:ISOA3", "psk:ISOA5"]
    //                    },
    //                    notPermittedWhen: {
    //                        "psk:PageMediaType": ["ns0000:Transparency", "ns0000:Envelope"]
    //                    }
    //                }
    //            }
    //        }
    //    };
    
    var constraints = {};
    if (constraintsString) {
        var constraintPairs = constraintsString.split(";");
        for (var i = 0; i < constraintPairs.length; ++i) {
            var constraintPair = constraintPairs[i];
            var parts = constraintPair.split("/");
            if (parts.length != 2)
                continue;
            var left = parseNameValuePair(parts[0].trim());
            var right = parseNameValuePair(parts[1].trim());

            var featureConstraints = constraints[left.name];
            if (!featureConstraints) {
                featureConstraints = {};
                constraints[left.name] = featureConstraints;
            }

            var values = parseCommaSeparatedValues(right.value);
            if (right.name.substr(right.name.length - 1) == "!") {
                right.isPermitted = false;
                right.name = right.name.substr(0, right.name.length - 1);
            }
            else {
                right.isPermitted = true;
            }

            var featureOrValueConstraints = featureConstraints;
            if (left.value !== undefined) {
                if (!featureConstraints.valueConstraints)
                    featureConstraints.valueConstraints = {};
                featureOrValueConstraints = featureConstraints.valueConstraints[left.value];
                if (!featureOrValueConstraints) {
                    featureOrValueConstraints = {};
                    featureConstraints.valueConstraints[left.value] = featureOrValueConstraints;
                }
            }

            var rightValues = featureOrValueConstraints[right.isPermitted ? "permittedWhen" : "notPermittedWhen"];
            if (!rightValues) {
                rightValues = {}
                featureOrValueConstraints[right.isPermitted ? "permittedWhen" : "notPermittedWhen"] = rightValues;
            }
            rightValues[right.name] = values;
        }
    }
    return constraints;
}
function parseNameValuePair(pairString) {
    if (!pairString)
        return null;
    var parts = pairString.split("=", 2);
    var name = parts[0];
    var value = parts.length > 1 ? parts[1] : undefined;
    return { name: name, value: value };
}
function parseCommaSeparatedValues(valuesString) {
    if (!valuesString)
        return [];
    var values = valuesString.split(",");
    return values;
}

function makeFeatureValuesMap(printTicket, printCapabilities, featureNodes) {
    var values = {};
    for (var i = 0; i < featureNodes.length; ++i) {
        var featureNode = featureNodes.item(i);
        var featureName = getElementName(featureNode, PREFIX_CANONICAL);
        var pcFeature = printCapabilities && getFeature(printCapabilities, featureName, PREFIX_CANONICAL);
        if (pcFeature && pcFeature.SelectionType != PrintSchemaSelectionType_PickOne)
            continue;

        var featureValues = [];

        var optionNodes = featureNode.selectNodes(psfPrefix + ":Option");
        for (var j = 0; j < optionNodes.length; ++j) {
            var optionNode = optionNodes.item(j);
            var value = getElementName(optionNode, PREFIX_CANONICAL);
            if (value != null)
                featureValues.push(value);
        }

        var featureValue = featureValues.join(",");
        values[featureName] = featureValue;
    }
    return values;
}

function getFeature(printCapabilitiesOrPrintTicket, featureNameString, prefixType) {
    var name = ParseNameWithNs(printCapabilitiesOrPrintTicket.XmlNode, featureNameString, prefixType);
    if (!name || !name.name || !name.ns)
        return null;
    var feature = printCapabilitiesOrPrintTicket.GetFeature(name.name, name.ns);
    return feature;
}
function getFeatureNode(parentNode, name, prefixType) {
    return findElementNode(parentNode, psfPrefix + ":Feature", name, prefixType);
}
function getParameterInitNode(parentNode, name, prefixType) {
    return findElementNode(parentNode, psfPrefix + ":ParameterInit", name, prefixType);
}
function getParameterDefNode(parentNode, name, prefixType) {
    return findElementNode(parentNode, psfPrefix + ":ParameterDef", name, prefixType);
}
function findElementNode(parentNode, tag, elementName, prefixType) {
    var name = ParseNameWithNs(parentNode, elementName, prefixType);
    if (!name || !name.name || !name.ns)
        return null;
    var node = searchByAttributeName(parentNode, tag, name.ns, name.name);
    return node;
}
function getOption(feature, optionName, prefixType) {
    var name = ParseNameWithNs(feature.XmlNode, optionName, prefixType);
    if (!name || !name.name || !name.ns)
        return null;
    var option = feature.GetOption(name.name, name.ns);
    return option;
}
function getOptionNode(featureNode, optionName, prefixType) {
    var name = ParseNameWithNs(featureNode, optionName, prefixType);
    if (!name || !name.name || !name.ns)
        return null;
    var optionNode = searchByAttributeName(featureNode, psfPrefix + ":Option", name.ns, name.name);
    return optionNode;
}
function getSelectedOptionNode(featureNode) {
    var optionNode = featureNode.selectSingleNode(psfPrefix + ":Option");
    return optionNode;
}
function getSelectedOptionName(featureNode, prefixType) {
    var optionNode = featureNode.selectSingleNode(psfPrefix + ":Option");
    return optionNode ? getElementName(optionNode, prefixType) : null;
}
function getElementName(optionNode, prefixType) {
    var realName = optionNode.getAttribute("name");
    if (!realName)
        return null;
    if (prefixType == PREFIX_REAL)
        return realName;
    var name = ParseNameWithNs(optionNode, realName, PREFIX_REAL);
    if (!name || !name.name || !name.ns)
        return null;
    var optionName = NameWithNs(optionNode, name.ns, name.name, prefixType);
    return optionName;
}

function addChildElement(parentNode, tagNs, tagName, nameCanonical) {
    var document = parentNode.ownerDocument;
    var child = nameCanonical ?
        findElementNode(parentNode, NameWithNs(document, tagNs, tagName, PREFIX_CANONICAL), nameCanonical, PREFIX_CANONICAL) : null;
    if (!child) {
        child = createChildElement(parentNode, tagNs, tagName, nameCanonical);
    }
    return child;
}
function createChildElement(parentNode, tagNs, tagName, nameCanonical) {
    var document = parentNode.ownerDocument;
    var child = document.createNode(NODE_ELEMENT, NameWithNs(document, tagNs, tagName), tagNs);
    if (nameCanonical) {
        child.setAttribute("name", ToRealNameWithNs(document, nameCanonical));
    }
    parentNode.appendChild(child);
    return child;
}

function removeElement(elementNode) {
    if (elementNode.parentNode) {
        elementNode.parentNode.removeChild(elementNode);
    }
}
function removeChildElements(elementNode, name) {
    var childElements = elementNode.selectNodes(name || "*");
    for (var i = 0; i < childElements.length; ++i) {
        var child = childElements.item(i);
        elementNode.removeChild(child);
    }
}
function isElementEmpty(elementNode) {
    var childElements = elementNode.selectNodes("*");
    return childElements.length == 0;
}

function parseNameValuePairsString(nameValuePairsString)
{
    var values = {};
    if (nameValuePairsString) {
        var nameValuePairs = nameValuePairsString.split(";");
        for (var i = 0; i < nameValuePairs.length; ++i) {
            var pair = nameValuePairs[i];
            var parts = pair.split("=", 2);
            var name = parts[0];
            if (!name)
                continue;
            var valueString = parts.length > 1 ? parts[1] : null;
            var value = valueString != null ? ParseNameWithNs(null, valueString, PREFIX_CANONICAL) : null;
            values[name] = value;
        }
    }
    return values;
}
function parseNameValuePairsString2(nameValuePairsString) {
    var values = {};
    if (nameValuePairsString) {
        var nameValuePairs = nameValuePairsString.split(";");
        for (var i = 0; i < nameValuePairs.length; ++i) {
            var pair = nameValuePairs[i];
            var parts = split2(pair, "=");
            var name = parts[0].trim();
            if (!name)
                continue;
            var value = parts.length > 1 ? parts[1] : null;
            if (value) {
                value = value.replace(/%3B/g, ";");
                value = value.replace(/%25/g, "%");
            }
            values[name] = value;
        }
    }
    return values;
}
function makeNameValuePairsString(values, separator) {
    separator = separator || ";\r\n";
    var valuePairs = [];
    for (var name in values) {
        var value = values[name];
        if (value) {
            value = value.replace(/%/g, "%25");
            value = value.replace(/;/g, "%3B");
        }
        valuePairs.push(name + "=" + value);
    }
    var str = valuePairs.join(separator);
    return str;
}

function parseParameterDef(parameterDefNode) {
    var parameterDef = {
        MinValue: parseInt(GetPropertyValue(getProperty(parameterDefNode, psfNs, "MinValue"))),
        MaxValue: parseInt(GetPropertyValue(getProperty(parameterDefNode, psfNs, "MaxValue"))),
        DefaultValue: parseInt(GetPropertyValue(getProperty(parameterDefNode, psfNs, "DefaultValue")))
    };
    return parameterDef;
}

function safeGetString(propertyBag, name) {
    try {
        var str = propertyBag.GetString(name);
        return str;
    }
    catch (e) {
        return null;
    }
}

function isFeatureValueConstrained(featureName, featureValues, constraints) {
    var featureValue = featureValues[featureName];
    var featureConstraints = constraints[featureName];
    if (featureConstraints) {
        if (isNotPermitted(featureConstraints, featureValues))
            return true;
        var valueConstraints = featureConstraints.valueConstraints && featureConstraints.valueConstraints[featureValue];
        if (valueConstraints) {
            if (isNotPermitted(valueConstraints, featureValues))
                return true;
        }
    }
    return false;
}

function isNotPermitted(featureOrValueConstraints, featureValues) {
    var permittedWhen = featureOrValueConstraints.permittedWhen;
    if (permittedWhen) {
        for (var featureName2 in permittedWhen) {
            var featureValue2 = featureValues[featureName2];
            var values = permittedWhen[featureName2];
            var found = false;
            for (var i = 0; i < values.length; ++i) {
                var value = values[i];
                if (featureValue2 == value) {
                    found = true;
                    break;
                }
            }
            if (!found)
                return true;
        }
    }
    var notPermittedWhen = featureOrValueConstraints.notPermittedWhen;
    if (notPermittedWhen) {
        for (var featureName2 in notPermittedWhen) {
            var featureValue2 = featureValues[featureName2];
            var values = notPermittedWhen[featureName2];
            for (var i = 0; i < values.length; ++i) {
                var value = values[i];
                if (featureValue2 == value)
                    return true;
            }
        }
    }
    return false;
}

function getScoredPropertyFullName(featureDef, scoredPropName, optionNumber) {
    if (featureDef.pickMany)
        return featureDef.name + "/" + optionNumber + "/" + scoredPropName;
    else
        return featureDef.name + "/" + scoredPropName;
}
function getScoredPropsArrayName(featureDef, optionNumber) {
    return featureDef.name + (featureDef.pickMany ? "/" + optionNumber : "") + "/sprops";
}
function getScoredPropRefName(scoredPropFullName) {
    return scoredPropFullName + "/ref";
}

function AddFeaturesAndParameters(printCapabilitiesOrPrintTicket, printCaps, values) {
    var paramDefsMap = {};
    forEach(printCaps.paramDefs, function (paramDef) {
        paramDefsMap[paramDef.name] = paramDef;
    });

    var featureNodesMap = getAllElementsMap(printCapabilitiesOrPrintTicket.XmlNode, psfPrefix + ":Feature");
    forEach(printCaps.features, function (featureDef) {
        AddFeature(printCapabilitiesOrPrintTicket.XmlNode.documentElement, featureDef, values, featureNodesMap, paramDefsMap);
    });
    var isPrintCapabilities = printCapabilitiesOrPrintTicket.XmlNode.documentElement.baseName == "PrintCapabilities";
    var paramNodesMap = getAllElementsMap(printCapabilitiesOrPrintTicket.XmlNode,
        psfPrefix + (isPrintCapabilities ? ":ParameterDef" : ":ParameterInit"));
    forEach(printCaps.paramDefs, function (paramDef) {
        AddParameter(printCapabilitiesOrPrintTicket.XmlNode.documentElement, paramDef, values, paramNodesMap);
    });
}
function AddFeature(parentNode, featureDef, values, featureNodesMap, paramDefsMap) {
    var document = parentNode.ownerDocument;
    var isPrintCapabilities = document.documentElement.baseName == "PrintCapabilities";
    var featureNode = AddFeatureContainer(parentNode, featureDef, featureNodesMap);
    if (featureNode) {
        if (featureDef.options) {
            if (isPrintCapabilities) {
                for (var i = 0; i < featureDef.options.length; ++i) {
                    var optionDef = featureDef.options[i];
                    if (!optionDef)
                        continue;
                    AddOption(featureDef, featureNode, optionDef);
                }
            }
            else {
                for (var optionNumber = 0; ; ++optionNumber) {
                    var optionName = values[featureDef.name + (featureDef.pickMany ? "/" + optionNumber : "")]; // may be undefined in case of unnamed Option

                    // find matching option definition (all ScoredProperties with immediate Value should match)
                    // if there is no property with immediate Value defined, then first option definition is taken
                    var optionDef = findInArray(featureDef.options, function (o) {
                        if (o.name)
                            return o.name == optionName;
                        else if (o.scoredProps) {
                            var allMatch = true;
                            forEach(o.scoredProps, function (scoredPropDef) {
                                if (!scoredPropDef.paramRef) {
                                    var scoredPropFullName = getScoredPropertyFullName(featureDef, scoredPropDef.name, optionNumber);
                                    var scoredPropValue = values[scoredPropFullName];
                                    if (!scoredPropValue || scoredPropValue != scoredPropDef.value) {
                                        allMatch = false;
                                        return false; // break
                                    }
                                }
                            });
                            return allMatch;
                        }
                        return false;
                    });

                    if (!optionDef && !optionName)
                        break;

                    // HACK: support for options which are not defined in PrintCaps
                    var isUnknownOption = !optionDef;
                    if (!optionDef) {
                        optionDef = { name: optionName };

                        var scoredPropNamesString = values[getScoredPropsArrayName(featureDef, optionNumber)];
                        if (scoredPropNamesString) {
                            var scoredPropNames = scoredPropNamesString.split(",");
                            var scoredPropDefs = [];
                            forEach(scoredPropNames, function (scoredPropName) {
                                var scoredPropDef = { name: scoredPropName };
                                var scoredPropFullName = getScoredPropertyFullName(featureDef, scoredPropName, optionNumber);
                                var scoredPropRefName = values[getScoredPropRefName(scoredPropFullName)];
                                if (scoredPropRefName) {
                                    scoredPropDef.paramRef = scoredPropRefName;
                                }
                                scoredPropDefs.push(scoredPropDef);
                            });
                            optionDef.scoredProps = scoredPropDefs;
                        }
                    }

                    if (!isUnknownOption || !findElementNode(featureNode, psfPrefix + ":Option", optionName, PREFIX_CANONICAL)) {
                        if (optionNumber == 0)
                            removeChildElements(featureNode, psfPrefix + ":Option");

                        var optionNode = AddOption(featureDef, featureNode, optionDef, optionNumber, values, paramDefsMap);
                        if (!optionNode)
                            break;
                    }

                    if (!featureDef.pickMany)
                        break;
                }
            }
        }
        if (featureDef.subFeatures) {
            for (var i = 0; i < featureDef.subFeatures.length; ++i) {
                var subFeature = featureDef.subFeatures[i];
                if (!subFeature)
                    continue;
                AddFeature(featureNode, subFeature, values, featureNodesMap, paramDefsMap);
            }
        }
    }
    if (!isPrintCapabilities) {
        var selectedOptionNode = getSelectedOptionNode(featureNode);
        if (!selectedOptionNode) { // remove empty features
            removeElement(featureNode);
            featureNode = null;
        }
    }
    return featureNode;
}
function AddFeatureContainer(parentNode, featureDef, featureNodesMap) {
    var document = parentNode.ownerDocument;
    var isPrintCapabilities = document.documentElement.baseName == "PrintCapabilities";
    var name = featureDef.name;
    var featureNode = featureNodesMap && featureNodesMap[name] ||
        !featureNodesMap && getFeatureNode(parentNode, name, PREFIX_CANONICAL);
    if (!featureNode || featureNode.parentNode != parentNode) {
        featureNode = featureNode || !featureNodesMap && getFeatureNode(parentNode.ownerDocument.documentElement, name, PREFIX_CANONICAL);
        if (featureNode) {
            removeElement(featureNode);
            parentNode.appendChild(featureNode);
        }
    }
    if (!featureNode) {
        featureNode = createChildElement(parentNode, psfNs, "Feature", name);
        if (isPrintCapabilities) {
            SetProperty(featureNode, "DisplayName", pskNs, featureDef.dispName || featureDef.name + " Feature", "string", xsdNs, true);
            SetProperty(featureNode, "SelectionType", psfNs, NameWithNs(document, pskNs, featureDef.pickMany ? "PickMany" : "PickOne"), "QName", xsdNs, true);
        }
    }
    return featureNode;
}
function AddOption(featureDef, featureNode, optionDef, optionNumber, values, paramDefsMap) {
    var name = optionDef.name;
    var document = featureNode.ownerDocument;
    var isPrintCapabilities = document.documentElement.baseName == "PrintCapabilities";
    var optionNode = addChildElement(featureNode, psfNs, "Option", name);
    if (isPrintCapabilities) {
        if (optionDef.dispName) {
            SetProperty(optionNode, "DisplayName", pskNs, optionDef.dispName || optionDef.name + " Option", "string", xsdNs, true);
        }
    }
    if (optionDef.scoredProps) {
        forEach(optionDef.scoredProps, function (scoredPropDef) {
            if (!isPrintCapabilities && !scoredPropDef.type && paramDefsMap) {
                // get type from referenced parameter definition
                if (scoredPropDef.paramRef) {
                    var paramDef = paramDefsMap[scoredPropDef.paramRef];
                    if (paramDef) {
                        var typeProp = findInArray(paramDef.props, function (prop) {
                            return prop.name == "psf:DataType";
                        });
                        if (typeProp) {
                            scoredPropDef.type = typeProp.value;
                        }
                    }
                }
            }
            AddScoredProperty(featureDef, optionNode, optionNumber, scoredPropDef, values);
        });
    }
    if (!name && isElementEmpty(optionNode)) {
        removeElement(optionNode);
        optionNode = null;
    }
    return optionNode;
}
function AddScoredProperty(featureDef, optionNode, optionNumber, scoredPropDef, values) {
    var document = optionNode.ownerDocument;
    var isPrintCapabilities = document.documentElement.baseName == "PrintCapabilities";

    var propertyNode = addChildElement(optionNode, psfNs, "ScoredProperty", scoredPropDef.name);

    if (isPrintCapabilities) {
        var paramRefName = scoredPropDef.paramRef;
        if (paramRefName) {
            removeChildElements(propertyNode);
            addChildElement(propertyNode, psfNs, "ParameterRef", paramRefName);
        }
        else if (scoredPropDef.value) {
            AddValue(propertyNode, scoredPropDef.value, scoredPropDef.type);
        }
    }
    else {
        if (scoredPropDef.value) { // ScoredProperty value is fixed for current option
            AddValue(propertyNode, scoredPropDef.value, scoredPropDef.type);
        }
        else if (scoredPropDef.paramRef) { // ScoredProperty value should be specified via parameter (only if not PickMany feature)
            var scoredPropFullName = getScoredPropertyFullName(featureDef, scoredPropDef.name, optionNumber);
            var value = values[scoredPropFullName]; // FUI can generate ScoredProperty with Value even if it is with ParameterRef in PrintCapabilities.
            if (featureDef.pickMany && value) { // add as immediate Value only if feature is PickMany and ScoredProperty was generated with immediate Value by FUI
                AddValue(propertyNode, value, scoredPropDef.type);
            }
            else if (values[scoredPropDef.paramRef] || value) { // otherwise, if there is a value for this ScoredProperty (either ParameterRef or immediate), add as ParameterRef
                // add as ParameterRef even if it was generated as Value by FUI to fix some defects (for example with Booklet feature)
                removeChildElements(propertyNode);
                addChildElement(propertyNode, psfNs, "ParameterRef", scoredPropDef.paramRef);

                if (value) {
                    values[scoredPropDef.paramRef] = value; // HACK: assuming that ParameterInit's will be added later
                }
            }
        }
    }

    //var paramRefName = scoredPropDef.paramRef;
    //if (paramRefName) {
    //    if (isPrintCapabilities || values[paramRefName]) {
    //        removeChildElements(propertyNode);
    //        addChildElement(propertyNode, psfNs, "ParameterRef", paramRefName);
    //    }
    //}
    //else if (scoredPropDef.value) {
    //    var value = isPrintCapabilities ? scoredPropDef.value : values[scoredPropDef.name];
    //    if (value) {
    //        AddValue(propertyNode, value, scoredPropDef.type);
    //    }
    //}

    if (isElementEmpty(propertyNode)) {
        removeElement(propertyNode);
        propertyNode = null;
    }

    return propertyNode;
}
function AddParameter(parentNode, paramDef, values, paramNodesMap) {
    var document = parentNode.ownerDocument;
    var isPrintCapabilities = document.documentElement.baseName == "PrintCapabilities";
    var parameterNode = paramNodesMap && paramNodesMap[paramDef.name];

    if (isPrintCapabilities) {
        if (paramDef.props && paramDef.props.length > 0) {
            if (!parameterNode)
                parameterNode = createChildElement(parentNode, psfNs, "ParameterDef", paramDef.name);
            forEach(paramDef.props, function (propDef) {
                AddProperty(parameterNode, propDef.name, propDef.value, propDef.type);
            });
        }
    }
    else {
        var value = values[paramDef.name];
        if (value) {
            if (!parameterNode)
                parameterNode = createChildElement(parentNode, psfNs, "ParameterInit", paramDef.name);
            var typeProp = findInArray(paramDef.props, function (p) {
                return p.name == "psf:DataType";
            });
            AddValue(parameterNode, value, typeProp && typeProp.value);
        }
    }

    return parameterNode;
}
function AddProperty(parentNode, name, value, type) {
    var document = parentNode.ownerDocument;
    var isPrintCapabilities = document.documentElement.baseName == "PrintCapabilities";

    var propertyNode = addChildElement(parentNode, psfNs, "Property", name);
    var valueNode = AddValue(propertyNode, value, type);
    return propertyNode;
}
function AddValue(parentNode, value, type) {
    var document = parentNode.ownerDocument;
    removeChildElements(parentNode);
    var valueNode = addChildElement(parentNode, psfNs, "Value");
    if (type)
        SetAttributeWithNs(valueNode, xsiNs, "type", ToRealNameWithNs(document, type || "xsd:string"));
    valueNode.text = value;
    return valueNode;
}

function SetProperty(featureNode, name, nameNs, value, type, typeNs, keepExisting) {
    var document = featureNode.ownerDocument;

    var propertyNode = searchByAttributeName(featureNode, psfPrefix + ":Property", nameNs, name);
    if (propertyNode && keepExisting)
        return;

    if (!propertyNode) {
        propertyNode = document.createNode(NODE_ELEMENT, NameWithNs(document, psfNs, "Property"), psfNs);
        propertyNode.setAttribute("name", NameWithNs(document, nameNs, name));
        featureNode.appendChild(propertyNode);
    }

    var valueNode = propertyNode.selectSingleNode(psfPrefix + ":Value");
    if (!valueNode) {
        valueNode = document.createNode(NODE_ELEMENT, NameWithNs(document, psfNs, "Value"), psfNs);
        propertyNode.appendChild(valueNode);
    }

    SetAttributeWithNs(valueNode, xsiNs, "type", NameWithNs(document, typeNs, type));
    valueNode.text = value;
}

function SetAttributeWithNs(element, attributeNs, attributeName, value) {
    var document = element.ownerDocument;

    var attributeNode = document.createNode(NODE_ATTRIBUTE, NameWithNs(document, xsiNs, "type"), xsiNs);
    attributeNode.value = value;
    element.setAttributeNode(attributeNode);
}

function getAllElementsMap(parentNode, tagName) {
    var documentRoot = parentNode.ownerDocument ? parentNode.ownerDocument.documentElement : parentNode.documentElement;
    var map = {};
    var nodes = parentNode.selectNodes("//" + tagName);
    for (var i = 0; i < nodes.length; ++i) {
        var node = nodes.item(i);
        var name = getElementName(node, PREFIX_CANONICAL);
        var prevNode = map[name];
        if (prevNode) { // duplicate feature detected
            var isNodeNested = node.parentNode != documentRoot;
            var isPrevNodeNested = prevNode.parentNode != documentRoot;
            if (!isNodeNested && isPrevNodeNested) // prefer nested feature
                node = prevNode;
        }
        map[name] = node;
    }
    return map;
}

function ToRealNameWithNs(node, nameWithNs) {
    var parsedName = ParseNameWithNs(null, nameWithNs, PREFIX_CANONICAL);
    var realNameWithNs = NameWithNs(node, parsedName.ns, parsedName.name, PREFIX_REAL);
    return realNameWithNs;
}
function NameWithNs(node, ns, name, prefixType) {
    var prefix = prefixType != PREFIX_CANONICAL ? getPrefixForNamespace(node, ns) :
        prefixes[ns];
    if (!prefix)
        return null;
    return prefix + ":" + name;
}
function ParseNameWithNs(node, nameWithNs, prefixType) {
    var parts = nameWithNs.split(':', 2);
    var prefix = parts.length > 1 ? parts[0] : null;
    var localName = parts.length > 1 ? parts[1] : parts[0];
    var ns = prefix == null ? null :
        prefixType == PREFIX_REAL ? getNamespaceForPrefix(node, prefix) :
        namespaces[prefix];
    return { ns: ns, name: localName };
}

function GetDecodedDevmode(printTicket) {
    if (!printTicket)
        return null;

    var devmodePropertyInit = getParameterInit(printTicket.XmlNode, saNs, "PageDevmodeSnapshot");

    if (devmodePropertyInit) {
        var value = GetPropertyValue(devmodePropertyInit);

        var decodedValue = base64_decode(value);
        return decodedValue;
    }

    return null;
}

function GetPropertyValue(propertyNode) {
    var valueNode = getPropertyFirstValueNode(propertyNode);
    if (valueNode) {
        var child = valueNode.firstChild;
        if (child) {
            return child.nodeValue;
        }
    }
    return null;
}

function setPropertyValue(propertyNode, value) {
    /// <summary>
    ///     Set the value contained in the 'Value' node under a 'Property'
    ///     or a 'ScoredProperty' node in the print ticket/print capabilities document.
    /// </summary>
    /// <param name="propertyNode" type="IXMLDOMNode">
    ///     The 'Property'/'ScoredProperty' node.
    /// </param>
    /// <param name="value" type="variant">
    ///     The value to be stored under the 'Value' node.
    /// </param>
    /// <returns type="IXMLDOMNode" mayBeNull="true" locid="R:propertyValue">
    ///     First child 'Property' node if found, Null otherwise.
    /// </returns>
    var valueNode = getPropertyFirstValueNode(propertyNode);
    if (valueNode) {
        var child = valueNode.firstChild;
        if (child) {
            child.nodeValue = value;
            return child;
        }
    }
    return null;
}

function setSubPropertyValue(parentProperty, keywordNamespace, subPropertyName, value) {
    /// <summary>
    ///     Set the value contained in an inner Property node's 'Value' node (i.e. 'Value' node in a Property node
    ///     contained inside another Property node).
    /// </summary>
    /// <param name="parentProperty" type="IXMLDOMNode">
    ///     The parent property node.
    /// </param>
    /// <param name="keywordNamespace" type="String">
    ///     The namespace in which the property name is defined.
    /// </param>
    /// <param name="subPropertyName" type="String">
    ///     The name of the sub-property node.
    /// </param>
    /// <param name="value" type="variant">
    ///     The value to be set in the sub-property node's 'Value' node.
    /// </param>
    /// <returns type="IXMLDOMNode" mayBeNull="true">
    ///     Refer setPropertyValue.
    /// </returns>
    if (!parentProperty ||
        !keywordNamespace ||
        !subPropertyName) {
            return null;
        }
    var subPropertyNode = getProperty(
                            parentProperty,
                            keywordNamespace,
                            subPropertyName);
    return setPropertyValue(
            subPropertyNode,
            value);
}

function getScoredProperty(node, keywordNamespace, scoredPropertyName) {
    /// <summary>
    ///     Retrieve a 'ScoredProperty' element in a print ticket/print capabilities document.
    /// </summary>
    /// <param name="node" type="IXMLDOMNode">
    ///     The scope of the search i.e. the parent node.
    /// </param>
    /// <param name="keywordNamespace" type="String">
    ///     The namespace in which the element's 'name' attribute is defined.
    /// </param>
    /// <param name="scoredPropertyName" type="String">
    ///     The ScoredProperty's 'name' attribute (without the namespace prefix).
    /// </param>
    /// <returns type="IXMLDOMNode" mayBeNull="true">
    ///     The node on success, 'null' on failure.
    /// </returns>

    // Note: It is possible to hard-code the 'psfPrefix' variable in the tag name since the
    // SelectionNamespace property has been set against 'psfPrefix'
    // in validatePrintTicket/completePrintCapabilities.
    return searchByAttributeName(
                node,
                psfPrefix + ":ScoredProperty",
                keywordNamespace,
                scoredPropertyName);
}

function getProperty(node, keywordNamespace, propertyName) {
    /// <summary>
    ///     Retrieve a 'Property' element in a print ticket/print capabilities document.
    /// </summary>
    /// <param name="node" type="IXMLDOMNode">
    ///     The scope of the search i.e. the parent node.
    /// </param>
    /// <param name="keywordNamespace" type="String">
    ///     The namespace in which the element's 'name' attribute is defined.
    /// </param>
    /// <param name="propertyName" type="String">
    ///     The Property's 'name' attribute (without the namespace prefix).
    /// </param>
    /// <returns type="IXMLDOMNode" mayBeNull="true">
    ///     The node on success, 'null' on failure.
    /// </returns>
    return searchByAttributeName(
            node,
            psfPrefix + ":Property",
            keywordNamespace,
            propertyName);
}

function getParameterInit(node, keywordNamespace, propertyName) {
    /// <summary>
    ///     Retrieve a 'ParameterInit' element in a print ticket/print capabilities document.
    /// </summary>
    /// <param name="node" type="IXMLDOMNode">
    ///     The scope of the search i.e. the parent node.
    /// </param>
    /// <param name="keywordNamespace" type="String">
    ///     The namespace in which the element's 'name' attribute is defined.
    /// </param>
    /// <param name="propertyName" type="String">
    ///     The Property's 'name' attribute (without the namespace prefix).
    /// </param>
    /// <returns type="IXMLDOMNode" mayBeNull="true">
    ///     The node on success, 'null' on failure.
    /// </returns>
    return searchByAttributeName(
            node,
            psfPrefix + ":ParameterInit",
            keywordNamespace,
            propertyName);
}

function getParameterDef(node, keywordNamespace, propertyName) {
    /// <summary>
    ///     Retrieve a 'ParameterDef' element in a print ticket/print capabilities document.
    /// </summary>
    /// <param name="node" type="IXMLDOMNode">
    ///     The scope of the search i.e. the parent node.
    /// </param>
    /// <param name="keywordNamespace" type="String">
    ///     The namespace in which the element's 'name' attribute is defined.
    /// </param>
    /// <param name="propertyName" type="String">
    ///     The Property's 'name' attribute (without the namespace prefix).
    /// </param>
    /// <returns type="IXMLDOMNode" mayBeNull="true">
    ///     The node on success, 'null' on failure.
    /// </returns>
    return searchByAttributeName(
            node,
            psfPrefix + ":ParameterDef",
            keywordNamespace,
            propertyName);
}

function setSelectedOptionName(printSchemaFeature, optionNs, optionName) {
    /// <summary>
    ///      Set the 'name' attribute of a Feature's selected option
    ///      Note: This function should be invoked with Feature type that is retrieved
    ///            via either PrintCapabilties->GetFeature() or PrintTicket->GetFeature().
    ///
    ///      Caution: Setting only the 'name' attribute can result in an invalid option element.
    ///            Some options require their entire subtree to be updated.
    /// </summary>
    /// <param name="printSchemaFeature" type="IPrintSchemaFeature">
    ///     Feature variable.
    /// </param>
    /// <param name="optionNs" type="String">
    ///     The namespace for the optionName parameter.
    /// </param>
    /// <param name="optionName" type="String">
    ///     The name (without prefix) to set as the 'name' attribute.
    /// </param>
    if (!printSchemaFeature ||
        !printSchemaFeature.SelectedOption ||
        !printSchemaFeature.SelectedOption.XmlNode) {
            return;
        }
    printSchemaFeature.SelectedOption.XmlNode.setAttribute(
        "name",
        NameWithNs(printSchemaFeature.SelectedOption.XmlNode.ownerDocument, optionNs, optionName));
}


/**************************************************************
*                                                             *
*              Functions used by utility functions            *
*                                                             *
**************************************************************/

function getPropertyFirstValueNode(propertyNode) {
    /// <summary>
    ///     Retrieve the first 'value' node found under a 'Property' or 'ScoredProperty' node.
    /// </summary>
    /// <param name="propertyNode" type="IXMLDOMNode">
    ///     The 'Property'/'ScoredProperty' node.
    /// </param>
    /// <returns type="IXMLDOMNode" mayBeNull="true">
    ///     The 'Value' node on success, 'null' on failure.
    /// </returns>
    if (!propertyNode) {
        return null;
    }

    var nodeName = propertyNode.nodeName;
    if ((nodeName.indexOf(":Property") < 0) &&
        (nodeName.indexOf(":ScoredProperty") < 0) &&
        (nodeName.indexOf(":ParameterInit") < 0)) {
            return null;
        }

    var valueNode = propertyNode.selectSingleNode(psfPrefix + ":Value");
    return valueNode;
}

function searchByAttributeName(node, tagName, keywordNamespace, nameAttribute) {
    /// <summary>
    ///      Search for a node that with a specific tag name and containing a
    ///      specific 'name' attribute
    ///      e.g. &lt;Bar name=\"ns:Foo\"&gt; is a valid result for the following search:
    ///           Retrieve elements with tagName='Bar' whose nameAttribute='Foo' in
    ///           the namespace corresponding to prefix 'ns'.
    /// </summary>
    /// <param name="node" type="IXMLDOMNode">
    ///     Scope of the search i.e. the parent node.
    /// </param>
    /// <param name="tagName" type="String">
    ///     Restrict the searches to elements with this tag name.
    /// </param>
    /// <param name="keywordNamespace" type="String">
    ///     The namespace in which the element's name is defined.
    /// </param>
    /// <param name="nameAttribute" type="String">
    ///     The 'name' attribute to search for.
    /// </param>
    /// <returns type="IXMLDOMNode" mayBeNull="true">
    ///     IXMLDOMNode on success, 'null' on failure.
    /// </returns>
    if (!node ||
        !tagName ||
        !keywordNamespace ||
        !nameAttribute) {
            return null;
        }

    // For more information on this XPath query, visit:
    // http://blogs.msdn.com/b/benkuhn/archive/2006/05/04/printticket-names-and-xpath.aspx
    var xPathQuery = "descendant::"
                    + tagName
                    + "[substring-after(@name,':')='"
                    + nameAttribute
                    + "']"
                    + "[name(namespace::*[.='"
                     + keywordNamespace
                     + "'])=substring-before(@name,':')]"
                     ;

    return node.selectSingleNode(xPathQuery);
}

function setSelectionNamespace(xmlNode, prefix, namespace) {
    /// <summary>
    ///     This function sets the 'SelectionNamespaces' property on the XML Node.
    ///     For more details: http://msdn.microsoft.com/en-us/library/ms756048(VS.85).aspx
    /// </summary>
    /// <param name="xmlNode" type="IXMLDOMNode">
    ///     The node on which the property is set.
    /// </param>
    /// <param name="prefix" type="String">
    ///     The prefix to be associated with the namespace.
    /// </param>
    /// <param name="namespace" type="String">
    ///     The namespace to be added to SelectionNamespaces.
    /// </param>
    xmlNode.setProperty(
        "SelectionNamespaces",
        "xmlns:"
            + prefix
            + "='"
            + namespace
            + "'"
        );
}

function getPrefixForNamespace(node, namespace) {
    /// <summary>
    ///     This function returns the prefix for a given namespace.
    ///     Example: In 'psf:printTicket', 'psf' is the prefix for the namespace.
    ///     xmlns:psf="http://schemas.microsoft.com/windows/2003/08/printing/printschemaframework"
    /// </summary>
    /// <param name="node" type="IXMLDOMNode">
    ///     A node in the XML document.
    /// </param>
    /// <param name="namespace" type="String">
    ///     The namespace for which prefix is returned.
    /// </param>
    /// <returns type="String">
    ///     Returns the namespace corresponding to the prefix.
    /// </returns>

    if (!node) {
        return null;
    }

    // navigate to the root element of the document.
    var rootNode = node.documentElement;

    // Query to retrieve the list of attribute nodes for the current node
    // that matches the namespace in the 'namespace' variable.
    var xPathQuery = "namespace::node()[.='"
                + namespace
                + "']";
    var namespaceNode = rootNode.selectSingleNode(xPathQuery);
    if (!namespaceNode)
        return null;
    var prefix = namespaceNode.baseName;

    return prefix;
}

function getNamespaceForPrefix(node, prefix) {
    /// <summary>
    ///     This function returns the full namespace for a given namespace prefix.
    ///     Example: In 'psf:printTicket', 'psf' is the prefix for the namespace.
    ///     xmlns:psf="http://schemas.microsoft.com/windows/2003/08/printing/printschemaframework"
    /// </summary>
    /// <param name="node" type="IXMLDOMNode">
    ///     A node in the XML document.
    /// </param>
    /// <param name="prefix" type="String">
    ///     The namespace prefix for which full namespace is returned.
    /// </param>
    /// <returns type="String">
    ///     Returns the prefix corresponding to the namespace.
    /// </returns>

    if (!node) {
        return null;
    }

    // navigate to the root element of the document.
    var rootNode = node.ownerDocument ? node.ownerDocument.documentElement : node.documentElement;

    // Query to retrieve the list of attribute nodes for the current node
    // that matches the namespace in the 'namespace' variable.
    var xPathQuery = "namespace::node()[name(.)='"
                + prefix
                + "']";
    var namespaceNode = rootNode.selectSingleNode(xPathQuery);
    if (!namespaceNode)
        return null;
    var namespace = namespaceNode.value;

    return namespace;
}

function base64_decode(data) {
    // Decodes string using MIME base64 algorithm
    // version: 1109.2015
    // discuss at: http://phpjs.org/functions/base64_decode
    var b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
    var o1, o2, o3, h1, h2, h3, h4, bits, i = 0,
        ac = 0,
        dec = "",
        tmp_arr = [];

    if (!data) {
        return data;
    }

    data += '';

    do { // unpack four hexets into three octets using index points in b64
        h1 = b64.indexOf(data.charAt(i++));
        h2 = b64.indexOf(data.charAt(i++));
        h3 = b64.indexOf(data.charAt(i++));
        h4 = b64.indexOf(data.charAt(i++));

        bits = h1 << 18 | h2 << 12 | h3 << 6 | h4;

        o1 = bits >> 16 & 0xff;
        o2 = bits >> 8 & 0xff;
        o3 = bits & 0xff;

        if (!isPrintable(o1))
            o1 = '.'.charCodeAt(0);
        if (!isPrintable(o2))
            o2 = '.'.charCodeAt(0);
        if (!isPrintable(o3))
            o3 = '.'.charCodeAt(0);

        if (h3 == 64) {
            tmp_arr[ac++] = String.fromCharCode(o1);
        } else if (h4 == 64) {
            tmp_arr[ac++] = String.fromCharCode(o1, o2);
        } else {
            tmp_arr[ac++] = String.fromCharCode(o1, o2, o3);
        }
    } while (i < data.length);

    dec = tmp_arr.join('');
    //dec = this.utf8_decode(dec);

    return dec;
}

function isPrintable(c) {
    return 0x09 <= c && c <= 0x0d || c >= 0x20;
}

function findInArray(array, predicate) {
    for (var i = 0; i < array.length; ++i) {
        var item = array[i];
        if (!item)
            continue;
        if (predicate(item))
            return item;
    }
}
function forEach(array, action) {
    for (var i = 0; i < array.length; ++i) {
        var item = array[i];
        if (!item)
            continue;
        var result = action(item);
        if (result === false)
            break;
    }
}
function split2(str, separator) {
    var index = str.indexOf(separator);
    if (index >= 0) {
        return [str.substr(0, index), str.substr(index + 1)];
    }
    else {
        return [str];
    }
}
