"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.UcumXmlDocument = void 0;

/**
 * This class handles opening, reading and parsing the XML file of ucum
 * definitions (prefixes, base units, and unit atoms).
 *
 * @author Lee Mericle
 *
 */
var Prefix = require("./prefix.js").Prefix;

var PrefixTables = require("./prefixTables.js").PrefixTables;

var Unit = require("./unit.js").Unit;

var UnitString = require("./unitString.js").UnitString;

var UnitTables = require('./unitTables.js').UnitTables;

var packArray = require('./jsonArrayPack.js').packArray;

var jsonfile = require('jsonfile');

var xmldoc = require('xmldoc');

var fs = require('fs');

var essenceFile_ = '../data/ucum-essence.xml';
/**
 * The full xml document
 * @type XmlDocument
 */

var xmlInput_ = null;

class UcumXmlDocument {
  /**
   * Constructor.  This reads the XML document (essenceFile_) into the
   * xmldoc object, which is an object used by the xmldoc class available
   * from GitHub - https://github.com/nfarina/xmldoc.  The object provides
   * methods to read the file and access its contents.
   *
   */
  constructor() {
    // read the XML file and create an xmlDocument object from it.
    let data = fs.readFileSync(essenceFile_);
    xmlInput_ = new xmldoc.XmlDocument(data); // Array of unit codes in the ucum-essence.xml file that indicate a
    // mole based unit.  The moleExp_ attribute for these units needs to be
    // set, but there doesn't seem to be an algorithmic way to find these.
    // Creation of unit objects after this file is processed will pick up
    // the moleExp_ value from the base mole unit, but the ones defined in
    // this file will not necessarily do that.

    this.moleCodes_ = ['mol', 'eq', 'osm', 'kat', 'U']; // Make this a singleton.  See UnitTables constructor for details.

    let holdThis = UcumXmlDocument.prototype;

    exports.UcumXmlDocument = UcumXmlDocument = function () {
      throw new Error('UcumXmlDocument is a Singleton.  ' + 'Use UcumXmlDocument.getInstance() instead.');
    };

    if (exports) exports.UcumXmlDocument = UcumXmlDocument;
    UcumXmlDocument.prototype = holdThis;
    let self = this;

    UcumXmlDocument.getInstance = function () {
      return self;
    };
  }
  /**
   * This method controls parsing of the XML into objects used by this
   * program.  It uses separate methods to parse the prefixes, the
   * base units, and the units.
   *
   * @returns nothing
   */


  parseXml() {
    this.parsePrefixes(xmlInput_.childrenNamed("prefix"));
    this.parseBaseUnits(xmlInput_.childrenNamed("base-unit"));
    this.parseUnitStrings(xmlInput_.childrenNamed("unit")); // Create or replace the json file of the prefix and unit definitions

    this.writeJsonFile();
    this.writeVersionText();
  }
  /**
   * Creates prefix objects from the xml prefix nodes passed in and gets
   * them added to the prefix tables.
   *
   * @params prefixes the array of prefix nodes from the xml file, in the
   *  order in which the nodes are defined in that file.
   *
   * @returns nothing
   */


  parsePrefixes(prefixes) {
    let plen = prefixes.length;

    for (let p = 0; p < plen; p++) {
      let curPfx = prefixes[p];
      let attrs = {};
      attrs["code_"] = curPfx.attr.Code;
      attrs["ciCode_"] = curPfx.attr.CODE;
      attrs["name_"] = curPfx.childNamed('name').val;
      attrs["printSymbol_"] = curPfx.childNamed('printSymbol').val; // Set the prefix value.  If there is a <sup> element in the
      // value node, then this is a base 10 based prefix (10 to the x power).
      // Set the value to 10 taken to the indicated power.
      // Otherwise this is not 10 based and the value contains the
      // actual value for the prefix.

      let pValNode = curPfx.childNamed('value');
      attrs["value_"] = null;
      attrs["exp_"] = pValNode.childNamed('sup');

      if (attrs["exp_"] != null) {
        attrs["exp_"] = attrs["exp_"].val;
        attrs["value_"] = Math.pow(10, attrs["exp_"]);
      } else {
        attrs["value_"] = pValNode.val;
        attrs["exp_"] = null;
      } // Make sure the prefix has not already been created.  If it hasn't,
      // create the prefix object and then add it to the prefix tables.


      let ptab = PrefixTables.getInstance();

      if (ptab.isDefined(attrs["code_"])) {
        throw new Error('Prefix constructor called for prefix already ' + `defined; code = ${attrs["code_"]}`);
      } else {
        let newPref = new Prefix(attrs);
        ptab.add(newPref);
      }
    }
  } // end parsePrefixes

  /**
   * Creates base unit objects from the xml nodes passed in and adds
   * them to the unit tables.
   *
   * @params unitNodes the array of base unit nodes from the xml file, in the
   *  order in which the nodes are defined in that file.  (Order is important
   *  for all units).
   *
   * @returns nothing
   */


  parseBaseUnits(unitNodes) {
    let blen = unitNodes.length;
    let utab = UnitTables.getInstance();

    for (let b = 0; b < blen; b++) {
      let curBUnit = unitNodes[b];
      let attrs = {};
      attrs['isBase_'] = true;
      attrs['name_'] = curBUnit.childNamed('name').val;
      attrs['csCode_'] = curBUnit.attr.Code;
      attrs['ciCode_'] = curBUnit.attr.CODE;
      attrs['property_'] = curBUnit.childNamed('property').val;
      attrs['variable_'] = curBUnit.attr.dim;
      attrs['printSymbol_'] = curBUnit.childNamed('printSymbol').val;
      attrs['dim_'] = b;
      attrs['source_'] = 'UCUM';
      let newUnit = new Unit(attrs);
      utab.addUnit(newUnit);
    }
  } // end parseBaseUnits

  /**
   * Creates non-base unit objects from the xml nodes passed in and adds
   * them to the unit tables.
   *
   * @params unitStrings the array of non-base unit nodes from the xml file, in the
   *  order in which the nodes are defined in that file.  (Order is important
   *  for all units).
   *
   * @returns nothing
   */


  parseUnitStrings(unitStrings) {
    let utab = UnitTables.getInstance();
    let uStrParser = UnitString.getInstance();
    let stopNow = false;
    let alen = unitStrings.length;

    for (let a = 0; a < alen && !stopNow; a++) {
      let haveUnit = true;
      let curUA = unitStrings[a];
      let attrs = {};
      attrs['isBase_'] = false;
      attrs['source_'] = 'UCUM';
      attrs['name_'] = curUA.childNamed('name').val;
      attrs['csCode_'] = curUA.attr.Code;
      if (curUA.attr.CODE) attrs['ciCode_'] = curUA.attr.CODE;else attrs['ciCode_'] = curUA.attr.Code.toUpperCase();
      attrs['property_'] = curUA.childNamed('property').val;

      if (curUA.childNamed('printSymbol')) {
        let sym = curUA.childNamed('printSymbol');
        let symVal = sym.val;
        symVal = symVal.replace(/\n/g, "");
        symVal = symVal.trim();
        let symI = sym.childNamed('i');
        if (symI) //symVal = '<i>' + symI.val + '</>';
          symVal = symI.toString({
            compressed: true
          });
        let sub = sym.childNamed('sub');
        let sup = sym.childNamed('sup');
        if (sub) symVal += sub.toString({
          compressed: true
        });
        if (sup) symVal += sup.toString({
          compressed: true
        });
        attrs['printSymbol_'] = symVal;
      }

      if (curUA.attr.isMetric === "yes") attrs['isMetric_'] = true;else attrs['isMetric_'] = false;
      if (curUA.attr.isArbitrary) attrs['isArbitrary_'] = true;else attrs['isArbitrary_'] = false;

      if (curUA.attr.class) {
        attrs['class_'] = curUA.attr.class;
      }

      let valNode = curUA.childNamed('value');
      if (this.moleCodes_.indexOf(curUA.attr.Code) !== -1) attrs['moleExp_'] = 1;else attrs['moleExp_'] = 0; // Process special units

      if (curUA.attr.isSpecial) {
        attrs['isSpecial_'] = curUA.attr.isSpecial === "yes";
        let funcNode = valNode.childNamed('function');
        attrs['cnv_'] = funcNode.attr.name;
        attrs['csUnitString_'] = funcNode.attr.Unit;

        if (attrs['csUnitString_'] === '1') {
          attrs['baseFactor_'] = 1;
        } else if (attrs['csCode_'] === '[pH]') {
          attrs['baseFactor_'] = parseFloat(funcNode.attr.value);
        } else {
          let slashPos = attrs['csUnitString_'].indexOf('/');
          let ar = []; // unit string = K/9 or K/4 or m2/s4/Hz

          if (slashPos >= 0) {
            ar = attrs['csUnitString_'].split('/');
          } // unit string = K/9 or K/4


          if (slashPos >= 0 && ar.length === 2) {
            attrs['csUnitString_'] = ar[0];
            attrs['baseFactor_'] = parseFloat(funcNode.attr.value / ar[1]);
          } // unit string = 10*-5.Pa
          else if (attrs['csCode_'] === 'B[SPL]') {
              attrs['baseFactor_'] = Math.pow(10, -5) * 2;
              attrs['csUnitString_'] = "Pa";
            } // unit string = m1/s4/Hz, K, deg, V, mV, uV, nV, W, kW
            else {
                attrs['baseFactor_'] = parseFloat(funcNode.attr.value);
              }
        } // end if the unit string is not 1

      } // end if the unit is special
      else {
          // what I'm calling the unit string is the string that defines the
          // unit based on other units, e.g., rad2 (radian squared) to define
          // a steradian unit.  It's not necessarily a proper base unit, although
          // it ultimately builds on base units.
          attrs['csUnitString_'] = valNode.attr.Unit;
          attrs['ciUnitString_'] = valNode.attr.UNIT; // what I'm calling the factor here (string and number versions)
          // is the magnitude used in conjunction with the unit string to define
          // the new unit, e.g., 3 for a yard that is based in the definition
          // of feet.

          attrs['baseFactorStr_'] = valNode.attr.value;
          if (attrs['csCode_'] === '[pi]') attrs['baseFactor_'] = parseFloat(attrs['baseFactorStr_']);else if (valNode.childNamed('sup')) {
            attrs['baseFactor_'] = parseFloat(valNode.attr.value);
          } else {
            attrs['baseFactor_'] = parseFloat(valNode.val);
          }
        } // end if this is not a special unit
      // Arbitrary units are defined in the UCUM spec as "not of any
      // specific dimension and are not commensurable with any other
      // unit" (3.2.24).  All arbitrary units in the units definition
      // XML file currently have a unit string of 1 and a base factor of 1
      // except the "international unit" with a code of [IU].  Its
      // unit string is the "international unit" with a code of [iU],
      // which is also an arbitrary unit - with a unit string of 1.
      // So I am assuming [IU] is just another code for the same unit.


      if (attrs['isArbitrary_'] === true) {
        attrs['magnitude_'] = 1;
        attrs['dim_'] = null;
      } // units with class = "dimless" don't have dimension arrays.
      // They're things like the number pi or the number 10 or percent.
      // Haven't figured out how to handle them yet.
      else if (attrs['class_'] === 'dimless' || attrs['csCode_'] === 'mol') {
          attrs['dim_'] = null; // figure the magnitude based on the unit string
          // if it's 1, the magnitude is the value specified for
          // the base factor, e.g., 3.141592653589793238462 ... for pi

          if (attrs['csUnitString_'] === '1') {
            attrs['magnitude_'] = attrs['baseFactor_'];
          } // else if the unit string starts with 10*, the magnitude is
          // 10 to the power specified following 10* e.g., unit = 10*-2
          // for the "%" unit.  Except for the mole, which is that
          // multiplied by the base factor, which in this case (only,
          // I think) is not 1.
          else if (attrs['csUnitString_'].substr(0, 3) == "10*") {
              let exp = parseInt(attrs['csUnitString_'].substr(3));
              attrs['magnitude_'] = Math.pow(10, exp);

              if (attrs['baseFactor_'] !== 1) {
                attrs['magnitude_'] *= attrs['baseFactor_'];
              }
            } // else I don't know what it is.
            else {
                attrs['defError_'] = true;
                console.log('unexpected dimless unit definition, unit code ' + 'is ' + attrs['csCode_']);
              }
        } // end if this is a unit with class = dimless
        // Handle carat of gold alloys - which doesn't get a dimension
        //
        else if (attrs['csCode_'] === "[car_Au]") {
            attrs['magnitude_'] = 1 / 24;
            attrs['dim_'] = null;
          } else {
            // Make sure there's a unit string to base the new unit on.  There
            // should be, but I'm just checking here to make sure.  And omit
            // ones with a unit string of 1.  That won't do us any good.
            if (attrs['csUnitString_'] && attrs['csUnitString_'] !== '1' && attrs['csUnitString_'] !== 1) {
              haveUnit = false; // Handle some special cases
              // 1. the Oersted unit, whose string is /[pi].A/m and whose
              //    value is 250.  Set the baseFactor to 250/[pi] and
              //    the unit string to A/m

              if (attrs['csCode_'] === 'Oe') {
                attrs['baseFactor_'] = 250 / Math.PI;
                attrs['csUnitString_'] = "A/m";
              } // 2.  Strings that start with '/'.  Set the function to
              //     the inverse function and trim the '/' off the front
              //     of the string.
              else if (attrs['csUnitString_'][0] === '/') {
                  attrs['cnv_'] = 'inv';
                  attrs['csUnitString_'] = attrs['csUnitString_'].substr(1);
                } // 3.  the Svedberg unit, whose string is 10*-13.s.  Set the
                //     base factor to 10*-13 and the unit string to s.
                else if (attrs['csCode_'] === '[S]') {
                    attrs['baseFactor_'] = Math.pow(10, -13);
                    attrs['csUnitString_'] = 's';
                  } // 4.  permeability of vaccuum - code [mu_0], unit given is
                  //     4.[pi].10*-7.N/A2
                  else if (attrs['csCode_'] === '[mu_0]') {
                      attrs['baseFactor_'] = 4 * Math.PI * Math.pow(10, -7);
                      attrs['csUnitString_'] = 'N/A2';
                    } // The unit string parser will use the unit(s) named in the
              // string to create a new unit with the appropriate dimension
              // object and magnitude before it's multiplied by the one
              // specified in the input node.


              try {
                let retObj = uStrParser.parseString(attrs['csUnitString_'], 'validate', false);
                let ret = retObj[0];
                let retString = retObj[1];
                let retMsg = retObj[2]; // Get the dimension object and magnitude (and adjust by
                // specified magnitude factor) from the unit created and
                // assign them to the attributes we'll use to create the
                // unit for this listing.

                if (ret) {
                  attrs['dim_'] = ret.getProperty('dim_');
                  let newMag = ret.getProperty('magnitude_');
                  newMag *= attrs['baseFactor_'];
                  attrs['magnitude_'] = newMag;
                  haveUnit = true;
                } // if there's no unit, report an error
                else {
                    attrs['defError_'] = true;
                    console.log(`unit definition error; code = ${attrs['csCode_']}; ` + `msg = ${retMsg}`);
                    attrs['dim_'] = null;
                    attrs['magnitude_'] = null;
                  }
              } catch (err) {
                console.log('error thrown from unit parsing code for unit name ' + attrs['name_'] + '\n' + err.message);
                stopNow = true;
              }
            } // end if there is a unit string to parse

          } // end if this is not a dimless unit


      if (haveUnit) {
        // Now create the unit we want based on the attributes we've
        // accumulated from the xml input and from figuring the dimension
        // and magnitude.  Add it to the unit tables
        let newUnit = new Unit(attrs);
        utab.addUnit(newUnit); // for now, create a list of the units created and save it to a file
        // for debugging.  This is a temporary file.

        let uList = utab.printUnits();
        fs.writeFileSync('UnitsList.txt', uList, {
          encoding: 'utf8',
          mode: 0o666,
          flag: 'w'
        });
      } // end if have a parsed unit

    } // end for a => - to alen

  } // end parseUnitStrings

  /**
   * This writes out the ucumDefs data file, which contains all prefixes and
   * units (base units and others) read and parsed from the XML file.
   *
   * This creates or replace the file "ucumDefs.json" in the data directory.
   */


  writeJsonFile() {
    let licenseText = "The following data (prefixes and units) was generated " + "by the UCUM LHC code from the UCUM data and selected " + "LOINC combinations of UCUM units.  The license for " + "the UCUM LHC code (demo and library code as well as " + "the combined units) is located at " + "https://github.com/lhncbc/ucum-lhc/blob/LICENSE.md.";
    let pfxTabs = PrefixTables.getInstance();
    let pfxArray = pfxTabs.allPrefixesByCode();
    let uTabs = UnitTables.getInstance();
    let uArray = uTabs.allUnitsByDef();
    let defsHash = {
      'license': licenseText,
      'prefixes': packArray(pfxArray),
      'units': packArray(uArray)
    };
    let dt = new Date();
    jsonfile.writeFileSync('../data/ucumDefs.json', defsHash, {
      spaces: 2,
      encoding: 'utf8',
      mode: 0o644,
      flag: 'w'
    });
    jsonfile.writeFileSync('../data/ucumDefs.min.json', defsHash, {
      encoding: 'utf8',
      mode: 0o644,
      flag: 'w'
    });
  } // end writeJsonFile

  /**
   * This writes out the ucumDefs data file, which contains all prefixes and
   * units (base units and others) read and parsed from the XML file.
   *
   * This creates the file in the data directory and appends the
   * current Date object value to "ucumDefs" so that this does not run
   * into problems with a previously existing file.
   */


  writeVersionText() {
    let rootNode = xmlInput_;
    let versionNum = rootNode.attr.version;
    let revNum = rootNode.attr.revision;
    revNum = revNum.replace('$Revision:', '');
    revNum = revNum.replace('$', '');
    revNum = revNum.trim();
    let rootString = rootNode.toString({
      compressed: true
    });
    let dateIdx = rootString.indexOf('$Date:');
    rootString = rootString.substr(dateIdx + 6);
    let nextDolIdx = rootString.indexOf('$');
    let revDate = rootString.substr(0, nextDolIdx).replace('$', '');
    revDate = revDate.trim();
    let versionText = `version ${versionNum}, revision ${revNum}, ` + `dated ${revDate}`;
    fs.writeFileSync('../data/ucumEssenceVersion.txt', versionText, {
      encoding: 'utf8',
      mode: 0o644,
      flag: 'w'
    });
  } // end writeVersionText


} // end UcumXmlDocument

/**
 *  This function exists ONLY until the original UcumXmlDocument constructor
 *  is called for the first time.  It's defined here in case getInstance
 *  is called before the constructor.   This calls the constructor.
 *
 *  The constructor redefines the getInstance function to return the
 *  singleton UcumXmlDocument object.  This is based on the UnitTables singleton
 *  implementation; see more detail in the UnitTables constructor description.
 *
 *  @return the singleton UcumXmlDocument object.
 */


exports.UcumXmlDocument = UcumXmlDocument;

UcumXmlDocument.getInstance = function () {
  return new UcumXmlDocument();
}; // Perform the first request for the document object, to get the
// getInstance method set.


UcumXmlDocument.getInstance();
//# sourceMappingURL=ucumXmlDocument.js.map
